Merge lp:~allenap/maas/normalise-logging--bug-1639182--yakkety--2.1 into lp:maas/2.1

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5558
Proposed branch: lp:~allenap/maas/normalise-logging--bug-1639182--yakkety--2.1
Merge into: lp:maas/2.1
Diff against target: 1003 lines (+347/-405)
9 files modified
services/rackd/run (+2/-1)
services/regiond/run (+3/-1)
services/regiond2/run (+3/-1)
src/provisioningserver/logger/__init__.py (+3/-0)
src/provisioningserver/logger/_twisted.py (+103/-125)
src/provisioningserver/logger/testing/__init__.py (+23/-2)
src/provisioningserver/logger/testing/logsomething.py (+5/-2)
src/provisioningserver/logger/tests/test__twisted.py (+201/-252)
src/provisioningserver/logger/tests/test_logger.py (+4/-21)
To merge this branch: bzr merge lp:~allenap/maas/normalise-logging--bug-1639182--yakkety--2.1
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+312039@code.launchpad.net

Commit message

Backport r5573 from lp:maas: Log consistently on both Xenial and Yakkety.

Twisted is going through a transition and some of the work done in Xenial for Twisted 16.0 does not have the desired effect in Yakkety with Twisted 16.4, and vice-versa.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Backport so self-reviewing.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'services/rackd/run'
--- services/rackd/run 2016-03-31 23:34:55 +0000
+++ services/rackd/run 2016-11-29 12:38:39 +0000
@@ -30,4 +30,5 @@
30script="$(readlink -f bin/twistd.rack)"30script="$(readlink -f bin/twistd.rack)"
3131
32exec $(command -v authbind && echo --deep) \32exec $(command -v authbind && echo --deep) \
33 "${script}" --nodaemon --pidfile="" maas-rackd33 "${script}" --logger=provisioningserver.logger.EventLogger \
34 --nodaemon --pidfile="" maas-rackd
3435
=== modified file 'services/regiond/run'
--- services/regiond/run 2016-06-22 17:03:02 +0000
+++ services/regiond/run 2016-11-29 12:38:39 +0000
@@ -33,4 +33,6 @@
3333
34# Exec the MAAS API and Web UI Server.34# Exec the MAAS API and Web UI Server.
35script="$(readlink -f bin/twistd.region)"35script="$(readlink -f bin/twistd.region)"
36exec "${script}" --nodaemon --pidfile="" maas-regiond36exec "${script}" \
37 --logger=provisioningserver.logger.EventLogger \
38 --nodaemon --pidfile="" maas-regiond
3739
=== modified file 'services/regiond2/run'
--- services/regiond2/run 2016-06-22 17:03:02 +0000
+++ services/regiond2/run 2016-11-29 12:38:39 +0000
@@ -33,4 +33,6 @@
3333
34# Exec the MAAS API and Web UI Server.34# Exec the MAAS API and Web UI Server.
35script="$(readlink -f bin/twistd.region)"35script="$(readlink -f bin/twistd.region)"
36exec "${script}" --nodaemon --pidfile="" maas-regiond36exec "${script}" \
37 --logger=provisioningserver.logger.EventLogger \
38 --nodaemon --pidfile="" maas-regiond
3739
=== modified file 'src/provisioningserver/logger/__init__.py'
--- src/provisioningserver/logger/__init__.py 2016-11-04 08:58:00 +0000
+++ src/provisioningserver/logger/__init__.py 2016-11-29 12:38:39 +0000
@@ -50,6 +50,7 @@
5050
51__all__ = [51__all__ = [
52 "configure",52 "configure",
53 "EventLogger",
53 "get_maas_logger",54 "get_maas_logger",
54 "LegacyLogger",55 "LegacyLogger",
55 "LoggingMode",56 "LoggingMode",
@@ -71,6 +72,7 @@
71from provisioningserver.logger._tftp import configure_tftp_logging72from provisioningserver.logger._tftp import configure_tftp_logging
72from provisioningserver.logger._twisted import (73from provisioningserver.logger._twisted import (
73 configure_twisted_logging,74 configure_twisted_logging,
75 EventLogger,
74 LegacyLogger,76 LegacyLogger,
75 set_twisted_verbosity,77 set_twisted_verbosity,
76 VerbosityOptions,78 VerbosityOptions,
@@ -146,6 +148,7 @@
146 for verbosity_setter in verbosity_setters:148 for verbosity_setter in verbosity_setters:
147 verbosity_setter(verbosity)149 verbosity_setter(verbosity)
148150
151
149verbosity_setters = [152verbosity_setters = [
150 set_standard_verbosity,153 set_standard_verbosity,
151 set_twisted_verbosity,154 set_twisted_verbosity,
152155
=== modified file 'src/provisioningserver/logger/_twisted.py'
--- src/provisioningserver/logger/_twisted.py 2016-11-07 20:51:43 +0000
+++ src/provisioningserver/logger/_twisted.py 2016-11-29 12:38:39 +0000
@@ -5,6 +5,7 @@
55
6__all__ = [6__all__ = [
7 "configure_twisted_logging",7 "configure_twisted_logging",
8 "EventLogger",
8 "LegacyLogger",9 "LegacyLogger",
9 "VerbosityOptions",10 "VerbosityOptions",
10]11]
@@ -29,7 +30,6 @@
29 log as twistedLegacy,30 log as twistedLegacy,
30 usage,31 usage,
31)32)
32from twisted.python.util import untilConcludes
3333
34# Map verbosity numbers to `twisted.logger` levels.34# Map verbosity numbers to `twisted.logger` levels.
35DEFAULT_TWISTED_VERBOSITY_LEVELS = {35DEFAULT_TWISTED_VERBOSITY_LEVELS = {
@@ -46,18 +46,6 @@
46 "Twisted verbosity map does not match expectations.")46 "Twisted verbosity map does not match expectations.")
4747
4848
49# Those levels for which we should emit log events.
50_filterByLevels = frozenset()
51
52
53def _filterByLevel(event):
54 """Only log if event's level is in `_filterByLevels`."""
55 if event.get("log_level") in _filterByLevels:
56 return twistedModern.PredicateResult.maybe
57 else:
58 return twistedModern.PredicateResult.no
59
60
61@typed49@typed
62def set_twisted_verbosity(verbosity: int):50def set_twisted_verbosity(verbosity: int):
63 """Reconfigure verbosity of the standard library's `logging` module."""51 """Reconfigure verbosity of the standard library's `logging` module."""
@@ -83,31 +71,9 @@
83 """71 """
84 set_twisted_verbosity(verbosity)72 set_twisted_verbosity(verbosity)
8573
86 # A list of markers for noise.74 warn_unless(hasattr(twistedLegacy, "startLoggingWithObserver"), (
87 noisy = (75 "No startLoggingWithObserver function found; please investigate!"))
88 {"log_system": "-", "log_text": "Log opened."},76 twistedLegacy.startLoggingWithObserver = _startLoggingWithObserver
89 {"log_system": "-", "log_text": "Main loop terminated."},
90 )
91
92 def filterByNoise(event, noisy=noisy):
93 """Only log if event is not noisy."""
94 for noise in noisy:
95 if all(key in event and event[key] == noise[key] for key in noise):
96 return twistedModern.PredicateResult.no
97 else:
98 return twistedModern.PredicateResult.maybe
99
100 predicates = _filterByLevel, filterByNoise
101
102 # When `twistd` starts the reactor it initialises the legacy logging
103 # system. Intercept this to wrap the observer in a level filter. We can
104 # use this same approach when not running under `twistd` too.
105 def startLoggingWithObserver(observer, setStdout=1):
106 observer = twistedModern.FilteringLogObserver(observer, predicates)
107 reallyStartLoggingWithObserver(observer, setStdout)
108
109 reallyStartLoggingWithObserver = twistedLegacy.startLoggingWithObserver
110 twistedLegacy.startLoggingWithObserver = startLoggingWithObserver
11177
112 # Customise warnings behaviour. Ensure that nothing else — neither the78 # Customise warnings behaviour. Ensure that nothing else — neither the
113 # standard library's `logging` module nor Django — clobbers this later.79 # standard library's `logging` module nor Django — clobbers this later.
@@ -120,17 +86,6 @@
120 twistedModern.globalLogBeginner.showwarning = warnings.showwarning86 twistedModern.globalLogBeginner.showwarning = warnings.showwarning
121 twistedLegacy.theLogPublisher.showwarning = warnings.showwarning87 twistedLegacy.theLogPublisher.showwarning = warnings.showwarning
12288
123 # Globally override Twisted's legacy logging format.
124 warn_unless(hasattr(twistedLegacy, "FileLogObserver"), (
125 "No t.p.log.FileLogObserver attribute found; please investigate!"))
126 LegacyFileLogObserver.install()
127
128 # Install a wrapper so that log events from `t.logger` are logged with a
129 # namespace and level by the legacy logger in `t.python.log`. This needs
130 # to be injected into the `t.p.log` module in order to process events as
131 # they move from the legacy to the modern systems.
132 LegacyLogObserverWrapper.install()
133
134 # Prevent `crochet` from initialising Twisted's logging.89 # Prevent `crochet` from initialising Twisted's logging.
135 warn_unless(hasattr(crochet._main, "_startLoggingWithObserver"), (90 warn_unless(hasattr(crochet._main, "_startLoggingWithObserver"), (
136 "No _startLoggingWithObserver function found; please investigate!"))91 "No _startLoggingWithObserver function found; please investigate!"))
@@ -155,81 +110,77 @@
155 # the original standard out stream when this process was started. This110 # the original standard out stream when this process was started. This
156 # bypasses any wrapping or redirection that may have been done elsewhere.111 # bypasses any wrapping or redirection that may have been done elsewhere.
157 if mode == LoggingMode.COMMAND:112 if mode == LoggingMode.COMMAND:
158 twistedLegacy.startLogging(sys.__stdout__, setStdout=False)113 twistedModern.globalLogBeginner.beginLoggingTo(
159114 [EventLogger()], discardBuffer=False, redirectStandardIO=False)
160115
161class LegacyFileLogObserver(twistedLegacy.FileLogObserver):116
162 """Log legacy/mixed events to a file, formatting the MAAS way."""117def EventLogger(outFile=sys.__stdout__):
163118 """Factory returning a `t.logger.ILogObserver`.
164 timeFormat = DEFAULT_LOG_FORMAT_DATE119
165 lineFormat = DEFAULT_LOG_FORMAT + "\n"120 This logs to the real standard out using MAAS's logging conventions.
166121
167 @classmethod122 Refer to this with `twistd`'s `--logger` argument.
168 def install(cls):123 """
169 """Install this wrapper in place of `log.FileLogObserver`."""124 return twistedModern.FilteringLogObserver(
170 twistedLegacy.FileLogObserver = cls125 twistedModern.FileLogObserver(outFile, _formatModernEvent),
171126 (_filterByLevel, _filterByNoise))
172 def emit(self, event):127
173 """Format and write out the given `event`."""128
174 text = twistedLegacy.textFromEventDict(event)129def _startLoggingWithObserver(observer, setStdout=1):
175 if text is None:130 """Replacement for `t.p.log.startLoggingWithObserver`.
176 return131
177132 When `twistd` starts in 16.0 it initialises the legacy logging system.
178 system = event["system"] if "system" in event else None133 Intercept this to DTRT with either a modern or legacy observer.
179 if system is None and "log_namespace" in event:134
180 system = event["log_namespace"]135 In Xenial (with Twisted 16.0) `observer` is probably a legacy observer,
181 # Logs written directly to `t.p.log.logfile` and `.logerr` get a136 like twisted.python.log.FileLogObserver, but we should check if it's
182 # namespace of "twisted.python.log". This is not interesting.137 modern. In either case we should call through to the `globalLogBeginner`
183 if system == twistedLegacy.__name__:138 ourselves. In Yakkety (with Twisted 16.4) this function will not be
184 system = None139 called; `t.application.app.AppLogger` does the right thing already.
185140 """
186 level = event["log_level"] if "log_level" in event else None141 if not twistedModern.ILogObserver.providedBy(observer):
187 if level is None:142 observer = twistedModern.LegacyLogObserverWrapper(observer)
188 if "isError" in event and event["isError"]:143 twistedModern.globalLogBeginner.beginLoggingTo(
189 level = twistedModern.LogLevel.critical144 [observer], discardBuffer=False, redirectStandardIO=bool(setStdout))
190 else:145
191 level = twistedModern.LogLevel.info146
192147_timeFormat = DEFAULT_LOG_FORMAT_DATE
193 line = twistedLegacy._safeFormat(self.lineFormat, {148_lineFormat = DEFAULT_LOG_FORMAT + "\n"
194 "asctime": self.formatTime(event["time"]),149
195 "levelname": "-" if level is None else level.name,150
196 "message": text.replace("\n", "\n\t"),151def _formatModernEvent(event):
197 "name": "-" if system is None else system,152 """Format a "modern" event according to MAAS's conventions."""
198 })153 text = twistedModern.formatEvent(event)
199154 time = event["log_time"] if "log_time" in event else None
200 untilConcludes(self.write, line)155 level = event["log_level"] if "log_level" in event else None
201 untilConcludes(self.flush)156 system = event["log_system"] if "log_system" in event else None
202157 if system is None and "log_namespace" in event:
203158 system = _getSystemName(event["log_namespace"])
204class LegacyLogObserverWrapper(twistedModern.LegacyLogObserverWrapper):159
205 """Ensure that `log_system` is set in the event.160 return _lineFormat % {
206161 "asctime": twistedModern.formatTime(time, _timeFormat),
207 A newly populated `log_system` value is seen by `LegacyLogObserverWrapper`162 "levelname": "-" if level is None else level.name,
208 (the superclass) and copied into the `system` key, later used when emitting163 "message": "-" if text is None else text.replace("\n", "\n\t"),
209 formatted log lines.164 "name": "-" if system is None else system,
210 """165 }
211166
212 @classmethod167
213 def install(cls):168def _getSystemName(system):
214 """Install this wrapper in place of `log.LegacyLogObserverWrapper`.169 """Return the "public" parts of `system`.
215170
216 Inject this wrapper into the `t.python.log` module then remove and171 `system` is a dot-separated name, e.g. a fully-qualified module name. This
217 re-add all the legacy observers so that they're re-wrapped.172 returns the leading parts of that name that are not pseudo-private, i.e.
218 """173 that start with an underscore. For example "a.b._c.d" would be transformed
219 twistedLegacy.LegacyLogObserverWrapper = cls174 into "a.b".
220 for observer in twistedLegacy.theLogPublisher.observers:175 """
221 twistedLegacy.theLogPublisher.removeObserver(observer)176 if system is None:
222 twistedLegacy.theLogPublisher.addObserver(observer)177 return None
223178 elif len(system) == 0:
224 def __call__(self, event):179 return None
225 # Be defensive: `system` could be missing or could have a value of180 elif system.startswith("_"):
226 # None. Same goes for `log_system` and `log_namespace`.181 return None
227 if event.get("system") is None and event.get("log_system") is None:182 else:
228 namespace = event.get("log_namespace")183 return system.split("._")[0]
229 if namespace is not None:
230 event["log_system"] = namespace
231 # Up-call, which will apply some more transformations.
232 return super().__call__(event)
233184
234185
235class LegacyLogger(twistedModern.Logger):186class LegacyLogger(twistedModern.Logger):
@@ -352,6 +303,33 @@
352 pass # We tried.303 pass # We tried.
353304
354305
306# Those levels for which we should emit log events.
307_filterByLevels = frozenset()
308
309
310def _filterByLevel(event):
311 """Only log if event's level is in `_filterByLevels`."""
312 if event.get("log_level") in _filterByLevels:
313 return twistedModern.PredicateResult.maybe
314 else:
315 return twistedModern.PredicateResult.no
316
317# A list of markers for noise.
318_filterByNoises = (
319 {"log_namespace": "log_legacy", "log_text": "Log opened."},
320 {"log_namespace": "log_legacy", "log_text": "Main loop terminated."},
321)
322
323
324def _filterByNoise(event):
325 """Only log if event is not noisy."""
326 for noise in _filterByNoises:
327 if all(key in event and event[key] == noise[key] for key in noise):
328 return twistedModern.PredicateResult.no
329 else:
330 return twistedModern.PredicateResult.maybe
331
332
355_observe_twisted_internet_tcp_noise = re.compile(333_observe_twisted_internet_tcp_noise = re.compile(
356 r"^(?:[(].+ Port \d+ Closed[)]|.+ starting on \d+)")334 r"^(?:[(].+ Port \d+ Closed[)]|.+ starting on \d+)")
357335
@@ -375,7 +353,7 @@
375353
376354
377_observe_twisted_internet_unix_noise = re.compile(355_observe_twisted_internet_unix_noise = re.compile(
378 r"^(?:[(]Port \d+ Closed[)]|.+ starting on .+)")356 r"^(?:[(]Port .+ Closed[)]|.+ starting on .+)")
379357
380358
381def observe_twisted_internet_unix(event):359def observe_twisted_internet_unix(event):
382360
=== modified file 'src/provisioningserver/logger/testing/__init__.py'
--- src/provisioningserver/logger/testing/__init__.py 2016-10-26 09:12:47 +0000
+++ src/provisioningserver/logger/testing/__init__.py 2016-11-29 12:38:39 +0000
@@ -4,6 +4,7 @@
4"""Testing helpers for `provisioningserver.logger`."""4"""Testing helpers for `provisioningserver.logger`."""
55
6__all__ = [6__all__ = [
7 "find_log_lines",
7 "make_event",8 "make_event",
8 "make_log_text",9 "make_log_text",
9 "pick_log_level",10 "pick_log_level",
@@ -11,20 +12,23 @@
11]12]
1213
13import random14import random
15import re
14import time16import time
1517
16from maastesting.factory import factory18from maastesting.factory import factory
17from twisted.logger import LogLevel19from twisted.logger import LogLevel
1820
1921
20def make_event(log_text=None, log_level=None, log_time=None):22def make_event(log_text=None, log_level=None, log_time=None, **other):
21 """Make a minimal event dict for use with Twisted."""23 """Make a minimal event dict for use with Twisted."""
22 return {24 event = {
23 "log_format": "{log_text}",25 "log_format": "{log_text}",
24 "log_level": pick_log_level() if log_level is None else log_level,26 "log_level": pick_log_level() if log_level is None else log_level,
25 "log_text": make_log_text() if log_text is None else log_text,27 "log_text": make_log_text() if log_text is None else log_text,
26 "log_time": pick_log_time() if log_time is None else log_time,28 "log_time": pick_log_time() if log_time is None else log_time,
27 }29 }
30 event.update(other)
31 return event
2832
2933
30def make_log_text():34def make_log_text():
@@ -43,3 +47,20 @@
43def pick_log_time(noise=float(60 * 60)):47def pick_log_time(noise=float(60 * 60)):
44 """Pick a random time based on now, but with some noise."""48 """Pick a random time based on now, but with some noise."""
45 return time.time() + (random.random() * noise) - (noise / 2)49 return time.time() + (random.random() * noise) - (noise / 2)
50
51
52# Matches lines like: 2016-10-18 14:23:55 namespace: [level] message
53find_log_lines_re = re.compile(
54 r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (.*?): [[](.*)[]] (.*)$",
55 re.MULTILINE)
56
57
58def find_log_lines(text):
59 """Find logs in `text` that match `find_log_lines_re`.
60
61 Checks for well-formed date/times but throws them away.
62 """
63 return [
64 (ns, level, line) for (ts, ns, level, line) in
65 find_log_lines_re.findall(text)
66 ]
4667
=== modified file 'src/provisioningserver/logger/testing/logsomething.py'
--- src/provisioningserver/logger/testing/logsomething.py 2016-11-04 00:11:06 +0000
+++ src/provisioningserver/logger/testing/logsomething.py 2016-11-29 12:38:39 +0000
@@ -46,9 +46,12 @@
46if options.set_verbosity is not None:46if options.set_verbosity is not None:
47 provisioningserver.logger.set_verbosity(options.set_verbosity)47 provisioningserver.logger.set_verbosity(options.set_verbosity)
4848
49# Simulate what `twistd` does when passed `--logfile=-`.49# Simulate what `twistd` does when passed `--logger=...` in Twisted 16.0. In
50# 16.4 something similar happens, but globalLogBeginner.beginLoggingTo() is
51# called without going via `twisted.python.log`.
50if options.mode == modes.TWISTD:52if options.mode == modes.TWISTD:
51 twisted.python.log.startLogging(sys.stdout)53 emitter = provisioningserver.logger.EventLogger()
54 twisted.python.log.startLoggingWithObserver(emitter)
5255
53# Twisted, new.56# Twisted, new.
54twisted.logger.Logger(options.name).debug("From `twisted.logger`.")57twisted.logger.Logger(options.name).debug("From `twisted.logger`.")
5558
=== modified file 'src/provisioningserver/logger/tests/test__twisted.py'
--- src/provisioningserver/logger/tests/test__twisted.py 2016-11-07 13:37:33 +0000
+++ src/provisioningserver/logger/tests/test__twisted.py 2016-11-29 12:38:39 +0000
@@ -5,31 +5,30 @@
55
6__all__ = []6__all__ = []
77
8from functools import partial
9import io8import io
10from unittest.mock import (
11 patch,
12 sentinel,
13)
149
15from maastesting.factory import factory10from maastesting.factory import factory
16from maastesting.matchers import DocTestMatches
17from maastesting.testcase import MAASTestCase11from maastesting.testcase import MAASTestCase
18from maastesting.twisted import TwistedLoggerFixture12from maastesting.twisted import TwistedLoggerFixture
13from provisioningserver.logger import _twisted
14from provisioningserver.logger._common import DEFAULT_LOG_FORMAT_DATE
19from provisioningserver.logger._twisted import (15from provisioningserver.logger._twisted import (
20 LegacyFileLogObserver,16 _formatModernEvent,
17 _getSystemName,
18 EventLogger,
21 LegacyLogger,19 LegacyLogger,
22 LegacyLogObserverWrapper,
23 observe_twisted_internet_tcp,20 observe_twisted_internet_tcp,
24 observe_twisted_internet_udp,21 observe_twisted_internet_udp,
25 observe_twisted_internet_unix,22 observe_twisted_internet_unix,
26)23)
27from provisioningserver.logger.testing import make_event24from provisioningserver.logger.testing import (
25 find_log_lines,
26 make_event,
27 pick_log_time,
28)
28from testtools.matchers import (29from testtools.matchers import (
29 AfterPreprocessing,30 AfterPreprocessing,
30 AllMatch,
31 Contains,31 Contains,
32 ContainsAll,
33 ContainsDict,32 ContainsDict,
34 Equals,33 Equals,
35 HasLength,34 HasLength,
@@ -37,11 +36,8 @@
37 IsInstance,36 IsInstance,
38 MatchesAll,37 MatchesAll,
39 MatchesDict,38 MatchesDict,
40 MatchesSetwise,
41 Not,
42)39)
43from twisted import logger40from twisted import logger
44from twisted.python import log
45from twisted.python.failure import Failure41from twisted.python.failure import Failure
4642
4743
@@ -50,243 +46,6 @@
50 {key: Equals(value) for key, value in expected.items()})46 {key: Equals(value) for key, value in expected.items()})
5147
5248
53def setLegacyObservers(observers):
54 """Remove existing legacy log observers, add those given."""
55 for observer in log.theLogPublisher.observers:
56 log.theLogPublisher.removeObserver(observer)
57 for observer in observers:
58 log.theLogPublisher.addObserver(observer)
59
60
61class TestLegacyFileLogObserver(MAASTestCase):
62 """Scenario tests for `LegacyFileLogObserver`."""
63
64 scenarios = tuple(
65 (log_level.name, dict(log_level=log_level))
66 for log_level in logger.LogLevel.iterconstants()
67 )
68
69 def test__namespace_and_level_is_printed_in_legacy_log(self):
70 # Restore existing observers at the end. This must be careful with
71 # ordering of clean-ups, hence the use of unittest.mock.patch.object
72 # as a context manager.
73 self.addCleanup(setLegacyObservers, log.theLogPublisher.observers)
74 # The global non-legacy `LogBeginner` emits critical messages straight
75 # to stderr, so temporarily put aside its observer to avoid seeing the
76 # critical log messages we're going to generate.
77 self.patch(logger.globalLogPublisher, "_observers", [])
78
79 logbuffer = io.StringIO()
80 observer = LegacyFileLogObserver(logbuffer)
81 observer.formatTime = lambda when: "<timestamp>"
82
83 oldlog = log.msg
84 # Deliberately use the default global observer in the new logger
85 # because we want to see how it behaves in a typical environment where
86 # logs are being emitted by the legacy logging infrastructure, for
87 # example running under `twistd`.
88 newlog = partial(logger.Logger().emit, self.log_level)
89
90 with patch.object(
91 log, "LegacyLogObserverWrapper",
92 log.LegacyLogObserverWrapper):
93 setLegacyObservers([observer.emit])
94 oldlog("Message from legacy", system="legacy")
95 newlog("Message from modern", log_system="modern")
96
97 self.assertThat(
98 logbuffer.getvalue(), DocTestMatches("""\
99 <timestamp> legacy: [info] Message from legacy
100 <timestamp> modern: [%s] Message from modern
101 """ % self.log_level.name))
102
103
104class TestLegacyFileLogObserver_Other(MAASTestCase):
105 """Other tests for `LegacyFileLogObserver`."""
106
107 def test__namespace_is_not_emitted_via_logfile_logerr(self):
108 # Restore existing observers at the end. This must be careful with
109 # ordering of clean-ups, hence the use of unittest.mock.patch.object
110 # as a context manager.
111 self.addCleanup(setLegacyObservers, log.theLogPublisher.observers)
112 # The global non-legacy `LogBeginner` emits critical messages straight
113 # to stderr, so temporarily put aside its observer to avoid seeing the
114 # critical log messages we're going to generate.
115 self.patch(logger.globalLogPublisher, "_observers", [])
116
117 logbuffer = io.StringIO()
118 observer = LegacyFileLogObserver(logbuffer)
119 observer.formatTime = lambda when: "<timestamp>"
120
121 with patch.object(
122 log, "LegacyLogObserverWrapper",
123 log.LegacyLogObserverWrapper):
124 setLegacyObservers([observer.emit])
125 log.logfile.write("Via log.logfile\n")
126 log.logerr.write("Via log.logerr\n")
127
128 self.assertThat(
129 logbuffer.getvalue(), DocTestMatches("""\
130 <timestamp> -: [info] Via log.logfile
131 <timestamp> -: [error] Via log.logerr
132 """))
133
134
135class TestLegacyLogObserverWrapper(MAASTestCase):
136 """Scenario tests for `LegacyLogObserverWrapper`."""
137
138 scenarios = tuple(
139 (log_level.name, dict(log_level=log_level))
140 for log_level in logger.LogLevel.iterconstants()
141 )
142
143 def processEvent(self, event):
144 events = []
145 observer = LegacyLogObserverWrapper(events.append)
146 observer(event)
147 self.assertThat(events, HasLength(1))
148 return events[0]
149
150 def test__adds_system_to_event(self):
151 self.assertThat(
152 # This is a `twisted.logger` event, not legacy, and requires
153 # values for `log_time` and `log_level` at a minimum.
154 self.processEvent({
155 "log_time": sentinel.log_time,
156 "log_level": self.log_level,
157 }),
158 MatchesAll(
159 Not(Contains("log_system")),
160 ContainsDictByEquality({"system": "-"}),
161 ),
162 )
163
164 def test__adds_log_system_and_system_to_event_with_namespace(self):
165 log_namespace = factory.make_name("log_namespace")
166 self.assertThat(
167 self.processEvent({
168 "log_time": sentinel.log_time,
169 "log_level": self.log_level,
170 "log_namespace": log_namespace,
171 }),
172 ContainsDictByEquality({
173 "log_system": log_namespace,
174 "system": log_namespace,
175 }),
176 )
177
178 def test__adds_system_to_legacy_event(self):
179 self.assertThat(
180 # This is a `twisted.python.log` event, i.e. legacy, and requires
181 # values for `time` and `isError` at a minimum.
182 self.processEvent({
183 "time": sentinel.time,
184 "isError": factory.pick_bool(),
185 }),
186 MatchesAll(
187 Not(Contains("log_system")),
188 ContainsDictByEquality({"system": "-"}),
189 ),
190 )
191
192 def test__preserves_log_system_in_event(self):
193 log_system = factory.make_name("log_system")
194 self.assertThat(
195 self.processEvent({
196 "log_time": sentinel.time,
197 "log_level": self.log_level,
198 "log_system": log_system,
199 }),
200 # `log_system` is not modified; `system` is set to match.
201 ContainsDictByEquality({
202 "log_system": log_system,
203 "system": log_system,
204 }),
205 )
206
207 def test__preserves_system_in_legacy_event(self):
208 system = factory.make_name("system")
209 self.assertThat(
210 self.processEvent({
211 "time": sentinel.time,
212 "isError": factory.pick_bool(),
213 "system": system,
214 }),
215 MatchesAll(
216 # `log_system` is not added when `system` already exists.
217 Not(Contains("log_system")),
218 ContainsDictByEquality({
219 "system": system,
220 }),
221 ),
222 )
223
224
225class TestLegacyLogObserverWrapper_Installation(MAASTestCase):
226 """Tests for `LegacyLogObserverWrapper`."""
227
228 def setUp(self):
229 super().setUp()
230 # Restore existing observers at the end. Tests must be careful with
231 # ordering of clean-ups, hence the use of unittest.mock.patch.object
232 # as a context manager in the tests themselves.
233 self.addCleanup(setLegacyObservers, log.theLogPublisher.observers)
234
235 def test__installs_wrapper_to_log_module(self):
236 with patch.object(log, "LegacyLogObserverWrapper", sentinel.unchanged):
237 self.assertThat(
238 log.LegacyLogObserverWrapper,
239 Is(sentinel.unchanged))
240 LegacyLogObserverWrapper.install()
241 self.assertThat(
242 log.LegacyLogObserverWrapper,
243 Is(LegacyLogObserverWrapper))
244
245 def test__rewraps_existing_observers(self):
246
247 class OldWrapper:
248
249 def __init__(self, observer):
250 self.legacyObserver = observer
251
252 def __call__(self, event):
253 return self.legacyObserver(event)
254
255 with patch.object(log, "LegacyLogObserverWrapper", OldWrapper):
256
257 observers = (lambda event: event), (lambda event: event)
258 setLegacyObservers(observers)
259
260 # Our legacy observers are all registered.
261 self.assertThat(
262 log.theLogPublisher.observers,
263 MatchesSetwise(*map(Is, observers)))
264 # Behind the scenes they're all wrapped with OldWrapper.
265 self.assertThat(
266 log.theLogPublisher._legacyObservers,
267 AllMatch(IsInstance(OldWrapper)))
268 # They're registered with the new global log publisher too.
269 self.assertThat(
270 logger.globalLogPublisher._observers,
271 ContainsAll(log.theLogPublisher._legacyObservers))
272
273 # Install!
274 LegacyLogObserverWrapper.install()
275
276 # Our legacy observers are all still registered.
277 self.assertThat(
278 log.theLogPublisher.observers,
279 MatchesSetwise(*map(Is, observers)))
280 # Behind the scenes they're now all wrapped with our wrapper.
281 self.assertThat(
282 log.theLogPublisher._legacyObservers,
283 AllMatch(IsInstance(LegacyLogObserverWrapper)))
284 # They're registered with the new global log publisher too.
285 self.assertThat(
286 logger.globalLogPublisher._observers,
287 ContainsAll(log.theLogPublisher._legacyObservers))
288
289
290def formatTimeStatic(when):49def formatTimeStatic(when):
291 """Just return <when>."""50 """Just return <when>."""
292 return "<when>"51 return "<when>"
@@ -443,7 +202,7 @@
443 """Tests for `observe_twisted_internet_unix`."""202 """Tests for `observe_twisted_internet_unix`."""
444203
445 def test__ignores_port_closed_events(self):204 def test__ignores_port_closed_events(self):
446 event = make_event("(Port %d Closed)" % factory.pick_port())205 event = make_event("(Port %r Closed)" % factory.make_name("port"))
447 with TwistedLoggerFixture() as logger:206 with TwistedLoggerFixture() as logger:
448 observe_twisted_internet_unix(event)207 observe_twisted_internet_unix(event)
449 self.assertThat(logger.events, HasLength(0))208 self.assertThat(logger.events, HasLength(0))
@@ -460,3 +219,193 @@
460 with TwistedLoggerFixture() as logger:219 with TwistedLoggerFixture() as logger:
461 observe_twisted_internet_unix(event)220 observe_twisted_internet_unix(event)
462 self.assertThat(logger.events, Contains(event))221 self.assertThat(logger.events, Contains(event))
222
223
224class TestGetSystemName(MAASTestCase):
225 """Tests for `_getSystemName`."""
226
227 expectations = {
228 "foo.bar.baz": "foo.bar.baz",
229 "f_o.bar.baz": "f_o.bar.baz",
230 "foo.b_r.baz": "foo.b_r.baz",
231 "foo.bar.b_z": "foo.bar.b_z",
232 "foo.bar._az": "foo.bar",
233 "foo._ar.baz": "foo",
234 "foo._ar._az": "foo",
235 "_oo.bar.baz": None,
236 "_": None,
237 "": None,
238 None: None,
239 }
240
241 scenarios = tuple(
242 ("%s => %s" % (
243 string_in or repr(string_in),
244 string_out or repr(string_out)),
245 {"string_in": string_in, "string_out": string_out})
246 for string_in, string_out in expectations.items()
247 )
248
249 def test(self):
250 self.assertThat(
251 _getSystemName(self.string_in),
252 Equals(self.string_out))
253
254
255class TestFormatModernEvent(MAASTestCase):
256 """Tests for `_formatModernEvent`."""
257
258 scenarios = tuple(
259 (level.name, {"log_level": level})
260 for level in logger.LogLevel.iterconstants()
261 )
262
263 def test_format_basics(self):
264 thing1 = factory.make_name("thing")
265 thing2 = factory.make_name("thing")
266 log_system = factory.make_name("system")
267 log_format = ">{thing1}< >{thing2}<"
268 log_time = pick_log_time()
269 self.assertThat(
270 _formatModernEvent({
271 "log_time": log_time,
272 "log_format": log_format,
273 "log_system": log_system,
274 "log_level": self.log_level,
275 "thing1": thing1,
276 "thing2": thing2,
277 }),
278 Equals(
279 "%s %s: [%s] >%s< >%s<\n" % (
280 logger.formatTime(log_time, DEFAULT_LOG_FORMAT_DATE),
281 log_system, self.log_level.name, thing1, thing2),
282 ),
283 )
284
285 def test_formats_without_format(self):
286 self.assertThat(
287 _formatModernEvent({
288 "log_level": self.log_level,
289 }),
290 Equals("- -: [%s] \n" % self.log_level.name),
291 )
292
293 def test_formats_with_null_format(self):
294 self.assertThat(
295 _formatModernEvent({
296 "log_format": None,
297 "log_level": self.log_level,
298 }),
299 Equals("- -: [%s] \n" % self.log_level.name),
300 )
301
302 def test_formats_without_time(self):
303 self.assertThat(
304 _formatModernEvent({
305 "log_level": self.log_level,
306 }),
307 Equals("- -: [%s] \n" % self.log_level.name),
308 )
309
310 def test_formats_with_null_time(self):
311 self.assertThat(
312 _formatModernEvent({
313 "log_time": None,
314 "log_level": self.log_level,
315 }),
316 Equals("- -: [%s] \n" % self.log_level.name),
317 )
318
319 def test_uses_namespace_if_system_missing(self):
320 log_namespace = factory.make_name("namespace")
321 self.assertThat(
322 _formatModernEvent({
323 "log_level": self.log_level,
324 "log_namespace": log_namespace,
325 }),
326 Equals(
327 "- %s: [%s] \n" % (
328 log_namespace, self.log_level.name),
329 ),
330 )
331
332 def test_uses_namespace_if_system_null(self):
333 log_namespace = factory.make_name("namespace")
334 self.assertThat(
335 _formatModernEvent({
336 "log_level": self.log_level,
337 "log_namespace": log_namespace,
338 "log_system": None,
339 }),
340 Equals(
341 "- %s: [%s] \n" % (
342 log_namespace, self.log_level.name),
343 ),
344 )
345
346
347class TestEventLogger(MAASTestCase):
348 """Tests for `EventLogger`."""
349
350 scenarios = tuple(
351 (level.name, {"log_level": level})
352 for level in logger.LogLevel.iterconstants()
353 )
354
355 def setUp(self):
356 super(TestEventLogger, self).setUp()
357 self.output = io.StringIO()
358 self.log = EventLogger(self.output)
359 self.get_logs = lambda: find_log_lines(self.output.getvalue())
360
361 def setLogLevel(self, log_level):
362 """Set the level at which events will be logged.
363
364 This is not a minimum level, it is an absolute level.
365 """
366 self.patch(_twisted, "_filterByLevels", {self.log_level})
367
368 def test_basics(self):
369 self.setLogLevel(self.log_level)
370 event = make_event(log_level=self.log_level)
371 event["log_system"] = factory.make_name("system")
372 self.log(event)
373 self.assertSequenceEqual(
374 [(event["log_system"], self.log_level.name, event["log_text"])],
375 self.get_logs(),
376 )
377
378 def test_filters_by_level(self):
379 self.setLogLevel(self.log_level)
380 events = {
381 log_level: make_event(log_level=log_level)
382 for log_level in logger.LogLevel.iterconstants()
383 }
384 for event in events.values():
385 self.log(event)
386 # Only the log at the current level will get through.
387 self.assertSequenceEqual(
388 [("-", self.log_level.name, events[self.log_level]["log_text"])],
389 self.get_logs(),
390 )
391
392 def test_filters_by_noise(self):
393 self.setLogLevel(self.log_level)
394 common = dict(log_namespace="log_legacy", log_system="-")
395 noisy = [
396 make_event("Log opened.", **common),
397 make_event("Main loop terminated.", **common),
398 ]
399 for event in noisy:
400 self.log(event)
401 okay = [
402 make_event(log_level=self.log_level, **common),
403 ]
404 for event in okay:
405 self.log(event)
406 # Only the `okay` logs will get through.
407 expected = [
408 ("-", self.log_level.name, event["log_text"])
409 for event in okay
410 ]
411 self.assertSequenceEqual(expected, self.get_logs())
463412
=== modified file 'src/provisioningserver/logger/tests/test_logger.py'
--- src/provisioningserver/logger/tests/test_logger.py 2016-11-07 13:37:33 +0000
+++ src/provisioningserver/logger/tests/test_logger.py 2016-11-29 12:38:39 +0000
@@ -6,7 +6,6 @@
6__all__ = []6__all__ = []
77
8import pathlib8import pathlib
9import re
10import subprocess9import subprocess
11import sys10import sys
1211
@@ -14,6 +13,7 @@
14from maastesting.matchers import DocTestMatches13from maastesting.matchers import DocTestMatches
15from maastesting.testcase import MAASTestCase14from maastesting.testcase import MAASTestCase
16from provisioningserver.logger import LoggingMode15from provisioningserver.logger import LoggingMode
16from provisioningserver.logger.testing import find_log_lines
17from provisioningserver.utils import typed17from provisioningserver.utils import typed
18from provisioningserver.utils.shell import select_c_utf8_locale18from provisioningserver.utils.shell import select_c_utf8_locale
19from testtools.content import text_content19from testtools.content import text_content
@@ -40,23 +40,6 @@
40 return output.decode("utf-8")40 return output.decode("utf-8")
4141
4242
43# Matches lines like: 2016-10-18 14:23:55 [namespace#level] message
44find_log_lines_re = re.compile(
45 r"^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (.*?): [[](.*)[]] (.*)$",
46 re.MULTILINE)
47
48
49def find_log_lines(text):
50 """Find logs in `text` that match `find_log_lines_re`.
51
52 Checks for well-formed date/times but throws them away.
53 """
54 return [
55 (ns, level, line) for (ts, ns, level, line) in
56 find_log_lines_re.findall(text)
57 ]
58
59
60class TestLogging(MAASTestCase):43class TestLogging(MAASTestCase):
61 """Test logging in MAAS as configured by `p.logger.configure`.44 """Test logging in MAAS as configured by `p.logger.configure`.
6245
@@ -115,7 +98,7 @@
115 ('maas.' + name, 'error', 'From `get_maas_logger`.'),98 ('maas.' + name, 'error', 'From `get_maas_logger`.'),
116 ('stdout', 'info', 'Printing to stdout.'),99 ('stdout', 'info', 'Printing to stdout.'),
117 ('stderr', 'error', 'Printing to stderr.'),100 ('stderr', 'error', 'Printing to stderr.'),
118 ('__main__', 'warn', 'UserWarning: This is a warning!'),101 ('-', 'warn', 'UserWarning: This is a warning!'),
119 ]102 ]
120 self.assertSequenceEqual(expected, observed)103 self.assertSequenceEqual(expected, observed)
121104
@@ -143,7 +126,7 @@
143 ('maas.' + name, 'error', 'From `get_maas_logger`.'),126 ('maas.' + name, 'error', 'From `get_maas_logger`.'),
144 ('stdout', 'info', 'Printing to stdout.'),127 ('stdout', 'info', 'Printing to stdout.'),
145 ('stderr', 'error', 'Printing to stderr.'),128 ('stderr', 'error', 'Printing to stderr.'),
146 ('__main__', 'warn', 'UserWarning: This is a warning!'),129 ('-', 'warn', 'UserWarning: This is a warning!'),
147 ]130 ]
148 self.assertSequenceEqual(expected, observed)131 self.assertSequenceEqual(expected, observed)
149132
@@ -163,7 +146,7 @@
163 ('maas.' + name, 'warn', 'From `get_maas_logger`.'),146 ('maas.' + name, 'warn', 'From `get_maas_logger`.'),
164 ('maas.' + name, 'error', 'From `get_maas_logger`.'),147 ('maas.' + name, 'error', 'From `get_maas_logger`.'),
165 ('stderr', 'error', 'Printing to stderr.'),148 ('stderr', 'error', 'Printing to stderr.'),
166 ('__main__', 'warn', 'UserWarning: This is a warning!'),149 ('-', 'warn', 'UserWarning: This is a warning!'),
167 ]150 ]
168 self.assertSequenceEqual(expected, observed)151 self.assertSequenceEqual(expected, observed)
169152

Subscribers

People subscribed via source and target branches

to all changes: