Merge lp:~jml/launchpad/extract-ssh-server-logging into lp:launchpad
- extract-ssh-server-logging
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Jonathan Lange |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~jml/launchpad/extract-ssh-server-logging |
Merge into: | lp:launchpad |
Prerequisite: | lp:~jml/launchpad/extract-ssh-server |
Diff against target: |
1186 lines (+524/-349) (has conflicts) 9 files modified
daemons/sftp.tac (+31/-7) lib/lp/codehosting/sftp.py (+3/-3) lib/lp/codehosting/sshserver/accesslog.py (+46/-176) lib/lp/codehosting/sshserver/auth.py (+3/-3) lib/lp/codehosting/sshserver/events.py (+155/-0) lib/lp/codehosting/sshserver/service.py (+125/-29) lib/lp/codehosting/sshserver/session.py (+3/-3) lib/lp/codehosting/sshserver/tests/test_events.py (+91/-0) lib/lp/codehosting/sshserver/tests/test_logging.py (+67/-128) Text conflict in daemons/sftp.tac Text conflict in lib/lp/codehosting/sshserver/accesslog.py Text conflict in lib/lp/codehosting/sshserver/service.py |
To merge this branch: | bzr merge lp:~jml/launchpad/extract-ssh-server-logging |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | Approve | ||
Michael Hudson-Doyle | Pending | ||
Review via email: mp+23425@code.launchpad.net |
Commit message
Parametrize the codehosting SSH server logging so that it does not assume codehosting.
Description of the change
This change builds on my previous SSH server refactoring branch. It takes the customized logging stuff that we've built for codehosting and tears out all of the codehosting specific bits.
Specifically, it:
* splits the event classes into another module
* parametrizes the main log, the access log and the access log file path in the logging manager
* moves the _log_event helper into the LoggingManager
* deletes the silly get_codehosting
* cleans up the tests a bit
It's a pretty boring branch, all told.
I look forward to your review.
jml
Jonathan Lange (jml) wrote : | # |
On Thu, Apr 15, 2010 at 10:47 AM, Michael Nelson
<email address hidden> wrote:
> Review: Approve
> Hi Jonathan,
>
> I love the test simplifications (LoggingManager
>
Thanks!
> 1. Some missing imports (as per our chat),
These have been fixed.
> 2. Some tests not asserting the intermediate state (but you said that other tests in the module already do this, but will check (I didn't)), and
I've checked, and they are all good.
> 3. A duplicate test.
>
It's not a duplicate test, but rather a very similar test for some
very similar code. I've put the two tests next to each other, changed
the names a bit and added comments so that the difference is clear.
Thanks,
jml
Preview Diff
1 | === modified file 'daemons/sftp.tac' |
2 | --- daemons/sftp.tac 2010-04-15 13:40:21 +0000 |
3 | +++ daemons/sftp.tac 2010-04-16 18:57:30 +0000 |
4 | @@ -9,20 +9,44 @@ |
5 | |
6 | from canonical.config import config |
7 | from canonical.launchpad.daemons import tachandler |
8 | +<<<<<<< TREE |
9 | |
10 | from lp.codehosting.sshserver.service import ( |
11 | get_key_path, make_portal, PRIVATE_KEY_FILE, PUBLIC_KEY_FILE, SSHService) |
12 | |
13 | |
14 | # Construct an Application that has the codehosting SSH server. |
15 | +======= |
16 | + |
17 | +from lp.codehosting.sshserver.service import ( |
18 | + ACCESS_LOG_NAME, get_key_path, LOG_NAME, make_portal, OOPS_CONFIG_SECTION, |
19 | + PRIVATE_KEY_FILE, PUBLIC_KEY_FILE, SSHService) |
20 | + |
21 | + |
22 | +# Construct an Application that has the codehosting SSH server. |
23 | +>>>>>>> MERGE-SOURCE |
24 | application = service.Application('sftponly') |
25 | -svc = SSHService( |
26 | - portal=make_portal(), |
27 | - private_key_path=get_key_path(PRIVATE_KEY_FILE), |
28 | - public_key_path=get_key_path(PUBLIC_KEY_FILE), |
29 | - strport=config.codehosting.port, |
30 | - idle_timeout=config.codehosting.idle_timeout, |
31 | - banner=config.codehosting.banner) |
32 | +<<<<<<< TREE |
33 | +svc = SSHService( |
34 | + portal=make_portal(), |
35 | + private_key_path=get_key_path(PRIVATE_KEY_FILE), |
36 | + public_key_path=get_key_path(PUBLIC_KEY_FILE), |
37 | + strport=config.codehosting.port, |
38 | + idle_timeout=config.codehosting.idle_timeout, |
39 | + banner=config.codehosting.banner) |
40 | +======= |
41 | +svc = SSHService( |
42 | + portal=make_portal(), |
43 | + private_key_path=get_key_path(PRIVATE_KEY_FILE), |
44 | + public_key_path=get_key_path(PUBLIC_KEY_FILE), |
45 | + oops_configuration=OOPS_CONFIG_SECTION, |
46 | + main_log=LOG_NAME, |
47 | + access_log=ACCESS_LOG_NAME, |
48 | + access_log_path=config.codehosting.access_log, |
49 | + strport=config.codehosting.port, |
50 | + idle_timeout=config.codehosting.idle_timeout, |
51 | + banner=config.codehosting.banner) |
52 | +>>>>>>> MERGE-SOURCE |
53 | svc.setServiceParent(application) |
54 | |
55 | # Service that announces when the daemon is ready |
56 | |
57 | === modified file 'lib/lp/codehosting/sftp.py' |
58 | --- lib/lp/codehosting/sftp.py 2010-03-19 10:43:51 +0000 |
59 | +++ lib/lp/codehosting/sftp.py 2010-04-16 18:57:30 +0000 |
60 | @@ -38,7 +38,7 @@ |
61 | from zope.interface import implements |
62 | |
63 | from lp.codehosting.vfs import AsyncLaunchpadTransport, LaunchpadServer |
64 | -from lp.codehosting.sshserver import accesslog |
65 | +from lp.codehosting.sshserver import events |
66 | from canonical.config import config |
67 | from lp.services.twistedsupport import gatherResults |
68 | |
69 | @@ -253,7 +253,7 @@ |
70 | avatar.branchfs_proxy, user_id, hosted_transport, mirror_transport) |
71 | server.start_server() |
72 | transport = AsyncLaunchpadTransport(server, server.get_url()) |
73 | - notify(accesslog.SFTPStarted(avatar)) |
74 | + notify(events.SFTPStarted(avatar)) |
75 | return TransportSFTPServer(transport) |
76 | |
77 | |
78 | @@ -272,7 +272,7 @@ |
79 | if self.avatar is not None: |
80 | avatar = self.avatar |
81 | self.avatar = None |
82 | - notify(accesslog.SFTPClosed(avatar)) |
83 | + notify(events.SFTPClosed(avatar)) |
84 | |
85 | |
86 | class TransportSFTPServer: |
87 | |
88 | === modified file 'lib/lp/codehosting/sshserver/accesslog.py' |
89 | --- lib/lp/codehosting/sshserver/accesslog.py 2010-04-13 19:07:23 +0000 |
90 | +++ lib/lp/codehosting/sshserver/accesslog.py 2010-04-16 18:57:30 +0000 |
91 | @@ -5,235 +5,105 @@ |
92 | |
93 | __metaclass__ = type |
94 | __all__ = [ |
95 | - 'AuthenticationFailed', |
96 | - 'BazaarSSHClosed', |
97 | - 'BazaarSSHStarted', |
98 | - 'LoggingEvent', |
99 | - 'ServerStarting', |
100 | - 'ServerStopped', |
101 | - 'SFTPClosed', |
102 | - 'SFTPStarted', |
103 | - 'UserConnected', |
104 | - 'UserDisconnected', |
105 | - 'UserLoggedIn', |
106 | - 'UserLoggedOut', |
107 | + 'LoggingManager', |
108 | ] |
109 | |
110 | import logging |
111 | |
112 | from twisted.python import log as tplog |
113 | |
114 | +from zope.component import adapter, getGlobalSiteManager, provideHandler |
115 | # This non-standard import is necessary to hook up the event system. |
116 | import zope.component.event |
117 | -from zope.interface import Attribute, implements, Interface |
118 | |
119 | -from canonical.config import config |
120 | from canonical.launchpad.scripts import WatchedFileHandler |
121 | +from lp.codehosting.sshserver.events import ILoggingEvent |
122 | from lp.services.utils import synchronize |
123 | -from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting |
124 | |
125 | |
126 | class LoggingManager: |
127 | +<<<<<<< TREE |
128 | """Class for managing codehosting logging.""" |
129 | |
130 | def setUp(self, configure_oops_reporting=False): |
131 | +======= |
132 | + """Class for managing SSH server logging.""" |
133 | + |
134 | + def __init__(self, main_log, access_log, access_log_path): |
135 | + """Construct the logging manager. |
136 | + |
137 | + :param main_log: The main log. Twisted will log to this. |
138 | + :param access_log: The access log object. |
139 | + :param access_log_path: The path to the file where access log |
140 | + messages go. |
141 | + """ |
142 | + self._main_log = main_log |
143 | + self._access_log = access_log |
144 | + self._access_log_path = access_log_path |
145 | + self._is_set_up = False |
146 | + |
147 | + def setUp(self): |
148 | +>>>>>>> MERGE-SOURCE |
149 | """Set up logging for the smart server. |
150 | |
151 | +<<<<<<< TREE |
152 | This sets up a debugging handler on the 'codehosting' logger, makes |
153 | sure that things logged there won't go to stderr (necessary because of |
154 | bzrlib.trace shenanigans) and then returns the 'codehosting' logger. |
155 | |
156 | :param configure_oops_reporting: If True, install a Twisted log |
157 | observer that ensures unhandled exceptions get reported as OOPSes. |
158 | +======= |
159 | + This sets up a debugging handler on the main logger and makes sure |
160 | + that things logged there won't go to stderr. It also sets up an access |
161 | + logger. |
162 | +>>>>>>> MERGE-SOURCE |
163 | """ |
164 | - log = get_codehosting_logger() |
165 | + log = self._main_log |
166 | self._orig_level = log.level |
167 | self._orig_handlers = list(log.handlers) |
168 | self._orig_observers = list(tplog.theLogPublisher.observers) |
169 | log.setLevel(logging.INFO) |
170 | log.addHandler(_NullHandler()) |
171 | - access_log = get_access_logger() |
172 | - handler = WatchedFileHandler(config.codehosting.access_log) |
173 | + handler = WatchedFileHandler(self._access_log_path) |
174 | handler.setFormatter( |
175 | logging.Formatter("%(asctime)s %(levelname)s %(message)s")) |
176 | - access_log.addHandler(handler) |
177 | - if configure_oops_reporting: |
178 | - set_up_oops_reporting('codehosting') |
179 | + self._access_log.addHandler(handler) |
180 | # Make sure that our logging event handler is there, ready to receive |
181 | # logging events. |
182 | - zope.component.provideHandler(_log_event) |
183 | + provideHandler(self._log_event) |
184 | + self._is_set_up = True |
185 | + |
186 | + @adapter(ILoggingEvent) |
187 | + def _log_event(self, event): |
188 | + """Log 'event' to the access log.""" |
189 | + self._access_log.log(event.level, event.message) |
190 | |
191 | def tearDown(self): |
192 | - log = get_codehosting_logger() |
193 | + if not self._is_set_up: |
194 | + return |
195 | + log = self._main_log |
196 | log.level = self._orig_level |
197 | synchronize( |
198 | log.handlers, self._orig_handlers, log.addHandler, |
199 | log.removeHandler) |
200 | - access_log = get_access_logger() |
201 | synchronize( |
202 | - access_log.handlers, self._orig_handlers, access_log.addHandler, |
203 | - access_log.removeHandler) |
204 | + self._access_log.handlers, self._orig_handlers, |
205 | + self._access_log.addHandler, self._access_log.removeHandler) |
206 | synchronize( |
207 | tplog.theLogPublisher.observers, self._orig_observers, |
208 | tplog.addObserver, tplog.removeObserver) |
209 | - zope.component.getGlobalSiteManager().unregisterHandler(_log_event) |
210 | - |
211 | - |
212 | -def get_codehosting_logger(): |
213 | - """Return the codehosting logger.""" |
214 | - # This is its own function to avoid spreading the string 'codehosting' |
215 | - # everywhere and to avoid duplicating information about how log objects |
216 | - # are acquired. |
217 | - return logging.getLogger('codehosting') |
218 | - |
219 | - |
220 | -def get_access_logger(): |
221 | - return logging.getLogger('codehosting.access') |
222 | + getGlobalSiteManager().unregisterHandler(self._log_event) |
223 | + self._is_set_up = False |
224 | |
225 | |
226 | class _NullHandler(logging.Handler): |
227 | """Logging handler that does nothing with messages. |
228 | |
229 | At the moment, we don't want to do anything with the Twisted log messages |
230 | - that go to the 'codehosting' logger, and we also don't want warnings about |
231 | + that go to the SSH server logger, and we also don't want warnings about |
232 | there being no handlers. Hence, we use this do-nothing handler. |
233 | """ |
234 | |
235 | def emit(self, record): |
236 | pass |
237 | - |
238 | - |
239 | -class ILoggingEvent(Interface): |
240 | - """An event is a logging event if it has a message and a severity level. |
241 | - |
242 | - Events that provide this interface will be logged in codehosting access |
243 | - log. |
244 | - """ |
245 | - |
246 | - level = Attribute("The level to log the event at.") |
247 | - message = Attribute("The message to log.") |
248 | - |
249 | - |
250 | -class LoggingEvent: |
251 | - """An event that can be logged to a Python logger. |
252 | - |
253 | - :ivar level: The level to log itself as. This should be defined as a |
254 | - class variable in subclasses. |
255 | - :ivar template: The format string of the message to log. This should be |
256 | - defined as a class variable in subclasses. |
257 | - """ |
258 | - |
259 | - implements(ILoggingEvent) |
260 | - |
261 | - def __init__(self, level=None, template=None, **data): |
262 | - """Construct a logging event. |
263 | - |
264 | - :param level: The level to log the event as. If specified, overrides |
265 | - the 'level' class variable. |
266 | - :param template: The format string of the message to log. If |
267 | - specified, overrides the 'template' class variable. |
268 | - :param **data: Information to be logged. Entries will be substituted |
269 | - into the template and stored as attributes. |
270 | - """ |
271 | - if level is not None: |
272 | - self._level = level |
273 | - if template is not None: |
274 | - self.template = template |
275 | - self._data = data |
276 | - |
277 | - @property |
278 | - def level(self): |
279 | - """See `ILoggingEvent`.""" |
280 | - return self._level |
281 | - |
282 | - @property |
283 | - def message(self): |
284 | - """See `ILoggingEvent`.""" |
285 | - return self.template % self._data |
286 | - |
287 | - |
288 | -class ServerStarting(LoggingEvent): |
289 | - |
290 | - level = logging.INFO |
291 | - template = '---- Server started ----' |
292 | - |
293 | - |
294 | -class ServerStopped(LoggingEvent): |
295 | - |
296 | - level = logging.INFO |
297 | - template = '---- Server stopped ----' |
298 | - |
299 | - |
300 | -class UserConnected(LoggingEvent): |
301 | - |
302 | - level = logging.INFO |
303 | - template = '[%(session_id)s] %(address)s connected.' |
304 | - |
305 | - def __init__(self, transport, address): |
306 | - LoggingEvent.__init__( |
307 | - self, session_id=id(transport), address=address) |
308 | - |
309 | - |
310 | -class AuthenticationFailed(LoggingEvent): |
311 | - |
312 | - level = logging.INFO |
313 | - template = '[%(session_id)s] failed to authenticate.' |
314 | - |
315 | - def __init__(self, transport): |
316 | - LoggingEvent.__init__(self, session_id=id(transport)) |
317 | - |
318 | - |
319 | -class UserDisconnected(LoggingEvent): |
320 | - |
321 | - level = logging.INFO |
322 | - template = '[%(session_id)s] disconnected.' |
323 | - |
324 | - def __init__(self, transport): |
325 | - LoggingEvent.__init__(self, session_id=id(transport)) |
326 | - |
327 | - |
328 | -class AvatarEvent(LoggingEvent): |
329 | - """Base avatar event.""" |
330 | - |
331 | - level = logging.INFO |
332 | - |
333 | - def __init__(self, avatar): |
334 | - self.avatar = avatar |
335 | - LoggingEvent.__init__( |
336 | - self, session_id=id(avatar.transport), username=avatar.username) |
337 | - |
338 | - |
339 | -class UserLoggedIn(AvatarEvent): |
340 | - |
341 | - template = '[%(session_id)s] %(username)s logged in.' |
342 | - |
343 | - |
344 | -class UserLoggedOut(AvatarEvent): |
345 | - |
346 | - template = '[%(session_id)s] %(username)s disconnected.' |
347 | - |
348 | - |
349 | -class SFTPStarted(AvatarEvent): |
350 | - |
351 | - template = '[%(session_id)s] %(username)s started SFTP session.' |
352 | - |
353 | - |
354 | -class SFTPClosed(AvatarEvent): |
355 | - |
356 | - template = '[%(session_id)s] %(username)s closed SFTP session.' |
357 | - |
358 | - |
359 | -class BazaarSSHStarted(AvatarEvent): |
360 | - |
361 | - template = '[%(session_id)s] %(username)s started bzr+ssh session.' |
362 | - |
363 | - |
364 | -class BazaarSSHClosed(AvatarEvent): |
365 | - |
366 | - template = '[%(session_id)s] %(username)s closed bzr+ssh session.' |
367 | - |
368 | - |
369 | -@zope.component.adapter(ILoggingEvent) |
370 | -def _log_event(event): |
371 | - """Log 'event' to the codehosting logger.""" |
372 | - get_access_logger().log(event.level, event.message) |
373 | |
374 | === modified file 'lib/lp/codehosting/sshserver/auth.py' |
375 | --- lib/lp/codehosting/sshserver/auth.py 2010-04-13 18:32:15 +0000 |
376 | +++ lib/lp/codehosting/sshserver/auth.py 2010-04-16 18:57:30 +0000 |
377 | @@ -39,7 +39,7 @@ |
378 | from zope.interface import implements |
379 | |
380 | from lp.codehosting import sftp |
381 | -from lp.codehosting.sshserver import accesslog |
382 | +from lp.codehosting.sshserver import events |
383 | from lp.codehosting.sshserver.session import ( |
384 | launch_smart_server, PatchedSSHSession) |
385 | from lp.services.twistedsupport.xmlrpc import trap_fault |
386 | @@ -71,7 +71,7 @@ |
387 | self.subsystemLookup = {'sftp': sftp.FileTransferServer} |
388 | |
389 | def logout(self): |
390 | - notify(accesslog.UserLoggedOut(self)) |
391 | + notify(events.UserLoggedOut(self)) |
392 | |
393 | |
394 | components.registerAdapter(launch_smart_server, LaunchpadAvatar, ISession) |
395 | @@ -215,7 +215,7 @@ |
396 | # connection in the logs. |
397 | avatar = self.transport.avatar |
398 | avatar.transport = self.transport |
399 | - notify(accesslog.UserLoggedIn(avatar)) |
400 | + notify(events.UserLoggedIn(avatar)) |
401 | return ret |
402 | |
403 | def _ebLogToBanner(self, reason): |
404 | |
405 | === added file 'lib/lp/codehosting/sshserver/events.py' |
406 | --- lib/lp/codehosting/sshserver/events.py 1970-01-01 00:00:00 +0000 |
407 | +++ lib/lp/codehosting/sshserver/events.py 2010-04-16 18:57:30 +0000 |
408 | @@ -0,0 +1,155 @@ |
409 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
410 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
411 | + |
412 | +"""Events generated by the SSH server.""" |
413 | + |
414 | +__metaclass__ = type |
415 | +__all__ = [ |
416 | + 'AuthenticationFailed', |
417 | + 'BazaarSSHClosed', |
418 | + 'BazaarSSHStarted', |
419 | + 'ILoggingEvent', |
420 | + 'LoggingEvent', |
421 | + 'ServerStarting', |
422 | + 'ServerStopped', |
423 | + 'SFTPClosed', |
424 | + 'SFTPStarted', |
425 | + 'UserConnected', |
426 | + 'UserDisconnected', |
427 | + 'UserLoggedIn', |
428 | + 'UserLoggedOut', |
429 | + ] |
430 | + |
431 | +import logging |
432 | + |
433 | +from zope.interface import Attribute, implements, Interface |
434 | + |
435 | + |
436 | +class ILoggingEvent(Interface): |
437 | + """An event is a logging event if it has a message and a severity level. |
438 | + |
439 | + Events that provide this interface will be logged in codehosting access |
440 | + log. |
441 | + """ |
442 | + |
443 | + level = Attribute("The level to log the event at.") |
444 | + message = Attribute("The message to log.") |
445 | + |
446 | + |
447 | +class LoggingEvent: |
448 | + """An event that can be logged to a Python logger. |
449 | + |
450 | + :ivar level: The level to log itself as. This should be defined as a |
451 | + class variable in subclasses. |
452 | + :ivar template: The format string of the message to log. This should be |
453 | + defined as a class variable in subclasses. |
454 | + """ |
455 | + |
456 | + implements(ILoggingEvent) |
457 | + |
458 | + def __init__(self, level=None, template=None, **data): |
459 | + """Construct a logging event. |
460 | + |
461 | + :param level: The level to log the event as. If specified, overrides |
462 | + the 'level' class variable. |
463 | + :param template: The format string of the message to log. If |
464 | + specified, overrides the 'template' class variable. |
465 | + :param **data: Information to be logged. Entries will be substituted |
466 | + into the template and stored as attributes. |
467 | + """ |
468 | + if level is not None: |
469 | + self._level = level |
470 | + if template is not None: |
471 | + self.template = template |
472 | + self._data = data |
473 | + |
474 | + @property |
475 | + def level(self): |
476 | + """See `ILoggingEvent`.""" |
477 | + return self._level |
478 | + |
479 | + @property |
480 | + def message(self): |
481 | + """See `ILoggingEvent`.""" |
482 | + return self.template % self._data |
483 | + |
484 | + |
485 | +class ServerStarting(LoggingEvent): |
486 | + |
487 | + level = logging.INFO |
488 | + template = '---- Server started ----' |
489 | + |
490 | + |
491 | +class ServerStopped(LoggingEvent): |
492 | + |
493 | + level = logging.INFO |
494 | + template = '---- Server stopped ----' |
495 | + |
496 | + |
497 | +class UserConnected(LoggingEvent): |
498 | + |
499 | + level = logging.INFO |
500 | + template = '[%(session_id)s] %(address)s connected.' |
501 | + |
502 | + def __init__(self, transport, address): |
503 | + LoggingEvent.__init__( |
504 | + self, session_id=id(transport), address=address) |
505 | + |
506 | + |
507 | +class AuthenticationFailed(LoggingEvent): |
508 | + |
509 | + level = logging.INFO |
510 | + template = '[%(session_id)s] failed to authenticate.' |
511 | + |
512 | + def __init__(self, transport): |
513 | + LoggingEvent.__init__(self, session_id=id(transport)) |
514 | + |
515 | + |
516 | +class UserDisconnected(LoggingEvent): |
517 | + |
518 | + level = logging.INFO |
519 | + template = '[%(session_id)s] disconnected.' |
520 | + |
521 | + def __init__(self, transport): |
522 | + LoggingEvent.__init__(self, session_id=id(transport)) |
523 | + |
524 | + |
525 | +class AvatarEvent(LoggingEvent): |
526 | + """Base avatar event.""" |
527 | + |
528 | + level = logging.INFO |
529 | + |
530 | + def __init__(self, avatar): |
531 | + self.avatar = avatar |
532 | + LoggingEvent.__init__( |
533 | + self, session_id=id(avatar.transport), username=avatar.username) |
534 | + |
535 | + |
536 | +class UserLoggedIn(AvatarEvent): |
537 | + |
538 | + template = '[%(session_id)s] %(username)s logged in.' |
539 | + |
540 | + |
541 | +class UserLoggedOut(AvatarEvent): |
542 | + |
543 | + template = '[%(session_id)s] %(username)s disconnected.' |
544 | + |
545 | + |
546 | +class SFTPStarted(AvatarEvent): |
547 | + |
548 | + template = '[%(session_id)s] %(username)s started SFTP session.' |
549 | + |
550 | + |
551 | +class SFTPClosed(AvatarEvent): |
552 | + |
553 | + template = '[%(session_id)s] %(username)s closed SFTP session.' |
554 | + |
555 | + |
556 | +class BazaarSSHStarted(AvatarEvent): |
557 | + |
558 | + template = '[%(session_id)s] %(username)s started bzr+ssh session.' |
559 | + |
560 | + |
561 | +class BazaarSSHClosed(AvatarEvent): |
562 | + |
563 | + template = '[%(session_id)s] %(username)s closed bzr+ssh session.' |
564 | |
565 | === modified file 'lib/lp/codehosting/sshserver/service.py' |
566 | --- lib/lp/codehosting/sshserver/service.py 2010-04-15 09:14:06 +0000 |
567 | +++ lib/lp/codehosting/sshserver/service.py 2010-04-16 18:57:30 +0000 |
568 | @@ -8,10 +8,17 @@ |
569 | |
570 | __metaclass__ = type |
571 | __all__ = [ |
572 | + 'ACCESS_LOG_NAME', |
573 | + 'get_key_path', |
574 | + 'LOG_NAME', |
575 | + 'make_portal', |
576 | + 'PRIVATE_KEY_FILE', |
577 | + 'PUBLIC_KEY_FILE', |
578 | 'SSHService', |
579 | ] |
580 | |
581 | |
582 | +import logging |
583 | import os |
584 | |
585 | from twisted.application import service, strports |
586 | @@ -25,9 +32,20 @@ |
587 | from zope.event import notify |
588 | |
589 | from canonical.config import config |
590 | -from lp.codehosting.sshserver import accesslog |
591 | +from lp.codehosting.sshserver import accesslog, events |
592 | from lp.codehosting.sshserver.auth import get_portal, SSHUserAuthServer |
593 | from lp.services.twistedsupport import gatherResults |
594 | +from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting |
595 | + |
596 | + |
597 | +# The names of the key files of the server itself. The directory itself is |
598 | +# given in config.codehosting.host_key_pair_path. |
599 | +PRIVATE_KEY_FILE = 'ssh_host_key_rsa' |
600 | +PUBLIC_KEY_FILE = 'ssh_host_key_rsa.pub' |
601 | + |
602 | +OOPS_CONFIG_SECTION = 'codehosting' |
603 | +LOG_NAME = 'codehosting' |
604 | +ACCESS_LOG_NAME = 'codehosting.access' |
605 | |
606 | |
607 | # The names of the key files of the server itself. The directory itself is |
608 | @@ -43,26 +61,46 @@ |
609 | self.transport.setTcpKeepAlive(True) |
610 | |
611 | |
612 | -def get_key_path(key_filename): |
613 | - key_directory = config.codehosting.host_key_pair_path |
614 | - return os.path.join(config.root, key_directory, key_filename) |
615 | - |
616 | - |
617 | -def make_portal(): |
618 | - """Create and return a `Portal` for the SSH service. |
619 | - |
620 | - This portal accepts SSH credentials and returns our customized SSH |
621 | - avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`). |
622 | - """ |
623 | - authentication_proxy = Proxy( |
624 | - config.codehosting.authentication_endpoint) |
625 | - branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) |
626 | - return get_portal(authentication_proxy, branchfs_proxy) |
627 | - |
628 | - |
629 | - |
630 | +<<<<<<< TREE |
631 | +def get_key_path(key_filename): |
632 | + key_directory = config.codehosting.host_key_pair_path |
633 | + return os.path.join(config.root, key_directory, key_filename) |
634 | + |
635 | + |
636 | +def make_portal(): |
637 | + """Create and return a `Portal` for the SSH service. |
638 | + |
639 | + This portal accepts SSH credentials and returns our customized SSH |
640 | + avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`). |
641 | + """ |
642 | + authentication_proxy = Proxy( |
643 | + config.codehosting.authentication_endpoint) |
644 | + branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) |
645 | + return get_portal(authentication_proxy, branchfs_proxy) |
646 | + |
647 | + |
648 | + |
649 | +======= |
650 | +def get_key_path(key_filename): |
651 | + key_directory = config.codehosting.host_key_pair_path |
652 | + return os.path.join(config.root, key_directory, key_filename) |
653 | + |
654 | + |
655 | +def make_portal(): |
656 | + """Create and return a `Portal` for the SSH service. |
657 | + |
658 | + This portal accepts SSH credentials and returns our customized SSH |
659 | + avatars (see `lp.codehosting.sshserver.auth.LaunchpadAvatar`). |
660 | + """ |
661 | + authentication_proxy = Proxy( |
662 | + config.codehosting.authentication_endpoint) |
663 | + branchfs_proxy = Proxy(config.codehosting.branchfs_endpoint) |
664 | + return get_portal(authentication_proxy, branchfs_proxy) |
665 | + |
666 | + |
667 | +>>>>>>> MERGE-SOURCE |
668 | class Factory(SSHFactory): |
669 | - """SSH factory that uses the codehosting custom authentication. |
670 | + """SSH factory that uses Launchpad's custom authentication. |
671 | |
672 | This class tells the SSH service to use our custom authentication service |
673 | and configures the host keys for the SSH server. It also logs connection |
674 | @@ -105,7 +143,7 @@ |
675 | transport._realConnectionLost = transport.connectionLost |
676 | transport.connectionLost = ( |
677 | lambda reason: self.connectionLost(transport, reason)) |
678 | - notify(accesslog.UserConnected(transport, address)) |
679 | + notify(events.UserConnected(transport, address)) |
680 | return transport |
681 | |
682 | def connectionLost(self, transport, reason): |
683 | @@ -124,9 +162,15 @@ |
684 | # b) the server doesn't normally generate a "go away" event. |
685 | # Rather, the client simply stops trying. |
686 | if getattr(transport, 'avatar', None) is None: |
687 | +<<<<<<< TREE |
688 | notify(accesslog.AuthenticationFailed(transport)) |
689 | notify(accesslog.UserDisconnected(transport)) |
690 | |
691 | +======= |
692 | + notify(events.AuthenticationFailed(transport)) |
693 | + notify(events.UserDisconnected(transport)) |
694 | + |
695 | +>>>>>>> MERGE-SOURCE |
696 | def getPublicKeys(self): |
697 | """Return the server's configured public key. |
698 | |
699 | @@ -143,6 +187,7 @@ |
700 | |
701 | |
702 | class SSHService(service.Service): |
703 | +<<<<<<< TREE |
704 | """A Twisted service for the codehosting SSH server.""" |
705 | |
706 | def __init__(self, portal, private_key_path, public_key_path, |
707 | @@ -159,20 +204,71 @@ |
708 | connection that isn't doing anything. Defaults to 3600. |
709 | :param banner: An announcement printed to users when they connect. |
710 | By default, announce nothing. |
711 | +======= |
712 | + """A Twisted service for the SSH server.""" |
713 | + |
714 | + def __init__(self, portal, private_key_path, public_key_path, |
715 | + oops_configuration, main_log, access_log, |
716 | + access_log_path, strport='tcp:22', idle_timeout=3600, |
717 | + banner=None): |
718 | + """Construct an SSH service. |
719 | + |
720 | + :param portal: The `Portal` that turns authentication requests into |
721 | + views on the system. |
722 | + :param private_key_path: The path to the SSH server's private key. |
723 | + :param public_key_path: The path to the SSH server's public key. |
724 | + :param oops_configuration: The section of the configuration file with |
725 | + the OOPS config details for this server. |
726 | + :param main_log: A `logging.Logger` object to log most of the server |
727 | + stuff to. |
728 | + :param access_log: A `logging.Logger` object to log the server access |
729 | + details to. |
730 | + :param access_log_path: The path to the access log file. |
731 | + :param strport: The port to run the server on, expressed in Twisted's |
732 | + "strports" mini-language. Defaults to 'tcp:22'. |
733 | + :param idle_timeout: The number of seconds to wait before killing a |
734 | + connection that isn't doing anything. Defaults to 3600. |
735 | + :param banner: An announcement printed to users when they connect. |
736 | + By default, announce nothing. |
737 | +>>>>>>> MERGE-SOURCE |
738 | """ |
739 | ssh_factory = TimeoutFactory( |
740 | - Factory( |
741 | - portal, |
742 | - private_key=Key.fromFile(private_key_path), |
743 | - public_key=Key.fromFile(public_key_path), |
744 | - banner=banner), |
745 | - timeoutPeriod=idle_timeout) |
746 | - self.service = strports.service(strport, ssh_factory) |
747 | +<<<<<<< TREE |
748 | + Factory( |
749 | + portal, |
750 | + private_key=Key.fromFile(private_key_path), |
751 | + public_key=Key.fromFile(public_key_path), |
752 | + banner=banner), |
753 | + timeoutPeriod=idle_timeout) |
754 | + self.service = strports.service(strport, ssh_factory) |
755 | +======= |
756 | + Factory( |
757 | + portal, |
758 | + private_key=Key.fromFile(private_key_path), |
759 | + public_key=Key.fromFile(public_key_path), |
760 | + banner=banner), |
761 | + timeoutPeriod=idle_timeout) |
762 | + self.service = strports.service(strport, ssh_factory) |
763 | + self._oops_configuration = oops_configuration |
764 | + self._main_log = main_log |
765 | + self._access_log = access_log |
766 | + self._access_log_path = access_log_path |
767 | +>>>>>>> MERGE-SOURCE |
768 | |
769 | def startService(self): |
770 | """Start the SSH service.""" |
771 | +<<<<<<< TREE |
772 | accesslog.LoggingManager().setUp(configure_oops_reporting=True) |
773 | notify(accesslog.ServerStarting()) |
774 | +======= |
775 | + manager = accesslog.LoggingManager( |
776 | + logging.getLogger(self._main_log), |
777 | + logging.getLogger(self._access_log_path), |
778 | + self._access_log_path) |
779 | + manager.setUp() |
780 | + set_up_oops_reporting(self._oops_configuration) |
781 | + notify(events.ServerStarting()) |
782 | +>>>>>>> MERGE-SOURCE |
783 | # By default, only the owner of files should be able to write to them. |
784 | # Perhaps in the future this line will be deleted and the umask |
785 | # managed by the startup script. |
786 | @@ -186,6 +282,6 @@ |
787 | defer.maybeDeferred(service.Service.stopService, self), |
788 | defer.maybeDeferred(self.service.stopService)]) |
789 | def log_stopped(ignored): |
790 | - notify(accesslog.ServerStopped()) |
791 | + notify(events.ServerStopped()) |
792 | return ignored |
793 | return deferred.addBoth(log_stopped) |
794 | |
795 | === modified file 'lib/lp/codehosting/sshserver/session.py' |
796 | --- lib/lp/codehosting/sshserver/session.py 2010-03-15 05:38:03 +0000 |
797 | +++ lib/lp/codehosting/sshserver/session.py 2010-04-16 18:57:30 +0000 |
798 | @@ -22,7 +22,7 @@ |
799 | |
800 | from canonical.config import config |
801 | from lp.codehosting import get_bzr_path |
802 | -from lp.codehosting.sshserver import accesslog |
803 | +from lp.codehosting.sshserver import events |
804 | |
805 | |
806 | class PatchedSSHSession(session.SSHSession, object): |
807 | @@ -116,7 +116,7 @@ |
808 | def closed(self): |
809 | """See ISession.""" |
810 | if self._transport is not None: |
811 | - notify(accesslog.BazaarSSHClosed(self.avatar)) |
812 | + notify(events.BazaarSSHClosed(self.avatar)) |
813 | try: |
814 | self._transport.signalProcess('HUP') |
815 | except (OSError, ProcessExitedAlready): |
816 | @@ -156,7 +156,7 @@ |
817 | # XXX: JonathanLange 2008-12-23: This is something of an abstraction |
818 | # violation. Apart from this line, this class knows nothing about |
819 | # Bazaar. |
820 | - notify(accesslog.BazaarSSHStarted(self.avatar)) |
821 | + notify(events.BazaarSSHStarted(self.avatar)) |
822 | self._transport = self.reactor.spawnProcess( |
823 | protocol, executable, arguments, env=self.environment) |
824 | |
825 | |
826 | === added file 'lib/lp/codehosting/sshserver/tests/test_events.py' |
827 | --- lib/lp/codehosting/sshserver/tests/test_events.py 1970-01-01 00:00:00 +0000 |
828 | +++ lib/lp/codehosting/sshserver/tests/test_events.py 2010-04-16 18:57:30 +0000 |
829 | @@ -0,0 +1,91 @@ |
830 | +# Copyright 2010 Canonical Ltd. This software is licensed under the |
831 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
832 | + |
833 | +"""Tests for the logging events.""" |
834 | + |
835 | +__metaclass__ = type |
836 | + |
837 | +import logging |
838 | +import unittest |
839 | + |
840 | +from zope.component import adapter, getGlobalSiteManager, provideHandler |
841 | +# This non-standard import is necessary to hook up the event system. |
842 | +import zope.component.event |
843 | +from zope.event import notify |
844 | + |
845 | +from lp.codehosting.sshserver.events import ILoggingEvent, LoggingEvent |
846 | + |
847 | +from lp.testing import TestCase |
848 | + |
849 | + |
850 | +class ListHandler(logging.Handler): |
851 | + """Logging handler that just appends records to a list. |
852 | + |
853 | + This handler isn't intended to be used by production code -- memory leak |
854 | + city! -- instead it's useful for unit tests that want to make sure the |
855 | + right events are being logged. |
856 | + """ |
857 | + |
858 | + def __init__(self, logging_list): |
859 | + """Construct a `ListHandler`. |
860 | + |
861 | + :param logging_list: A list that will be appended to. The handler |
862 | + mutates this list. |
863 | + """ |
864 | + logging.Handler.__init__(self) |
865 | + self._list = logging_list |
866 | + |
867 | + def emit(self, record): |
868 | + """Append 'record' to the list.""" |
869 | + self._list.append(record) |
870 | + |
871 | + |
872 | +class TestLoggingEvent(TestCase): |
873 | + |
874 | + def assertLogs(self, records, function, *args, **kwargs): |
875 | + """Assert 'function' logs 'records' when run with the given args.""" |
876 | + logged_events = [] |
877 | + handler = ListHandler(logged_events) |
878 | + self.logger.addHandler(handler) |
879 | + result = function(*args, **kwargs) |
880 | + self.logger.removeHandler(handler) |
881 | + self.assertEqual( |
882 | + [(record.levelno, record.getMessage()) |
883 | + for record in logged_events], records) |
884 | + return result |
885 | + |
886 | + def assertEventLogs(self, record, logging_event): |
887 | + self.assertLogs([record], notify, logging_event) |
888 | + |
889 | + def setUp(self): |
890 | + TestCase.setUp(self) |
891 | + logger = logging.getLogger(self.factory.getUniqueString()) |
892 | + logger.setLevel(logging.DEBUG) |
893 | + self.logger = logger |
894 | + |
895 | + @adapter(ILoggingEvent) |
896 | + def _log_event(event): |
897 | + logger.log(event.level, event.message) |
898 | + |
899 | + provideHandler(_log_event) |
900 | + self.addCleanup(getGlobalSiteManager().unregisterHandler, _log_event) |
901 | + |
902 | + def test_level(self): |
903 | + event = LoggingEvent(logging.CRITICAL, "foo") |
904 | + self.assertEventLogs((logging.CRITICAL, 'foo'), event) |
905 | + |
906 | + def test_formatting(self): |
907 | + event = LoggingEvent(logging.DEBUG, "foo: %(name)s", name="bar") |
908 | + self.assertEventLogs((logging.DEBUG, 'foo: bar'), event) |
909 | + |
910 | + def test_subclass(self): |
911 | + class SomeEvent(LoggingEvent): |
912 | + template = "%(something)s happened." |
913 | + level = logging.INFO |
914 | + self.assertEventLogs( |
915 | + (logging.INFO, 'foo happened.'), SomeEvent(something='foo')) |
916 | + |
917 | + |
918 | +def test_suite(): |
919 | + return unittest.TestLoader().loadTestsFromName(__name__) |
920 | + |
921 | |
922 | === modified file 'lib/lp/codehosting/sshserver/tests/test_logging.py' |
923 | --- lib/lp/codehosting/sshserver/tests/test_logging.py 2010-01-22 03:27:59 +0000 |
924 | +++ lib/lp/codehosting/sshserver/tests/test_logging.py 2010-04-16 18:57:30 +0000 |
925 | @@ -7,26 +7,48 @@ |
926 | |
927 | import codecs |
928 | import logging |
929 | +import os |
930 | from StringIO import StringIO |
931 | +import tempfile |
932 | import unittest |
933 | import sys |
934 | |
935 | from bzrlib.tests import TestCase as BzrTestCase |
936 | |
937 | -from twisted.python import log as tplog |
938 | - |
939 | import zope.component.event |
940 | -from zope.event import notify |
941 | |
942 | -from lp.codehosting.sshserver.accesslog import ( |
943 | - _log_event, get_access_logger, get_codehosting_logger, LoggingEvent, |
944 | - LoggingManager) |
945 | -from canonical.config import config |
946 | from canonical.launchpad.scripts import WatchedFileHandler |
947 | +from lp.codehosting.sshserver.accesslog import LoggingManager |
948 | from lp.testing import TestCase |
949 | |
950 | |
951 | -class TestLoggingBazaarInteraction(BzrTestCase): |
952 | +class LoggingManagerMixin: |
953 | + |
954 | + _log_count = 0 |
955 | + |
956 | + def makeLogger(self, name=None): |
957 | + if name is None: |
958 | + self._log_count += 1 |
959 | + name = '%s-%s' % (self.id().split('.')[-1], self._log_count) |
960 | + return logging.getLogger(name) |
961 | + |
962 | + def installLoggingManager(self, main_log=None, access_log=None, |
963 | + access_log_path=None): |
964 | + if main_log is None: |
965 | + main_log = self.makeLogger() |
966 | + if access_log is None: |
967 | + access_log = self.makeLogger() |
968 | + if access_log_path is None: |
969 | + fd, access_log_path = tempfile.mkstemp() |
970 | + os.close(fd) |
971 | + self.addCleanup(os.unlink, access_log_path) |
972 | + manager = LoggingManager(main_log, access_log, access_log_path) |
973 | + manager.setUp() |
974 | + self.addCleanup(manager.tearDown) |
975 | + return manager |
976 | + |
977 | + |
978 | +class TestLoggingBazaarInteraction(BzrTestCase, LoggingManagerMixin): |
979 | |
980 | def setUp(self): |
981 | BzrTestCase.setUp(self) |
982 | @@ -44,9 +66,7 @@ |
983 | root_handlers = logging.getLogger('').handlers |
984 | bzr_handlers = logging.getLogger('bzr').handlers |
985 | |
986 | - manager = LoggingManager() |
987 | - manager.setUp() |
988 | - self.addCleanup(manager.tearDown) |
989 | + self.installLoggingManager() |
990 | |
991 | self.assertEqual(root_handlers, logging.getLogger('').handlers) |
992 | self.assertEqual(bzr_handlers, logging.getLogger('bzr').handlers) |
993 | @@ -55,30 +75,24 @@ |
994 | # Once logging setup is called, any messages logged to the |
995 | # codehosting logger should *not* be logged to stderr. If they are, |
996 | # they will appear on the user's terminal. |
997 | - manager = LoggingManager() |
998 | - manager.setUp() |
999 | - self.addCleanup(manager.tearDown) |
1000 | + log = self.makeLogger() |
1001 | + self.installLoggingManager(log) |
1002 | |
1003 | # Make sure that a logged message does not go to stderr. |
1004 | - get_codehosting_logger().info('Hello hello') |
1005 | + log.info('Hello hello') |
1006 | self.assertEqual(sys.stderr.getvalue(), '') |
1007 | |
1008 | |
1009 | -class TestLoggingManager(TestCase): |
1010 | - |
1011 | - def test_returns_codehosting_logger(self): |
1012 | - # get_codehosting_logger returns the 'codehosting' logger. |
1013 | - self.assertIs( |
1014 | - logging.getLogger('codehosting'), get_codehosting_logger()) |
1015 | - |
1016 | - def test_codehosting_handlers(self): |
1017 | - # There needs to be at least one handler for the codehosting root |
1018 | - # logger. |
1019 | - manager = LoggingManager() |
1020 | - manager.setUp() |
1021 | - self.addCleanup(manager.tearDown) |
1022 | - handlers = get_codehosting_logger().handlers |
1023 | - self.assertNotEqual([], handlers) |
1024 | +class TestLoggingManager(TestCase, LoggingManagerMixin): |
1025 | + |
1026 | + def test_main_log_handlers(self): |
1027 | + # There needs to be at least one handler for the root logger. If there |
1028 | + # isn't, we'll get constant errors complaining about the lack of |
1029 | + # logging handlers. |
1030 | + log = self.makeLogger() |
1031 | + self.assertEqual([], log.handlers) |
1032 | + self.installLoggingManager(log) |
1033 | + self.assertNotEqual([], log.handlers) |
1034 | |
1035 | def _get_handlers(self): |
1036 | registrations = ( |
1037 | @@ -87,126 +101,51 @@ |
1038 | registration.factory |
1039 | for registration in registrations] |
1040 | |
1041 | - def test_log_event_handler_not_registered(self): |
1042 | - self.assertNotIn(_log_event, self._get_handlers()) |
1043 | - |
1044 | def test_set_up_registers_event_handler(self): |
1045 | - manager = LoggingManager() |
1046 | - manager.setUp() |
1047 | - self.addCleanup(manager.tearDown) |
1048 | - self.assertIn(_log_event, self._get_handlers()) |
1049 | + manager = self.installLoggingManager() |
1050 | + self.assertIn(manager._log_event, self._get_handlers()) |
1051 | |
1052 | def test_teardown_restores_event_handlers(self): |
1053 | handlers = self._get_handlers() |
1054 | - manager = LoggingManager() |
1055 | - manager.setUp() |
1056 | + manager = self.installLoggingManager() |
1057 | manager.tearDown() |
1058 | self.assertEqual(handlers, self._get_handlers()) |
1059 | |
1060 | def test_teardown_restores_level(self): |
1061 | - log = get_codehosting_logger() |
1062 | + log = self.makeLogger() |
1063 | old_level = log.level |
1064 | - manager = LoggingManager() |
1065 | - manager.setUp() |
1066 | + manager = self.installLoggingManager(log) |
1067 | manager.tearDown() |
1068 | self.assertEqual(old_level, log.level) |
1069 | |
1070 | - def test_teardown_restores_handlers(self): |
1071 | - log = get_codehosting_logger() |
1072 | + def test_teardown_restores_main_log_handlers(self): |
1073 | + # tearDown restores log handlers for the main logger. |
1074 | + log = self.makeLogger() |
1075 | handlers = list(log.handlers) |
1076 | - manager = LoggingManager() |
1077 | - manager.setUp() |
1078 | + manager = self.installLoggingManager(log) |
1079 | manager.tearDown() |
1080 | self.assertEqual(handlers, log.handlers) |
1081 | |
1082 | - def test_teardown_restores_twisted_observers(self): |
1083 | - observers = list(tplog.theLogPublisher.observers) |
1084 | - manager = LoggingManager() |
1085 | - manager.setUp(True) |
1086 | + def test_teardown_restores_access_log_handlers(self): |
1087 | + # tearDown restores log handlers for the access logger. |
1088 | + log = self.makeLogger() |
1089 | + handlers = list(log.handlers) |
1090 | + manager = self.installLoggingManager(access_log=log) |
1091 | manager.tearDown() |
1092 | - self.assertEqual(observers, list(tplog.theLogPublisher.observers)) |
1093 | + self.assertEqual(handlers, log.handlers) |
1094 | |
1095 | def test_access_handlers(self): |
1096 | # The logging setup installs a rotatable log handler that logs output |
1097 | # to config.codehosting.access_log. |
1098 | - manager = LoggingManager() |
1099 | - manager.setUp() |
1100 | - self.addCleanup(manager.tearDown) |
1101 | - [handler] = get_access_logger().handlers |
1102 | + directory = self.makeTemporaryDirectory() |
1103 | + access_log = self.makeLogger() |
1104 | + access_log_path = os.path.join(directory, 'access.log') |
1105 | + self.installLoggingManager( |
1106 | + access_log=access_log, |
1107 | + access_log_path=access_log_path) |
1108 | + [handler] = access_log.handlers |
1109 | self.assertIsInstance(handler, WatchedFileHandler) |
1110 | - self.assertEqual(config.codehosting.access_log, handler.baseFilename) |
1111 | - |
1112 | - def test_teardown_restores_access_handlers(self): |
1113 | - log = get_access_logger() |
1114 | - handlers = list(log.handlers) |
1115 | - manager = LoggingManager() |
1116 | - manager.setUp() |
1117 | - manager.tearDown() |
1118 | - self.assertEqual(handlers, log.handlers) |
1119 | - |
1120 | - |
1121 | -class ListHandler(logging.Handler): |
1122 | - """Logging handler that just appends records to a list. |
1123 | - |
1124 | - This handler isn't intended to be used by production code -- memory leak |
1125 | - city! -- instead it's useful for unit tests that want to make sure the |
1126 | - right events are being logged. |
1127 | - """ |
1128 | - |
1129 | - def __init__(self, logging_list): |
1130 | - """Construct a `ListHandler`. |
1131 | - |
1132 | - :param logging_list: A list that will be appended to. The handler |
1133 | - mutates this list. |
1134 | - """ |
1135 | - logging.Handler.__init__(self) |
1136 | - self._list = logging_list |
1137 | - |
1138 | - def emit(self, record): |
1139 | - """Append 'record' to the list.""" |
1140 | - self._list.append(record) |
1141 | - |
1142 | - |
1143 | -class TestLoggingEvent(TestCase): |
1144 | - |
1145 | - def assertLogs(self, records, function, *args, **kwargs): |
1146 | - """Assert 'function' logs 'records' when run with the given args.""" |
1147 | - logged_events = [] |
1148 | - handler = ListHandler(logged_events) |
1149 | - self.logger.addHandler(handler) |
1150 | - result = function(*args, **kwargs) |
1151 | - self.logger.removeHandler(handler) |
1152 | - self.assertEqual( |
1153 | - [(record.levelno, record.getMessage()) |
1154 | - for record in logged_events], records) |
1155 | - return result |
1156 | - |
1157 | - def assertEventLogs(self, record, logging_event): |
1158 | - self.assertLogs([record], notify, logging_event) |
1159 | - |
1160 | - def setUp(self): |
1161 | - TestCase.setUp(self) |
1162 | - zope.component.provideHandler(_log_event) |
1163 | - self.addCleanup( |
1164 | - zope.component.getGlobalSiteManager().unregisterHandler, |
1165 | - _log_event) |
1166 | - self.logger = get_codehosting_logger() |
1167 | - self.logger.setLevel(logging.DEBUG) |
1168 | - |
1169 | - def test_level(self): |
1170 | - event = LoggingEvent(logging.CRITICAL, "foo") |
1171 | - self.assertEventLogs((logging.CRITICAL, 'foo'), event) |
1172 | - |
1173 | - def test_formatting(self): |
1174 | - event = LoggingEvent(logging.DEBUG, "foo: %(name)s", name="bar") |
1175 | - self.assertEventLogs((logging.DEBUG, 'foo: bar'), event) |
1176 | - |
1177 | - def test_subclass(self): |
1178 | - class SomeEvent(LoggingEvent): |
1179 | - template = "%(something)s happened." |
1180 | - level = logging.INFO |
1181 | - self.assertEventLogs( |
1182 | - (logging.INFO, 'foo happened.'), SomeEvent(something='foo')) |
1183 | + self.assertEqual(access_log_path, handler.baseFilename) |
1184 | |
1185 | |
1186 | def test_suite(): |
Hi Jonathan,
I love the test simplifications (LoggingManager Mixin). The only things I noticed were:
1. Some missing imports (as per our chat),
2. Some tests not asserting the intermediate state (but you said that other tests in the module already do this, but will check (I didn't)), and
3. A duplicate test.
Great stuff!
IRC chat: .adapter? Is there a preference? event anyway
{{{
11:11 < noodles> jml: any reason that you're not importing adapter and using @adapter, rather than @zope.component
11:12 < jml> noodles: just me being lazy. I have to import zope.component.
11:13 < noodles> jml: ok, while you're there, provideHandler too I guess.
11:13 < jml> (as a side-effect, z.c.event import starts up the event system)
11:35 < noodles> jml: there are some tests that setup the logging manager, and then immediately tear it down and make an assertion about the end state, without asserting the intermediate state (ie. showing that it is *not* the same as the endstate). Is that not worthwhile? (if not, I'll stop doing it :) ).
11:36 < noodles> oops, wrong channel, but anyway...
11:36 < jml> noodles: I'm on a call atm.
11:36 < noodles> np.
11:36 < jml> noodles: I think you'll find that the intermediate state is tested in other tests.
11:37 < noodles> Ah, right.
11:37 * jml isn't sure though
}}}
Notes: codehosting/ sshserver/ accesslog. py' codehosting/ sshserver/ accesslog. py 2010-04-14 19:38:29 +0000 codehosting/ sshserver/ accesslog. py 2010-04-14 19:38:33 +0000
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -25,70 +14,72 @@ event launchpad. scripts import WatchedFileHandler sshserver. events import ILoggingEvent twistedsupport. loggingsupport import set_up_ oops_reporting oops_reporting= False): log_path = access_log_path oops_reporting: If True, install a Twisted log _logger( )
>
> # This non-standard import is necessary to hook up the event system.
> import zope.component.
> -from zope.interface import Attribute, implements, Interface
>
> -from canonical.config import config
> from canonical.
> +from lp.codehosting.
> from lp.services.utils import synchronize
> -from lp.services.
>
>
> class LoggingManager:
> """Class for managing codehosting logging."""
>
> - def setUp(self, configure_
> + def __init__(self, main_log, access_log, access_log_path):
> + """Construct the logging manager.
> +
> + :param main_log: The main log. Twisted will log to this.
> + :param access_log: The access log object.
> + :param access_log_path: The path to the file where access log
> + messages go.
> + """
> + self._main_log = main_log
> + self._access_log = access_log
> + self._access_
> + self._is_set_up = False
> +
> + def setUp(self):
> """Set up logging for the smart server.
>
> This sets up a debugging handler on the 'codehosting' logger, makes
> sure that things logged there won't go to stderr (necessary because of
> bzrlib.trace shenanigans) and then returns the 'codehosting' logger.
> -
> - :param configure_
> - observer that ensures unhandled exceptions get reported as OOPSes.
> """
> - log = get_codehosting
>...