root/livinglogic.python.tipimaid/liaalh.py @ 75:228a3120d62d

Revision 75:228a3120d62d, 7.1 KB (checked in by Nikolas Tautenhahn <nik@…>, 10 years ago)

added option to create symlinks pointing to most recent log files, fixed a bug which caused a crash when liaalh was feeded an empty line

  • Property exe set to *
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Apache sends a SIGTERM to its piped logger when it exits - when apache is killed with -9, it doesn't send anything
5
6import sys, os, datetime, errno, bisect, re, gzip, signal, time
7
8
9class LogLine(tuple):
10    def __lt__(self, other):
11        return self[0] < other[0]
12
13    def __le__(self, other):
14        return self[0] <= other[0]
15
16
17class Buffer(object):
18    def __init__(self, pattern='', gzip_logs=None, buffertime=0, stream=sys.stdin, utcrotate=False, symlinkpattern=None):
19        self.pattern = pattern
20        self.gzip_logs = gzip_logs
21        self.data = []
22        self.servers = {}
23        self.buffertime = datetime.timedelta(seconds=buffertime)
24        self.stream = stream
25        self.re_find_date = re.compile(" \[(.*?)\] ")
26        if gzip_logs is not None and not pattern.endswith(".gz"):
27            self.pattern = "%s.gz" % self.pattern
28        self.utcrotate = utcrotate
29        self.updateutcoffset()
30        self.handlevirtualhost = "%v" in pattern
31        self.symlinkpattern = symlinkpattern
32        if buffertime > 0:
33            self.run = self.run_buffered
34        else:
35            self.run = self.run_unbuffered
36
37    def openfile(self, filename, server):
38        try:
39            f = open(filename, "a", 1)
40        except IOError, exc:
41            if exc.errno == errno.ENOENT:
42                os.makedirs(os.path.dirname(filename))
43                f = open(filename, "a", 1)
44            else:
45                raise
46        if self.symlinkpattern:
47            symlinkname = self.symlinkpattern.replace("%v", server)
48            try:
49                os.symlink(os.path.abspath(filename), symlinkname)
50            except OSError, exc:
51                if exc.errno == errno.ENOENT:
52                    os.makedirs(os.path.dirname(symlinkname))
53                    os.symlink(os.path.abspath(filename), symlinkname)
54                elif exc.errno == errno.EEXIST:
55                    os.remove(symlinkname)
56                    os.symlink(os.path.abspath(filename), symlinkname)
57                else:
58                    raise
59        if self.gzip_logs is not None:
60            f = gzip.GzipFile(fileobj=f, compresslevel=self.gzip_logs)
61            f.name = f.fileobj.name # gzip-fileobjects don't have a name attribute
62        return f
63
64    def readlines(self):
65        while True:
66            try:
67                line = self.stream.readline()
68            except IOError, exc:
69                if exc[0] == errno.EINTR:
70                    continue
71                else:
72                    raise
73            if not line:
74                break
75            if self.handlevirtualhost:
76                ret = line.split(None, 1)
77                if ret == []:
78                    ret = [None, None]
79                yield ret
80            else:
81                yield (None, line)
82
83    def writeline(self, utclogdate, server, line):
84        if not self.utcrotate:
85            utclogdate += self.localutcoffset
86        filename = utclogdate.strftime(self.pattern)
87        if self.handlevirtualhost:
88            filename = filename.replace("%v", server)
89        if server in self.servers:
90            f = self.servers[server]
91            if f.name != filename:
92                f.flush()
93                f.close()
94                self.updateutcoffset()
95                self.servers[server] = f = self.openfile(filename, server)
96        else:
97            self.servers[server] = f = self.openfile(filename, server)
98        f.write(line)
99        f.flush()
100
101    def run_unbuffered(self):
102        for (server, data) in self.readlines():
103            try:
104                if server is None and data is None: # got an empty line
105                    continue
106                datestring = self.re_find_date.findall(data)[0]
107                utclogdate = self.apachedate2utc(datestring)
108                self.writeline(utclogdate, server, data)
109            except IndexError, exc: # index error bc we didn't find an apache date -> malformed logline
110                continue # ignore it
111
112    def run_buffered(self):
113        signal.signal(signal.SIGHUP, self.sighandler)
114        signal.signal(signal.SIGTERM, self.sighandler)
115        signal.signal(signal.SIGINT, self.sighandler)
116        signal.signal(signal.SIGQUIT, self.sighandler)
117        try:
118            for (server, data) in self.readlines():
119                try:
120                    if server is None and data is None: # got an empty line
121                        continue
122                    datestring = self.re_find_date.findall(data)[0]
123                    utclogdate = self.apachedate2utc(datestring)
124                    self.add(LogLine((utclogdate, server, data)))
125                except IndexError, exc: # index error bc we didn't find an apache date -> malformed logline
126                    continue # ignore it
127                except Exception, exc:
128                    print server, data
129                    raise
130        except Exception, exc:
131            self.flushall()
132            raise
133
134    def add(self, logline):
135        if not self.data or self.data[-1] <= logline:
136            self.data.append(logline)
137        else:
138            bisect.insort_right(self.data, logline)
139        self.flush()
140
141    def flushall(self):
142        for (utclogdate, server, logdata) in self.data:
143            self.writeline(utclogdate, server, logdata)
144        self.data = []
145
146    def flush(self):
147        while self.data:
148            line = self.data[0]
149            (utclogdate, server, logdata) = line
150            if datetime.datetime.utcnow() - utclogdate < self.buffertime:
151                return
152            self.writeline(utclogdate, server, logdata)
153            self.data.pop(0)
154
155    def apachedate2utc(self, d):
156        temp = d.split()
157        utcdate = datetime.datetime(*(time.strptime(temp[0], "%d/%b/%Y:%H:%M:%S")[0:6])) # support ancient distributions with python < 2.5
158        minsoff = int("%s%s" % (temp[1][0], temp[1][-2:]))
159        hrsoff = int("%s%s" % (temp[1][0], temp[1][1:3]))
160        utcdate -= datetime.timedelta(hours=hrsoff, minutes=minsoff)
161        return utcdate
162
163    def updateutcoffset(self):
164        temp = datetime.datetime.now() - datetime.datetime.utcnow()
165        self.localutcoffset = datetime.timedelta(days=temp.days, seconds=temp.seconds+1, microseconds=0)
166
167    def sighandler(self, signum, frame):
168        self.flushall()
169        if signum in (signal.SIGQUIT, signal.SIGTERM, signal.SIGINT):
170            sys.exit(signum)
171
172
173def main(args=None):
174    import optparse
175    p = optparse.OptionParser(usage="usage: %prog filename-pattern [options]\nIf you use virtual hosts please note that the virtual host column (%v) has to be the first column in every logfile!")
176    p.add_option("-z", "--gzip", dest="gzip", type="int", action="store", help="If set, logs are gzipped with this compression level (lowest: 1, highest: 9)", default=None)
177    p.add_option("-b", "--buffertime", dest="buffertime", type="int", action="store", help="Time in seconds for which log entries are buffered, default=0. Set to 0 to disable buffering", default=0)
178    p.add_option("-u", "--utcrotate", dest="utcrotate", action="store_true", help="If set, UTC time determines the time for filenames and rotation. Otherwise local time is used.", default=False)
179    p.add_option("-s", "--symlink", dest="symlinkpattern", action="store", help="""Create a symlink pointing to the most recent log file (of each virtual host if you use %v). Needs a filename pattern for the symlink but only "%v" is allowed here as symlinks which include time/date data are useless, e.g. %v/access.log or symlinks/access-%v.log""", default=None)
180    (options, args) = p.parse_args()
181    if options.gzip is not None:
182        if options.gzip < 1:
183            options.gzip = 1
184        elif options.gzip > 9:
185            options.gzip = 9
186    if len(args) != 1:
187        p.print_usage(sys.stderr)
188        sys.stderr.write("%s: We need a filename-pattern\n" % p.get_prog_name())
189        sys.stderr.flush()
190        return 1
191    if options.symlinkpattern is not None and "%v" in args[0] and ("%v" in options.symlinkpattern and options.symlinkpattern.count("%") > 1) or ("%v" not in options.symlinkpattern):
192        p.print_usage(sys.stderr)
193        sys.stderr.write("%s: If you split logfiles by virtual hosts you should use virtual hosts (%v) in the symlink-pattern as well. But you shouldn't use any patterns for time/date data.\n" % p.get_prog_name())
194        sys.stderr.flush()
195        return 1
196
197    buf = Buffer(pattern=args[0], gzip_logs=options.gzip, buffertime=options.buffertime, utcrotate=options.utcrotate, symlinkpattern=options.symlinkpattern)
198    buf.run()
199
200
201if __name__ == "__main__":
202    sys.exit(main())
Note: See TracBrowser for help on using the browser.