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