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 |