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 |