##// END OF EJS Templates
blackbox: extract function to test if log event is tracked...
Yuya Nishihara -
r40684:85372dc0 default
parent child Browse files
Show More
@@ -1,275 +1,278
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
21 # record the source of log messages
22 logsource = True
22 logsource = True
23
23
24 [blackbox]
24 [blackbox]
25 track = command, commandfinish, commandexception, exthook, pythonhook
25 track = command, commandfinish, commandexception, exthook, pythonhook
26
26
27 [blackbox]
27 [blackbox]
28 track = incoming
28 track = incoming
29
29
30 [blackbox]
30 [blackbox]
31 # limit the size of a log file
31 # limit the size of a log file
32 maxsize = 1.5 MB
32 maxsize = 1.5 MB
33 # 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
34 maxfiles = 3
34 maxfiles = 3
35
35
36 [blackbox]
36 [blackbox]
37 # Include nanoseconds in log entries with %f (see Python function
37 # Include nanoseconds in log entries with %f (see Python function
38 # datetime.datetime.strftime)
38 # datetime.datetime.strftime)
39 date-format = '%Y-%m-%d @ %H:%M:%S.%f'
39 date-format = '%Y-%m-%d @ %H:%M:%S.%f'
40
40
41 """
41 """
42
42
43 from __future__ import absolute_import
43 from __future__ import absolute_import
44
44
45 import errno
45 import errno
46 import re
46 import re
47
47
48 from mercurial.i18n import _
48 from mercurial.i18n import _
49 from mercurial.node import hex
49 from mercurial.node import hex
50
50
51 from mercurial import (
51 from mercurial import (
52 encoding,
52 encoding,
53 pycompat,
53 pycompat,
54 registrar,
54 registrar,
55 ui as uimod,
55 ui as uimod,
56 util,
56 util,
57 )
57 )
58 from mercurial.utils import (
58 from mercurial.utils import (
59 dateutil,
59 dateutil,
60 procutil,
60 procutil,
61 )
61 )
62
62
63 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
63 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
64 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
64 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
65 # be specifying the version(s) of Mercurial they are tested with, or
65 # be specifying the version(s) of Mercurial they are tested with, or
66 # leave the attribute unspecified.
66 # leave the attribute unspecified.
67 testedwith = 'ships-with-hg-core'
67 testedwith = 'ships-with-hg-core'
68
68
69 cmdtable = {}
69 cmdtable = {}
70 command = registrar.command(cmdtable)
70 command = registrar.command(cmdtable)
71
71
72 configtable = {}
72 configtable = {}
73 configitem = registrar.configitem(configtable)
73 configitem = registrar.configitem(configtable)
74
74
75 configitem('blackbox', 'dirty',
75 configitem('blackbox', 'dirty',
76 default=False,
76 default=False,
77 )
77 )
78 configitem('blackbox', 'maxsize',
78 configitem('blackbox', 'maxsize',
79 default='1 MB',
79 default='1 MB',
80 )
80 )
81 configitem('blackbox', 'logsource',
81 configitem('blackbox', 'logsource',
82 default=False,
82 default=False,
83 )
83 )
84 configitem('blackbox', 'maxfiles',
84 configitem('blackbox', 'maxfiles',
85 default=7,
85 default=7,
86 )
86 )
87 configitem('blackbox', 'track',
87 configitem('blackbox', 'track',
88 default=lambda: ['*'],
88 default=lambda: ['*'],
89 )
89 )
90 configitem('blackbox', 'date-format',
90 configitem('blackbox', 'date-format',
91 default='%Y/%m/%d %H:%M:%S',
91 default='%Y/%m/%d %H:%M:%S',
92 )
92 )
93
93
94 _lastlogger = None
94 _lastlogger = None
95
95
96 def _openlogfile(ui, vfs):
96 def _openlogfile(ui, vfs):
97 def rotate(oldpath, newpath):
97 def rotate(oldpath, newpath):
98 try:
98 try:
99 vfs.unlink(newpath)
99 vfs.unlink(newpath)
100 except OSError as err:
100 except OSError as err:
101 if err.errno != errno.ENOENT:
101 if err.errno != errno.ENOENT:
102 ui.debug("warning: cannot remove '%s': %s\n" %
102 ui.debug("warning: cannot remove '%s': %s\n" %
103 (newpath, err.strerror))
103 (newpath, err.strerror))
104 try:
104 try:
105 if newpath:
105 if newpath:
106 vfs.rename(oldpath, newpath)
106 vfs.rename(oldpath, newpath)
107 except OSError as err:
107 except OSError as err:
108 if err.errno != errno.ENOENT:
108 if err.errno != errno.ENOENT:
109 ui.debug("warning: cannot rename '%s' to '%s': %s\n" %
109 ui.debug("warning: cannot rename '%s' to '%s': %s\n" %
110 (newpath, oldpath, err.strerror))
110 (newpath, oldpath, err.strerror))
111
111
112 maxsize = ui.configbytes('blackbox', 'maxsize')
112 maxsize = ui.configbytes('blackbox', 'maxsize')
113 name = 'blackbox.log'
113 name = 'blackbox.log'
114 if maxsize > 0:
114 if maxsize > 0:
115 try:
115 try:
116 st = vfs.stat(name)
116 st = vfs.stat(name)
117 except OSError:
117 except OSError:
118 pass
118 pass
119 else:
119 else:
120 if st.st_size >= maxsize:
120 if st.st_size >= maxsize:
121 path = vfs.join(name)
121 path = vfs.join(name)
122 maxfiles = ui.configint('blackbox', 'maxfiles')
122 maxfiles = ui.configint('blackbox', 'maxfiles')
123 for i in pycompat.xrange(maxfiles - 1, 1, -1):
123 for i in pycompat.xrange(maxfiles - 1, 1, -1):
124 rotate(oldpath='%s.%d' % (path, i - 1),
124 rotate(oldpath='%s.%d' % (path, i - 1),
125 newpath='%s.%d' % (path, i))
125 newpath='%s.%d' % (path, i))
126 rotate(oldpath=path,
126 rotate(oldpath=path,
127 newpath=maxfiles > 0 and path + '.1')
127 newpath=maxfiles > 0 and path + '.1')
128 return vfs(name, 'a')
128 return vfs(name, 'a')
129
129
130 class blackboxlogger(object):
130 class blackboxlogger(object):
131 def __init__(self, ui):
131 def __init__(self, ui):
132 self._repo = None
132 self._repo = None
133 self._inlog = False
133 self._inlog = False
134 self.track = ui.configlist('blackbox', 'track')
134 self._trackedevents = set(ui.configlist('blackbox', 'track'))
135
135
136 @property
136 @property
137 def _bbvfs(self):
137 def _bbvfs(self):
138 vfs = None
138 vfs = None
139 if self._repo:
139 if self._repo:
140 vfs = self._repo.vfs
140 vfs = self._repo.vfs
141 if not vfs.isdir('.'):
141 if not vfs.isdir('.'):
142 vfs = None
142 vfs = None
143 return vfs
143 return vfs
144
144
145 def tracked(self, event):
146 return b'*' in self._trackedevents or event in self._trackedevents
147
145 def log(self, ui, event, msg, opts):
148 def log(self, ui, event, msg, opts):
146 global _lastlogger
149 global _lastlogger
147 if not '*' in self.track and not event in self.track:
150 if not self.tracked(event):
148 return
151 return
149
152
150 if self._bbvfs:
153 if self._bbvfs:
151 _lastlogger = self
154 _lastlogger = self
152 elif _lastlogger and _lastlogger._bbvfs:
155 elif _lastlogger and _lastlogger._bbvfs:
153 # certain logger instances exist outside the context of
156 # certain logger instances exist outside the context of
154 # a repo, so just default to the last blackbox logger that
157 # a repo, so just default to the last blackbox logger that
155 # was seen.
158 # was seen.
156 pass
159 pass
157 else:
160 else:
158 return
161 return
159 _lastlogger._log(ui, event, msg, opts)
162 _lastlogger._log(ui, event, msg, opts)
160
163
161 def _log(self, ui, event, msg, opts):
164 def _log(self, ui, event, msg, opts):
162 if self._inlog:
165 if self._inlog:
163 # recursion and failure guard
166 # recursion and failure guard
164 return
167 return
165 self._inlog = True
168 self._inlog = True
166 default = ui.configdate('devel', 'default-date')
169 default = ui.configdate('devel', 'default-date')
167 date = dateutil.datestr(default, ui.config('blackbox', 'date-format'))
170 date = dateutil.datestr(default, ui.config('blackbox', 'date-format'))
168 user = procutil.getuser()
171 user = procutil.getuser()
169 pid = '%d' % procutil.getpid()
172 pid = '%d' % procutil.getpid()
170 formattedmsg = msg[0] % msg[1:]
173 formattedmsg = msg[0] % msg[1:]
171 rev = '(unknown)'
174 rev = '(unknown)'
172 changed = ''
175 changed = ''
173 ctx = self._repo[None]
176 ctx = self._repo[None]
174 parents = ctx.parents()
177 parents = ctx.parents()
175 rev = ('+'.join([hex(p.node()) for p in parents]))
178 rev = ('+'.join([hex(p.node()) for p in parents]))
176 if (ui.configbool('blackbox', 'dirty') and
179 if (ui.configbool('blackbox', 'dirty') and
177 ctx.dirty(missing=True, merge=False, branch=False)):
180 ctx.dirty(missing=True, merge=False, branch=False)):
178 changed = '+'
181 changed = '+'
179 if ui.configbool('blackbox', 'logsource'):
182 if ui.configbool('blackbox', 'logsource'):
180 src = ' [%s]' % event
183 src = ' [%s]' % event
181 else:
184 else:
182 src = ''
185 src = ''
183 try:
186 try:
184 fmt = '%s %s @%s%s (%s)%s> %s'
187 fmt = '%s %s @%s%s (%s)%s> %s'
185 args = (date, user, rev, changed, pid, src, formattedmsg)
188 args = (date, user, rev, changed, pid, src, formattedmsg)
186 with _openlogfile(ui, self._bbvfs) as fp:
189 with _openlogfile(ui, self._bbvfs) as fp:
187 fp.write(fmt % args)
190 fp.write(fmt % args)
188 except (IOError, OSError) as err:
191 except (IOError, OSError) as err:
189 ui.debug('warning: cannot write to blackbox.log: %s\n' %
192 ui.debug('warning: cannot write to blackbox.log: %s\n' %
190 encoding.strtolocal(err.strerror))
193 encoding.strtolocal(err.strerror))
191 # do not restore _inlog intentionally to avoid failed
194 # do not restore _inlog intentionally to avoid failed
192 # logging again
195 # logging again
193 else:
196 else:
194 self._inlog = False
197 self._inlog = False
195
198
196 def setrepo(self, repo):
199 def setrepo(self, repo):
197 self._repo = repo
200 self._repo = repo
198
201
199 def wrapui(ui):
202 def wrapui(ui):
200 class blackboxui(ui.__class__):
203 class blackboxui(ui.__class__):
201 def __init__(self, src=None):
204 def __init__(self, src=None):
202 super(blackboxui, self).__init__(src)
205 super(blackboxui, self).__init__(src)
203 if src and r'_bblogger' in src.__dict__:
206 if src and r'_bblogger' in src.__dict__:
204 self._bblogger = src._bblogger
207 self._bblogger = src._bblogger
205
208
206 # trick to initialize logger after configuration is loaded, which
209 # trick to initialize logger after configuration is loaded, which
207 # can be replaced later with blackboxlogger(ui) in uisetup(), where
210 # can be replaced later with blackboxlogger(ui) in uisetup(), where
208 # both user and repo configurations should be available.
211 # both user and repo configurations should be available.
209 @util.propertycache
212 @util.propertycache
210 def _bblogger(self):
213 def _bblogger(self):
211 return blackboxlogger(self)
214 return blackboxlogger(self)
212
215
213 def debug(self, *msg, **opts):
216 def debug(self, *msg, **opts):
214 super(blackboxui, self).debug(*msg, **opts)
217 super(blackboxui, self).debug(*msg, **opts)
215 if self.debugflag:
218 if self.debugflag:
216 self.log('debug', '%s', ''.join(msg))
219 self.log('debug', '%s', ''.join(msg))
217
220
218 def log(self, event, *msg, **opts):
221 def log(self, event, *msg, **opts):
219 super(blackboxui, self).log(event, *msg, **opts)
222 super(blackboxui, self).log(event, *msg, **opts)
220 self._bblogger.log(self, event, msg, opts)
223 self._bblogger.log(self, event, msg, opts)
221
224
222 ui.__class__ = blackboxui
225 ui.__class__ = blackboxui
223 uimod.ui = blackboxui
226 uimod.ui = blackboxui
224
227
225 def uisetup(ui):
228 def uisetup(ui):
226 wrapui(ui)
229 wrapui(ui)
227
230
228 def reposetup(ui, repo):
231 def reposetup(ui, repo):
229 # During 'hg pull' a httppeer repo is created to represent the remote repo.
232 # During 'hg pull' a httppeer repo is created to represent the remote repo.
230 # It doesn't have a .hg directory to put a blackbox in, so we don't do
233 # It doesn't have a .hg directory to put a blackbox in, so we don't do
231 # the blackbox setup for it.
234 # the blackbox setup for it.
232 if not repo.local():
235 if not repo.local():
233 return
236 return
234
237
235 logger = getattr(ui, '_bblogger', None)
238 logger = getattr(ui, '_bblogger', None)
236 if logger:
239 if logger:
237 logger.setrepo(repo)
240 logger.setrepo(repo)
238
241
239 # Set _lastlogger even if ui.log is not called. This gives blackbox a
242 # Set _lastlogger even if ui.log is not called. This gives blackbox a
240 # fallback place to log.
243 # fallback place to log.
241 global _lastlogger
244 global _lastlogger
242 if _lastlogger is None:
245 if _lastlogger is None:
243 _lastlogger = logger
246 _lastlogger = logger
244
247
245 repo._wlockfreeprefix.add('blackbox.log')
248 repo._wlockfreeprefix.add('blackbox.log')
246
249
247 @command('blackbox',
250 @command('blackbox',
248 [('l', 'limit', 10, _('the number of events to show')),
251 [('l', 'limit', 10, _('the number of events to show')),
249 ],
252 ],
250 _('hg blackbox [OPTION]...'),
253 _('hg blackbox [OPTION]...'),
251 helpcategory=command.CATEGORY_MAINTENANCE,
254 helpcategory=command.CATEGORY_MAINTENANCE,
252 helpbasic=True)
255 helpbasic=True)
253 def blackbox(ui, repo, *revs, **opts):
256 def blackbox(ui, repo, *revs, **opts):
254 '''view the recent repository events
257 '''view the recent repository events
255 '''
258 '''
256
259
257 if not repo.vfs.exists('blackbox.log'):
260 if not repo.vfs.exists('blackbox.log'):
258 return
261 return
259
262
260 limit = opts.get(r'limit')
263 limit = opts.get(r'limit')
261 fp = repo.vfs('blackbox.log', 'r')
264 fp = repo.vfs('blackbox.log', 'r')
262 lines = fp.read().split('\n')
265 lines = fp.read().split('\n')
263
266
264 count = 0
267 count = 0
265 output = []
268 output = []
266 for line in reversed(lines):
269 for line in reversed(lines):
267 if count >= limit:
270 if count >= limit:
268 break
271 break
269
272
270 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
273 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
271 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
274 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
272 count += 1
275 count += 1
273 output.append(line)
276 output.append(line)
274
277
275 ui.status('\n'.join(reversed(output)))
278 ui.status('\n'.join(reversed(output)))
General Comments 0
You need to be logged in to leave comments. Login now