~paulgear/python-oops-datedir-repo/variable-retention-period

« back to all changes in this revision

Viewing changes to oops_datedir_repo/uniquefileallocator.py

  • Committer: Robert Collins
  • Date: 2011-08-15 05:30:39 UTC
  • Revision ID: robertc@robertcollins.net-20110815053039-inldp90usrsu36x6
Move disk related code from python-oops.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2010, 2011, Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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).
 
16
 
 
17
 
 
18
"""Create uniquely named log files on disk."""
 
19
 
 
20
 
 
21
__all__ = ['UniqueFileAllocator']
 
22
 
 
23
__metaclass__ = type
 
24
 
 
25
 
 
26
import datetime
 
27
import errno
 
28
import os.path
 
29
import stat
 
30
import threading
 
31
 
 
32
import pytz
 
33
 
 
34
 
 
35
UTC = pytz.utc
 
36
 
 
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)
 
40
 
 
41
 
 
42
class UniqueFileAllocator:
 
43
    """Assign unique file names to logs being written from an app/script.
 
44
 
 
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.
 
48
    """
 
49
 
 
50
    def __init__(self, output_root, log_type, log_subtype):
 
51
        """Create a UniqueFileAllocator.
 
52
 
 
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
 
58
            "Testing".
 
59
        """
 
60
        self._lock = threading.Lock()
 
61
        self._output_root = output_root
 
62
        self._last_serial = 0
 
63
        self._last_output_dir = None
 
64
        self._log_type = log_type
 
65
        self._log_subtype = log_subtype
 
66
        self._log_token = ""
 
67
 
 
68
    def _findHighestSerialFilename(self, directory=None, time=None):
 
69
        """Find details of the last log present in the given directory.
 
70
 
 
71
        This function only considers logs with the currently
 
72
        configured log_subtype.
 
73
 
 
74
        One of directory, time must be supplied.
 
75
 
 
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
 
81
            simply the basename.
 
82
        """
 
83
        if directory is None:
 
84
            directory = self.output_dir(time)
 
85
        prefix = self.get_log_infix()
 
86
        lastid = 0
 
87
        lastfilename = None
 
88
        for filename in os.listdir(directory):
 
89
            logid = filename.rsplit('.', 1)[1]
 
90
            if not logid.startswith(prefix):
 
91
                continue
 
92
            logid = logid[len(prefix):]
 
93
            if logid.isdigit() and (lastid is None or int(logid) > lastid):
 
94
                lastid = int(logid)
 
95
                lastfilename = filename
 
96
        if lastfilename is not None:
 
97
            lastfilename = os.path.join(directory, lastfilename)
 
98
        return lastid, lastfilename
 
99
 
 
100
    def _findHighestSerial(self, directory):
 
101
        """Find the last serial actually applied to disk in directory.
 
102
 
 
103
        The purpose of this function is to not repeat sequence numbers
 
104
        if the logging application is restarted.
 
105
 
 
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
 
108
        integration tests).
 
109
        """
 
110
        return self._findHighestSerialFilename(directory)[0]
 
111
 
 
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
 
121
        return os.path.join(
 
122
            output_dir, '%05d.%s%s' % (
 
123
            second_in_day, log_subtype, log_serial))
 
124
 
 
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
 
128
 
 
129
    def newId(self, now=None):
 
130
        """Returns an (id, filename) pair for use by the caller.
 
131
 
 
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.
 
134
 
 
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
 
137
        sorted lexically.
 
138
        """
 
139
        if now is not None:
 
140
            now = now.astimezone(UTC)
 
141
        else:
 
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
 
145
        # counter to zero.
 
146
        self.output_dir(now)
 
147
        self._lock.acquire()
 
148
        try:
 
149
            self._last_serial += 1
 
150
            newid = self._last_serial
 
151
        finally:
 
152
            self._lock.release()
 
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
 
158
 
 
159
    def output_dir(self, now=None):
 
160
        """Find or make the directory to allocate log names in.
 
161
 
 
162
        Log names are assigned within subdirectories containing the date the
 
163
        assignment happened.
 
164
        """
 
165
        if now is not None:
 
166
            now = now.astimezone(UTC)
 
167
        else:
 
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:
 
172
            self._lock.acquire()
 
173
            try:
 
174
                self._last_output_dir = result
 
175
                # make sure the directory exists
 
176
                try:
 
177
                    os.makedirs(result)
 
178
                except OSError, e:
 
179
                    if e.errno != errno.EEXIST:
 
180
                        raise
 
181
                # Make sure the directory permission is set to: rwxr-xr-x
 
182
                permission = (
 
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)
 
193
            finally:
 
194
                self._lock.release()
 
195
        return result
 
196
 
 
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
 
205
 
 
206
    def setToken(self, token):
 
207
        """Append a string to the log subtype in filenames and log ids.
 
208
 
 
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.
 
212
        """
 
213
        self._log_token = token