~launchpad-pqm/launchpad/devel

10637.3.1 by Guilherme Salgado
Use the default python version instead of a hard-coded version
1
#!/usr/bin/python
8452.3.3 by Karl Fogel
* utilities/: Add copyright header block to source files that were
2
#
8687.15.2 by Karl Fogel
In files modified by r8688, change "<YEARS>" to "2009", as per
3
# Copyright 2009 Canonical Ltd.  This software is licensed under the
8687.15.3 by Karl Fogel
Shorten the copyright header block to two lines.
4
# GNU Affero General Public License version 3 (see the file LICENSE).
8452.3.3 by Karl Fogel
* utilities/: Add copyright header block to source files that were
5
1782 by Canonical.com Patch Queue Manager
[r=jamesh] database log watch script'n'framework
6
"""
7
Watch live PostgreSQL logs for interesting stuff
8
"""
9
10
import re, sys
11
from optparse import OptionParser
12
import subprocess
13
14
def get_options(args=None):
15
    parser = OptionParser()
16
    parser.add_option("-l", "--logfile", dest="logfile",
17
            default="/var/log/postgresql/postgres.log",
18
            metavar="LOG", help="Monitor LOG instead of the default"
19
            )
20
    parser.add_option("--slow", dest="slow",
21
            type="float", default=100.0, metavar="TIME",
22
            help="Report slow queries taking over TIME seconds",
23
            )
24
    (options, args) = parser.parse_args(args)
25
    return options
26
27
def generate_loglines(logfile):
28
    """Generator returning the next line in the logfile (blocking)"""
29
    cmd = subprocess.Popen(
30
            ['tail', '-f', logfile],
31
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
32
    while cmd.poll() is None:
33
        yield cmd.stdout.readline()
34
    if cmd.returncode != 0:
35
        print >> sys.stderr, cmd.stderr.read()
36
        raise RuntimeError("tail returned %d" % cmd.returncode)
37
38
39
class Process(object):
40
    statement = None
41
    duration = None
42
    connection = None
43
    auth = None
44
45
    def __init__(self, pid):
46
        self.pid = pid
47
48
49
class Watcher(object):
50
    _line_re = re.compile("""
51
        ^\d{4}-\d\d-\d\d \s \d\d:\d\d:\d\d \s 
52
        \[(?P<pid>\d+)\] \s (?P<type>LOG|ERROR|DETAIL): \s+ (?P<rest>.*)$
53
        """, re.X)
54
55
    _statement_re = re.compile("""
56
        ^statement: \s (?P<statement>.*)$
57
        """, re.X)
58
59
    _duration_re = re.compile("""
60
        ^duration: \s (?P<duration>\d+\.\d+) \s ms$
61
        """, re.X)
62
63
    _connection_received_re = re.compile("""
64
        ^connection \s received: \s+ (?P<connection>.*)$
65
        """, re.X)
66
67
    _connection_authorized_re = re.compile("""
68
        ^connection \s authorized: \s+ (?P<auth>.*)$
69
        """, re.X)
70
71
    _ignored_rest_re = re.compile("""
72
        ^(received \s | ERROR: \s | unexpected \s EOF \s) .*$
73
        """, re.X)
74
75
    _ignored_statements_re = re.compile("""
76
        ^(BEGIN.*|END)$
77
        """, re.X)
78
79
    def __init__(self, options):
80
        self.processes = {}
81
        self.options = options
82
        self.previous_process = None
83
84
    def run(self):
85
        lines = generate_loglines(options.logfile)
86
        for line in lines:
87
            self.feed(line)
88
89
    def feed(self, line):
90
91
        # Handle continuations of previous statement
92
        if line.startswith('\t'):
93
            if self.previous_process is not None:
94
                self.previous_process.statement += '\n%s' % line[1:-1]
95
            return
96
97
        match = self._line_re.search(line)
98
        if match is None:
99
            raise ValueError('Badly formatted line %r' % (line,))
100
101
        t = match.group('type')
102
        if t in ['ERROR', 'DETAIL']:
103
            return
104
        if t != 'LOG':
105
            raise ValueError('Unknown line type %s (%r)' % (t, line))
106
107
        pid = int(match.group('pid'))
108
        rest = match.group('rest')
109
        
110
        process = self.processes.get(pid, None)
111
        if process is None:
112
            process = Process(pid)
113
            self.processes[pid] = process
114
        self.previous_process = process
115
        
116
        match = self._statement_re.search(rest)
117
        if match is not None:
118
            statement = match.group('statement')
119
            if process.statement:
120
                process.statement += '\n%s' % statement
121
            else:
122
                process.statement = statement
123
            return
124
125
        match = self._duration_re.search(rest)
126
        if match is not None:
127
            process.duration = float(match.group('duration'))
128
            self.reportDuration(process)
129
            self.previous_process = None
130
            del self.processes[process.pid]
131
            return
132
133
        match = self._connection_received_re.search(rest)
134
        if match is not None:
135
            process.connection = match.group('connection')
136
            return
137
138
        match = self._connection_authorized_re.search(rest)
139
        if match is not None:
140
            process.auth = match.group('auth')
141
            return
142
143
        match = self._ignored_rest_re.search(rest)
144
        if match is not None:
145
            return
146
147
        raise ValueError('Unknown entry: %r' % (rest,))
148
149
    def reportDuration(self, process):
150
        """Report a slow statement if it is above a threshold"""
151
        if self.options.slow is None or process.statement is None:
152
            return
153
154
        match = self._ignored_statements_re.search(process.statement)
155
        if match is not None:
156
            return
157
158
        if process.duration > options.slow:
159
            print '[%5d] %s' % (process.pid, process.statement)
160
            print '        Duration: %0.3f' % (process.duration,)
161
162
if __name__ == '__main__':
163
    options = get_options()
164
165
    watcher = Watcher(options)
166
    try:
167
        watcher.run()
168
    except KeyboardInterrupt:
169
        pass
170