##// END OF EJS Templates
blackbox: avoid creating multiple file handles for a single log...
timeless -
r28243:45313f5a default
parent child Browse files
Show More
@@ -1,172 +1,187 b''
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 Examples::
13 Examples::
14
14
15 [blackbox]
15 [blackbox]
16 track = *
16 track = *
17
17
18 [blackbox]
18 [blackbox]
19 track = command, commandfinish, commandexception, exthook, pythonhook
19 track = command, commandfinish, commandexception, exthook, pythonhook
20
20
21 [blackbox]
21 [blackbox]
22 track = incoming
22 track = incoming
23
23
24 [blackbox]
24 [blackbox]
25 # limit the size of a log file
25 # limit the size of a log file
26 maxsize = 1.5 MB
26 maxsize = 1.5 MB
27 # rotate up to N log files when the current one gets too big
27 # rotate up to N log files when the current one gets too big
28 maxfiles = 3
28 maxfiles = 3
29
29
30 """
30 """
31
31
32 from __future__ import absolute_import
32 from __future__ import absolute_import
33
33
34 import errno
34 import errno
35 import re
35 import re
36
36
37 from mercurial.i18n import _
37 from mercurial.i18n import _
38 from mercurial import (
38 from mercurial import (
39 cmdutil,
39 cmdutil,
40 util,
40 util,
41 )
41 )
42
42
43 cmdtable = {}
43 cmdtable = {}
44 command = cmdutil.command(cmdtable)
44 command = cmdutil.command(cmdtable)
45 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 # Note for extension authors: ONLY specify testedwith = 'internal' for
46 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
47 # be specifying the version(s) of Mercurial they are tested with, or
47 # be specifying the version(s) of Mercurial they are tested with, or
48 # leave the attribute unspecified.
48 # leave the attribute unspecified.
49 testedwith = 'internal'
49 testedwith = 'internal'
50 lastblackbox = None
50 lastblackbox = None
51
51
52 filehandles = {}
53
54 def _openlog(vfs):
55 path = vfs.join('blackbox.log')
56 if path in filehandles:
57 return filehandles[path]
58 filehandles[path] = fp = vfs('blackbox.log', 'a')
59 return fp
60
61 def _closelog(vfs):
62 path = vfs.join('blackbox.log')
63 fp = filehandles[path]
64 del filehandles[path]
65 fp.close()
66
52 def wrapui(ui):
67 def wrapui(ui):
53 class blackboxui(ui.__class__):
68 class blackboxui(ui.__class__):
54 @util.propertycache
69 @util.propertycache
55 def track(self):
70 def track(self):
56 return self.configlist('blackbox', 'track', ['*'])
71 return self.configlist('blackbox', 'track', ['*'])
57
72
58 def _openlogfile(self):
73 def _openlogfile(self):
59 def rotate(oldpath, newpath):
74 def rotate(oldpath, newpath):
60 try:
75 try:
61 self._bbvfs.unlink(newpath)
76 self._bbvfs.unlink(newpath)
62 except OSError as err:
77 except OSError as err:
63 if err.errno != errno.ENOENT:
78 if err.errno != errno.ENOENT:
64 self.debug("warning: cannot remove '%s': %s\n" %
79 self.debug("warning: cannot remove '%s': %s\n" %
65 (newpath, err.strerror))
80 (newpath, err.strerror))
66 try:
81 try:
67 if newpath:
82 if newpath:
68 self._bbvfs.rename(oldpath, newpath)
83 self._bbvfs.rename(oldpath, newpath)
69 except OSError as err:
84 except OSError as err:
70 if err.errno != errno.ENOENT:
85 if err.errno != errno.ENOENT:
71 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
86 self.debug("warning: cannot rename '%s' to '%s': %s\n" %
72 (newpath, oldpath, err.strerror))
87 (newpath, oldpath, err.strerror))
73
88
74 fp = self._bbvfs('blackbox.log', 'a')
89 fp = _openlog(self._bbvfs)
75 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
90 maxsize = self.configbytes('blackbox', 'maxsize', 1048576)
76 if maxsize > 0:
91 if maxsize > 0:
77 st = self._bbvfs.fstat(fp)
92 st = self._bbvfs.fstat(fp)
78 if st.st_size >= maxsize:
93 if st.st_size >= maxsize:
79 path = fp.name
94 path = fp.name
80 fp.close()
95 _closelog(self._bbvfs)
81 maxfiles = self.configint('blackbox', 'maxfiles', 7)
96 maxfiles = self.configint('blackbox', 'maxfiles', 7)
82 for i in xrange(maxfiles - 1, 1, -1):
97 for i in xrange(maxfiles - 1, 1, -1):
83 rotate(oldpath='%s.%d' % (path, i - 1),
98 rotate(oldpath='%s.%d' % (path, i - 1),
84 newpath='%s.%d' % (path, i))
99 newpath='%s.%d' % (path, i))
85 rotate(oldpath=path,
100 rotate(oldpath=path,
86 newpath=maxfiles > 0 and path + '.1')
101 newpath=maxfiles > 0 and path + '.1')
87 fp = self._bbvfs('blackbox.log', 'a')
102 fp = _openlog(self._bbvfs)
88 return fp
103 return fp
89
104
90 def log(self, event, *msg, **opts):
105 def log(self, event, *msg, **opts):
91 global lastblackbox
106 global lastblackbox
92 super(blackboxui, self).log(event, *msg, **opts)
107 super(blackboxui, self).log(event, *msg, **opts)
93
108
94 if not '*' in self.track and not event in self.track:
109 if not '*' in self.track and not event in self.track:
95 return
110 return
96
111
97 if util.safehasattr(self, '_blackbox'):
112 if util.safehasattr(self, '_blackbox'):
98 blackbox = self._blackbox
113 blackbox = self._blackbox
99 elif util.safehasattr(self, '_bbvfs'):
114 elif util.safehasattr(self, '_bbvfs'):
100 try:
115 try:
101 self._blackbox = self._openlogfile()
116 self._blackbox = self._openlogfile()
102 except (IOError, OSError) as err:
117 except (IOError, OSError) as err:
103 self.debug('warning: cannot write to blackbox.log: %s\n' %
118 self.debug('warning: cannot write to blackbox.log: %s\n' %
104 err.strerror)
119 err.strerror)
105 del self._bbvfs
120 del self._bbvfs
106 self._blackbox = None
121 self._blackbox = None
107 blackbox = self._blackbox
122 blackbox = self._blackbox
108 else:
123 else:
109 # certain ui instances exist outside the context of
124 # certain ui instances exist outside the context of
110 # a repo, so just default to the last blackbox that
125 # a repo, so just default to the last blackbox that
111 # was seen.
126 # was seen.
112 blackbox = lastblackbox
127 blackbox = lastblackbox
113
128
114 if blackbox:
129 if blackbox:
115 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
130 date = util.datestr(None, '%Y/%m/%d %H:%M:%S')
116 user = util.getuser()
131 user = util.getuser()
117 pid = str(util.getpid())
132 pid = str(util.getpid())
118 formattedmsg = msg[0] % msg[1:]
133 formattedmsg = msg[0] % msg[1:]
119 try:
134 try:
120 blackbox.write('%s %s (%s)> %s' %
135 blackbox.write('%s %s (%s)> %s' %
121 (date, user, pid, formattedmsg))
136 (date, user, pid, formattedmsg))
122 blackbox.flush()
137 blackbox.flush()
123 except IOError as err:
138 except IOError as err:
124 self.debug('warning: cannot write to blackbox.log: %s\n' %
139 self.debug('warning: cannot write to blackbox.log: %s\n' %
125 err.strerror)
140 err.strerror)
126 lastblackbox = blackbox
141 lastblackbox = blackbox
127
142
128 def setrepo(self, repo):
143 def setrepo(self, repo):
129 self._bbvfs = repo.vfs
144 self._bbvfs = repo.vfs
130
145
131 ui.__class__ = blackboxui
146 ui.__class__ = blackboxui
132
147
133 def uisetup(ui):
148 def uisetup(ui):
134 wrapui(ui)
149 wrapui(ui)
135
150
136 def reposetup(ui, repo):
151 def reposetup(ui, repo):
137 # During 'hg pull' a httppeer repo is created to represent the remote repo.
152 # During 'hg pull' a httppeer repo is created to represent the remote repo.
138 # It doesn't have a .hg directory to put a blackbox in, so we don't do
153 # It doesn't have a .hg directory to put a blackbox in, so we don't do
139 # the blackbox setup for it.
154 # the blackbox setup for it.
140 if not repo.local():
155 if not repo.local():
141 return
156 return
142
157
143 if util.safehasattr(ui, 'setrepo'):
158 if util.safehasattr(ui, 'setrepo'):
144 ui.setrepo(repo)
159 ui.setrepo(repo)
145
160
146 @command('^blackbox',
161 @command('^blackbox',
147 [('l', 'limit', 10, _('the number of events to show')),
162 [('l', 'limit', 10, _('the number of events to show')),
148 ],
163 ],
149 _('hg blackbox [OPTION]...'))
164 _('hg blackbox [OPTION]...'))
150 def blackbox(ui, repo, *revs, **opts):
165 def blackbox(ui, repo, *revs, **opts):
151 '''view the recent repository events
166 '''view the recent repository events
152 '''
167 '''
153
168
154 if not repo.vfs.exists('blackbox.log'):
169 if not repo.vfs.exists('blackbox.log'):
155 return
170 return
156
171
157 limit = opts.get('limit')
172 limit = opts.get('limit')
158 blackbox = repo.vfs('blackbox.log', 'r')
173 blackbox = repo.vfs('blackbox.log', 'r')
159 lines = blackbox.read().split('\n')
174 lines = blackbox.read().split('\n')
160
175
161 count = 0
176 count = 0
162 output = []
177 output = []
163 for line in reversed(lines):
178 for line in reversed(lines):
164 if count >= limit:
179 if count >= limit:
165 break
180 break
166
181
167 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
182 # count the commands by matching lines like: 2013/01/23 19:13:36 root>
168 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
183 if re.match('^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} .*> .*', line):
169 count += 1
184 count += 1
170 output.append(line)
185 output.append(line)
171
186
172 ui.status('\n'.join(reversed(output)))
187 ui.status('\n'.join(reversed(output)))
General Comments 0
You need to be logged in to leave comments. Login now