1
# Copyright 2010 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Create uniquely named log files on disk."""
7
__all__ = ['UniqueFileAllocator']
23
# the section of the ID before the instance identifier is the
24
# days since the epoch, which is defined as the start of 2006.
25
epoch = datetime.datetime(2006, 01, 01, 00, 00, 00, tzinfo=UTC)
28
class UniqueFileAllocator:
29
"""Assign unique file names to logs being written from an app/script.
31
UniqueFileAllocator causes logs written from one process to be uniquely
32
named. It is not safe for use in multiple processes with the same output
33
root - each process must have a unique output root.
36
def __init__(self, output_root, log_type, log_subtype):
37
"""Create a UniqueFileAllocator.
39
:param output_root: The root directory that logs should be placed in.
40
:param log_type: A string to use as a prefix in the ID assigned to new
41
logs. For instance, "OOPS".
42
:param log_subtype: A string to insert in the generate log filenames
43
between the day number and the serial. For instance "T" for
46
self._lock = threading.Lock()
47
self._output_root = output_root
49
self._last_output_dir = None
50
self._log_type = log_type
51
self._log_subtype = log_subtype
54
def _findHighestSerialFilename(self, directory=None, time=None):
55
"""Find details of the last log present in the given directory.
57
This function only considers logs with the currently
58
configured log_subtype.
60
One of directory, time must be supplied.
62
:param directory: Look in this directory.
63
:param time: Look in the directory that a log written at this time
64
would have been written to. If supplied, supercedes directory.
65
:return: a tuple (log_serial, log_filename), which will be (0,
66
None) if no logs are found. log_filename is a usable path, not
70
directory = self.output_dir(time)
71
prefix = self.get_log_infix()
74
for filename in os.listdir(directory):
75
logid = filename.rsplit('.', 1)[1]
76
if not logid.startswith(prefix):
78
logid = logid[len(prefix):]
79
if logid.isdigit() and (lastid is None or int(logid) > lastid):
81
lastfilename = filename
82
if lastfilename is not None:
83
lastfilename = os.path.join(directory, lastfilename)
84
return lastid, lastfilename
86
def _findHighestSerial(self, directory):
87
"""Find the last serial actually applied to disk in directory.
89
The purpose of this function is to not repeat sequence numbers
90
if the logging application is restarted.
92
This method is not thread safe, and only intended to be called
93
from the constructor (but it is called from other places in
96
return self._findHighestSerialFilename(directory)[0]
98
def getFilename(self, log_serial, time):
99
"""Get the filename for a given log serial and time."""
100
log_subtype = self.get_log_infix()
101
# TODO: Calling output_dir causes a global lock to be taken and a
102
# directory scan, which is bad for performance. It would be better
103
# to have a split out 'directory name for time' function which the
104
# 'want to use this directory now' function can call.
105
output_dir = self.output_dir(time)
106
second_in_day = time.hour * 3600 + time.minute * 60 + time.second
108
output_dir, '%05d.%s%s' % (
109
second_in_day, log_subtype, log_serial))
111
def get_log_infix(self):
112
"""Return the current log infix to use in ids and file names."""
113
return self._log_subtype + self._log_token
115
def newId(self, now=None):
116
"""Returns an (id, filename) pair for use by the caller.
118
The ID is composed of a short string to identify the Launchpad
119
instance followed by an ID that is unique for the day.
121
The filename is composed of the zero padded second in the day
122
followed by the ID. This ensures that reports are in date order when
126
now = now.astimezone(UTC)
128
now = datetime.datetime.now(UTC)
129
# We look up the error directory before allocating a new ID,
130
# because if the day has changed, errordir() will reset the ID
135
self._last_serial += 1
136
newid = self._last_serial
139
subtype = self.get_log_infix()
140
day_number = (now - epoch).days + 1
141
log_id = '%s-%d%s%d' % (self._log_type, day_number, subtype, newid)
142
filename = self.getFilename(newid, now)
143
return log_id, filename
145
def output_dir(self, now=None):
146
"""Find or make the directory to allocate log names in.
148
Log names are assigned within subdirectories containing the date the
152
now = now.astimezone(UTC)
154
now = datetime.datetime.now(UTC)
155
date = now.strftime('%Y-%m-%d')
156
result = os.path.join(self._output_root, date)
157
if result != self._last_output_dir:
160
self._last_output_dir = result
161
# make sure the directory exists
165
if e.errno != errno.EEXIST:
167
# Make sure the directory permission is set to: rwxr-xr-x
169
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP |
170
stat.S_IROTH | stat.S_IXOTH)
171
os.chmod(result, permission)
172
# TODO: Note that only one process can do this safely: its not
173
# cross-process safe, and also not entirely threadsafe:
174
# another # thread that has a new log and hasn't written it
175
# could then use that serial number. We should either make it
176
# really safe, or remove the contention entirely and log
177
# uniquely per thread of execution.
178
self._last_serial = self._findHighestSerial(result)
183
def listRecentReportFiles(self):
184
now = datetime.datetime.now(UTC)
185
yesterday = now - datetime.timedelta(days=1)
186
directories = [self.output_dir(now), self.output_dir(yesterday)]
187
for directory in directories:
188
report_names = os.listdir(directory)
189
for name in sorted(report_names, reverse=True):
190
yield directory, name
192
def setToken(self, token):
193
"""Append a string to the log subtype in filenames and log ids.
195
:param token: a string to append..
196
Scripts that run multiple processes can use this to create a
197
unique identifier for each process.
199
self._log_token = token