##// END OF EJS Templates
blackbox: optionally log event source
timeless -
r28305:f5ae291d default
parent child Browse files
Show More
@@ -1,233 +1,239
1 # blackbox.py - log repository events to a file for post-mortem debugging
1 # blackbox.py - log repository events to a file for post-mortem debugging
2 #
2 #
3 # Copyright 2010 Nicolas Dumazet
3 # Copyright 2010 Nicolas Dumazet
4 # Copyright 2013 Facebook, Inc.
4 # Copyright 2013 Facebook, Inc.
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 """log repository events to a blackbox for debugging
9 """log repository events to a blackbox for debugging
10
10
11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
11 Logs event information to .hg/blackbox.log to help debug and diagnose problems.
12 The events that get logged can be configured via the blackbox.track config key.
12 The events that get logged can be configured via the blackbox.track config key.
13
13
14 Examples::
14 Examples::
15
15
16 [blackbox]
16 [blackbox]
17 track = *
17 track = *
18 # dirty is *EXPENSIVE* (slow);
18 # dirty is *EXPENSIVE* (slow);
19 # each log entry indicates `+` if the repository is dirty, like :hg:`id`.
19 # each log entry indicates `+` if the repository is dirty, like :hg:`id`.
20 dirty = True
20 dirty = True
21 # record the source of log messages
22 logsource = True
21
23
22 [blackbox]
24 [blackbox]
23 track = command, commandfinish, commandexception, exthook, pythonhook
25 track = command, commandfinish, commandexception, exthook, pythonhook
24
26
25 [blackbox]
27 [blackbox]
26 track = incoming
28 track = incoming
27
29
28 [blackbox]
30 [blackbox]
29 # limit the size of a log file
31 # limit the size of a log file
30 maxsize = 1.5 MB
32 maxsize = 1.5 MB
31 # rotate up to N log files when the current one gets too big
33 # rotate up to N log files when the current one gets too big
32 maxfiles = 3
34 maxfiles = 3
33
35
34 """
36 """
35
37
36 from __future__ import absolute_import
38 from __future__ import absolute_import
37
39
38 import errno
40 import errno
39 import re
41 import re
40
42
41 from mercurial.i18n import _
43 from mercurial.i18n import _
42 from mercurial.node import hex
44 from mercurial.node import hex
43
45
44 from mercurial import (
46 from mercurial import (
45 cmdutil,
47 cmdutil,
46 ui as uimod,
48 ui as uimod,
47 util,
49 util,
48 )
50 )
49
51
50 cmdtable = {}
52 cmdtable = {}
51 command = cmdutil.command(cmdtable)
53 command = cmdutil.command(cmdtable)
52 # Note for extension authors: ONLY specify testedwith = 'internal' for
54 # Note for extension authors: ONLY specify testedwith = 'internal' for
53 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
55 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
54 # be specifying the version(s) of Mercurial they are tested with, or
56 # be specifying the version(s) of Mercurial they are tested with, or
55 # leave the attribute unspecified.
57 # leave the attribute unspecified.
56 testedwith = 'internal'
58 testedwith = 'internal'
57 lastui = None
59 lastui = None
58
60
59 filehandles = {}
61 filehandles = {}
60
62
61 def _openlog(vfs):
63 def _openlog(vfs):
62 path = vfs.join('blackbox.log')
64 path = vfs.join('blackbox.log')
63 if path in filehandles:
65 if path in filehandles:
64 return filehandles[path]
66 return filehandles[path]
65 filehandles[path] = fp = vfs('blackbox.log', 'a')
67 filehandles[path] = fp = vfs('blackbox.log', 'a')
66 return fp
68 return fp
67
69
68 def _closelog(vfs):
70 def _closelog(vfs):
69 path = vfs.join('blackbox.log')
71 path = vfs.join('blackbox.log')
70 fp = filehandles[path]
72 fp = filehandles[path]
71 del filehandles[path]
73 del filehandles[path]
72 fp.close()
74 fp.close()
73
75
74 def wrapui(ui):
76 def wrapui(ui):
75 class blackboxui(ui.__class__):
77 class blackboxui(ui.__class__):
76 def __init__(self, src=None):
78 def __init__(self, src=None):
77 super(blackboxui, self).__init__(src)
79 super(blackboxui, self).__init__(src)
78 if src is None:
80 if src is None:
79 self._partialinit()
81 self._partialinit()
80 else:
82 else:
81 self._bbfp = src._bbfp
83 self._bbfp = src._bbfp
82 self._bbrepo = src._bbrepo
84 self._bbrepo = src._bbrepo
83 self._bbvfs = src._bbvfs
85 self._bbvfs = src._bbvfs
84
86
85 def _partialinit(self):
87 def _partialinit(self):
86 if util.safehasattr(self, '_bbvfs'):
88 if util.safehasattr(self, '_bbvfs'):
87 return
89 return
88 self._bbfp = None
90 self._bbfp = None
89 self._bbrepo = None
91 self._bbrepo = None
90 self._bbvfs = None
92 self._bbvfs = None
91
93
92 def copy(self):
94 def copy(self):
93 self._partialinit()
95 self._partialinit()
94 return self.__class__(self)
96 return self.__class__(self)
95
97
96 @util.propertycache
98 @util.propertycache
97 def track(self):
99 def track(self):
98 return self.configlist('blackbox', 'track', ['*'])
100 return self.configlist('blackbox', 'track', ['*'])
99
101
100 def _openlogfile(self):
102 def _openlogfile(self):
101 def rotate(oldpath, newpath):
103 def rotate(oldpath, newpath):
102 try:
104 try:
103 self._bbvfs.unlink(newpath)
105 self._bbvfs.unlink(newpath)
104 except OSError as err:
106 except OSError as err:
105 if err.errno != errno.ENOENT:
107 if err.errno != errno.ENOENT:
106 self.debug("warning: cannot remove '%s': %s\n" %
108 self.debug("warning: cannot remove '%s': %s\n" %
107 (newpath, err.strerror))
109 (newpath, err.strerror))
108 try:
110 try:
109 if newpath:
111 if newpath:
110 self._bbvfs.rename(oldpath, newpath)
112 self._bbvfs.rename(oldpath, newpath)
111 except OSError as err:
113 except OSError as err:
112 if err.errno != errno.ENOENT:
114 if err.errno != errno.ENOENT:
113 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
115 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
114 (newpath, oldpath, err.strerror))
116 (newpath, oldpath, err.strerror))
115
117
116 fp = _openlog(self._bbvfs)
118 fp = _openlog(self._bbvfs)
117 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
119 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
118 if maxsize > 0:
120 if maxsize > 0:
119 st = self._bbvfs.fstat(fp)
121 st = self._bbvfs.fstat(fp)
120 if st.st_size >= maxsize:
122 if st.st_size >= maxsize:
121 path = fp.name
123 path = fp.name
122 _closelog(self._bbvfs)
124 _closelog(self._bbvfs)
123 maxfiles = self.configint('blackbox', 'maxfiles', 7)
125 maxfiles = self.configint('blackbox', 'maxfiles', 7)
124 for i in xrange(maxfiles - 1, 1, -1):
126 for i in xrange(maxfiles - 1, 1, -1):
125 rotate(oldpath='%s.%d' % (path, i - 1),
127 rotate(oldpath='%s.%d' % (path, i - 1),
126 newpath='%s.%d' % (path, i))
128 newpath='%s.%d' % (path, i))
127 rotate(oldpath=path,
129 rotate(oldpath=path,
128 newpath=maxfiles > 0 and path + '.1')
130 newpath=maxfiles > 0 and path + '.1')
129 fp = _openlog(self._bbvfs)
131 fp = _openlog(self._bbvfs)
130 return fp
132 return fp
131
133
132 def _bbwrite(self, fmt, *args):
134 def _bbwrite(self, fmt, *args):
133 self._bbfp.write(fmt % args)
135 self._bbfp.write(fmt % args)
134 self._bbfp.flush()
136 self._bbfp.flush()
135
137
136 def log(self, event, *msg, **opts):
138 def log(self, event, *msg, **opts):
137 global lastui
139 global lastui
138 super(blackboxui, self).log(event, *msg, **opts)
140 super(blackboxui, self).log(event, *msg, **opts)
139 self._partialinit()
141 self._partialinit()
140
142
141 if not '*' in self.track and not event in self.track:
143 if not '*' in self.track and not event in self.track:
142 return
144 return
143
145
144 if self._bbfp:
146 if self._bbfp:
145 ui = self
147 ui = self
146 elif self._bbvfs:
148 elif self._bbvfs:
147 try:
149 try:
148 self._bbfp = self._openlogfile()
150 self._bbfp = self._openlogfile()
149 except (IOError, OSError) as err:
151 except (IOError, OSError) as err:
150 self.debug('warning: cannot write to blackbox.log: %s\n' %
152 self.debug('warning: cannot write to blackbox.log: %s\n' %
151 err.strerror)
153 err.strerror)
152 del self._bbvfs
154 del self._bbvfs
153 self._bbfp = None
155 self._bbfp = None
154 ui = self
156 ui = self
155 else:
157 else:
156 # certain ui instances exist outside the context of
158 # certain ui instances exist outside the context of
157 # a repo, so just default to the last blackbox that
159 # a repo, so just default to the last blackbox that
158 # was seen.
160 # was seen.
159 ui = lastui
161 ui = lastui
160
162
161 if ui and ui._bbfp:
163 if ui and ui._bbfp:
162 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
164 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
163 user = util.getuser()
165 user = util.getuser()
164 pid = str(util.getpid())
166 pid = str(util.getpid())
165 formattedmsg = msg[0] % msg[1:]
167 formattedmsg = msg[0] % msg[1:]
166 rev = '(unknown)'
168 rev = '(unknown)'
167 changed = ''
169 changed = ''
168 if ui._bbrepo:
170 if ui._bbrepo:
169 ctx = ui._bbrepo[None]
171 ctx = ui._bbrepo[None]
170 parents = ctx.parents()
172 parents = ctx.parents()
171 rev = ('+'.join([hex(p.node()) for p in parents]))
173 rev = ('+'.join([hex(p.node()) for p in parents]))
172 if (ui.configbool('blackbox', 'dirty', False) and (
174 if (ui.configbool('blackbox', 'dirty', False) and (
173 any(ui._bbrepo.status()) or
175 any(ui._bbrepo.status()) or
174 any(ctx.sub(s).dirty() for s in ctx.substate)
176 any(ctx.sub(s).dirty() for s in ctx.substate)
175 )):
177 )):
176 changed = '+'
178 changed = '+'
179 if ui.configbool('blackbox', 'logsource', False):
180 src = ' [%s]' % event
181 else:
182 src = ''
177 try:
183 try:
178 ui._bbwrite('%s %s @%s%s (%s)> %s',
184 ui._bbwrite('%s %s @%s%s (%s)%s> %s',
179 date, user, rev, changed, pid, formattedmsg)
185 date, user, rev, changed, pid, src, formattedmsg)
180 except IOError as err:
186 except IOError as err:
181 self.debug('warning: cannot write to blackbox.log: %s\n' %
187 self.debug('warning: cannot write to blackbox.log: %s\n' %
182 err.strerror)
188 err.strerror)
183 if not lastui or ui._bbrepo:
189 if not lastui or ui._bbrepo:
184 lastui = ui
190 lastui = ui
185
191
186 def setrepo(self, repo):
192 def setrepo(self, repo):
187 self._bbfp = None
193 self._bbfp = None
188 self._bbrepo = repo
194 self._bbrepo = repo
189 self._bbvfs = repo.vfs
195 self._bbvfs = repo.vfs
190
196
191 ui.__class__ = blackboxui
197 ui.__class__ = blackboxui
192 uimod.ui = blackboxui
198 uimod.ui = blackboxui
193
199
194 def uisetup(ui):
200 def uisetup(ui):
195 wrapui(ui)
201 wrapui(ui)
196
202
197 def reposetup(ui, repo):
203 def reposetup(ui, repo):
198 # During 'hg pull' a httppeer repo is created to represent the remote repo.
204 # During 'hg pull' a httppeer repo is created to represent the remote repo.
199 # It doesn't have a .hg directory to put a blackbox in, so we don't do
205 # It doesn't have a .hg directory to put a blackbox in, so we don't do
200 # the blackbox setup for it.
206 # the blackbox setup for it.
201 if not repo.local():
207 if not repo.local():
202 return
208 return
203
209
204 if util.safehasattr(ui, 'setrepo'):
210 if util.safehasattr(ui, 'setrepo'):
205 ui.setrepo(repo)
211 ui.setrepo(repo)
206
212
207 @command('^blackbox',
213 @command('^blackbox',
208 [('l', 'limit', 10, _('the number of events to show')),
214 [('l', 'limit', 10, _('the number of events to show')),
209 ],
215 ],
210 _('hg blackbox [OPTION]...'))
216 _('hg blackbox [OPTION]...'))
211 def blackbox(ui, repo, *revs, **opts):
217 def blackbox(ui, repo, *revs, **opts):
212 '''view the recent repository events
218 '''view the recent repository events
213 '''
219 '''
214
220
215 if not repo.vfs.exists('blackbox.log'):
221 if not repo.vfs.exists('blackbox.log'):
216 return
222 return
217
223
218 limit = opts.get('limit')
224 limit = opts.get('limit')
219 fp = repo.vfs('blackbox.log', 'r')
225 fp = repo.vfs('blackbox.log', 'r')
220 lines = fp.read().split('\n')
226 lines = fp.read().split('\n')
221
227
222 count = 0
228 count = 0
223 output = []
229 output = []
224 for line in reversed(lines):
230 for line in reversed(lines):
225 if count >= limit:
231 if count >= limit:
226 break
232 break
227
233
228 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
234 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
229 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
235 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
230 count += 1
236 count += 1
231 output.append(line)
237 output.append(line)
232
238
233 ui.status('\n'.join(reversed(output)))
239 ui.status('\n'.join(reversed(output)))
General Comments 0
You need to be logged in to leave comments. Login now