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