1
# Copyright (c) 2010, 2011, Canonical Ltd
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU Affero General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU Affero General Public License for more details.
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
# GNU Affero General Public License version 3 (see the file LICENSE).
18
"""Create uniquely named log files on disk."""
21
__all__ = ['UniqueFileAllocator']
37
# the section of the ID before the instance identifier is the
38
# days since the epoch, which is defined as the start of 2006.
39
epoch = datetime.datetime(2006, 01, 01, 00, 00, 00, tzinfo=UTC)
42
class UniqueFileAllocator:
43
"""Assign unique file names to logs being written from an app/script.
45
UniqueFileAllocator causes logs written from one process to be uniquely
46
named. It is not safe for use in multiple processes with the same output
47
root - each process must have a unique output root.
50
def __init__(self, output_root, log_type, log_subtype):
51
"""Create a UniqueFileAllocator.
53
:param output_root: The root directory that logs should be placed in.
54
:param log_type: A string to use as a prefix in the ID assigned to new
55
logs. For instance, "OOPS".
56
:param log_subtype: A string to insert in the generate log filenames
57
between the day number and the serial. For instance "T" for
60
self._lock = threading.Lock()
61
self._output_root = output_root
63
self._last_output_dir = None
64
self._log_type = log_type
65
self._log_subtype = log_subtype
68
def _findHighestSerialFilename(self, directory=None, time=None):
69
"""Find details of the last log present in the given directory.
71
This function only considers logs with the currently
72
configured log_subtype.
74
One of directory, time must be supplied.
76
:param directory: Look in this directory.
77
:param time: Look in the directory that a log written at this time
78
would have been written to. If supplied, supercedes directory.
79
:return: a tuple (log_serial, log_filename), which will be (0,
80
None) if no logs are found. log_filename is a usable path, not
84
directory = self.output_dir(time)
85
prefix = self.get_log_infix()
88
for filename in os.listdir(directory):
89
logid = filename.rsplit('.', 1)[1]
90
if not logid.startswith(prefix):
92
logid = logid[len(prefix):]
93
if logid.isdigit() and (lastid is None or int(logid) > lastid):
95
lastfilename = filename
96
if lastfilename is not None:
97
lastfilename = os.path.join(directory, lastfilename)
98
return lastid, lastfilename
100
def _findHighestSerial(self, directory):
101
"""Find the last serial actually applied to disk in directory.
103
The purpose of this function is to not repeat sequence numbers
104
if the logging application is restarted.
106
This method is not thread safe, and only intended to be called
107
from the constructor (but it is called from other places in
110
return self._findHighestSerialFilename(directory)[0]
112
def getFilename(self, log_serial, time):
113
"""Get the filename for a given log serial and time."""
114
log_subtype = self.get_log_infix()
115
# TODO: Calling output_dir causes a global lock to be taken and a
116
# directory scan, which is bad for performance. It would be better
117
# to have a split out 'directory name for time' function which the
118
# 'want to use this directory now' function can call.
119
output_dir = self.output_dir(time)
120
second_in_day = time.hour * 3600 + time.minute * 60 + time.second
122
output_dir, '%05d.%s%s' % (
123
second_in_day, log_subtype, log_serial))
125
def get_log_infix(self):
126
"""Return the current log infix to use in ids and file names."""
127
return self._log_subtype + self._log_token
129
def newId(self, now=None):
130
"""Returns an (id, filename) pair for use by the caller.
132
The ID is composed of a short string to identify the Launchpad
133
instance followed by an ID that is unique for the day.
135
The filename is composed of the zero padded second in the day
136
followed by the ID. This ensures that reports are in date order when
140
now = now.astimezone(UTC)
142
now = datetime.datetime.now(UTC)
143
# We look up the error directory before allocating a new ID,
144
# because if the day has changed, errordir() will reset the ID
149
self._last_serial += 1
150
newid = self._last_serial
153
subtype = self.get_log_infix()
154
day_number = (now - epoch).days + 1
155
log_id = '%s-%d%s%d' % (self._log_type, day_number, subtype, newid)
156
filename = self.getFilename(newid, now)
157
return log_id, filename
159
def output_dir(self, now=None):
160
"""Find or make the directory to allocate log names in.
162
Log names are assigned within subdirectories containing the date the
166
now = now.astimezone(UTC)
168
now = datetime.datetime.now(UTC)
169
date = now.strftime('%Y-%m-%d')
170
result = os.path.join(self._output_root, date)
171
if result != self._last_output_dir:
174
self._last_output_dir = result
175
# make sure the directory exists
179
if e.errno != errno.EEXIST:
181
# Make sure the directory permission is set to: rwxr-xr-x
183
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
184
stat.S_IROTH | stat.S_IXOTH)
185
os.chmod(result, permission)
186
# TODO: Note that only one process can do this safely: its not
187
# cross-process safe, and also not entirely threadsafe:
188
# another # thread that has a new log and hasn't written it
189
# could then use that serial number. We should either make it
190
# really safe, or remove the contention entirely and log
191
# uniquely per thread of execution.
192
self._last_serial = self._findHighestSerial(result)
197
def listRecentReportFiles(self):
198
now = datetime.datetime.now(UTC)
199
yesterday = now - datetime.timedelta(days=1)
200
directories = [self.output_dir(now), self.output_dir(yesterday)]
201
for directory in directories:
202
report_names = os.listdir(directory)
203
for name in sorted(report_names, reverse=True):
204
yield directory, name
206
def setToken(self, token):
207
"""Append a string to the log subtype in filenames and log ids.
209
:param token: a string to append..
210
Scripts that run multiple processes can use this to create a
211
unique identifier for each process.
213
self._log_token = token