Merge lp:~thomir-deactivatedaccount/uservice-utils/trunk-add-logging-decorator into lp:uservice-utils

Proposed by Thomi Richards
Status: Merged
Approved by: Thomi Richards
Approved revision: 7
Merged at revision: 6
Proposed branch: lp:~thomir-deactivatedaccount/uservice-utils/trunk-add-logging-decorator
Merge into: lp:uservice-utils
Diff against target: 201 lines (+135/-0)
4 files modified
CHANGELOG (+6/-0)
setup.py (+1/-0)
uservice_utils/logging.py (+60/-0)
uservice_utils/tests/test_logging.py (+68/-0)
To merge this branch: bzr merge lp:~thomir-deactivatedaccount/uservice-utils/trunk-add-logging-decorator
Reviewer Review Type Date Requested Status
Celso Providelo (community) Approve
Review via email: mp+255029@code.launchpad.net

Commit message

Add the ExtraLogger class.

Description of the change

Add the ExtraLogger class, which helps us by not having to pass 'extra=extra' everywhere.

To post a comment you must log in.
Revision history for this message
Joe Talbott (joetalbott) wrote :

Some in-line comments. Everything appears functionally correct.

Revision history for this message
Celso Providelo (cprov) wrote :

Thomi,

It looks very good!

As discussed on IRC, most of our workers use-case will involve calling set_extra_args() in the beginning of the initialisation with the 'default' extra logging keys (solution, service and hostname) and the call set_extra_args() again with the received message payload repeatedly on 'worker.on_message_cb'.

Since set_extra_args() overrides the logging "extra" dict, it won't erroneously mask invalid/incomplete payloads, in contrast to logger.info('', extra=payload), the latter should be blocked on reviews.

review: Approve
7. By Thomi Richards

Fix comments from code review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CHANGELOG'
2--- CHANGELOG 2015-04-01 20:40:20 +0000
3+++ CHANGELOG 2015-04-02 03:03:13 +0000
4@@ -1,6 +1,12 @@
5 Release History:
6 ################
7
8+NEXT
9+====
10+
11+* Added `uservice_utils.logging.ExtraLogging` class to help manage passing
12+ 'extra' arguments to multiple logging calls.
13+
14 1.0.0
15 =====
16
17
18=== modified file 'setup.py'
19--- setup.py 2015-04-01 22:03:21 +0000
20+++ setup.py 2015-04-02 03:03:13 +0000
21@@ -49,6 +49,7 @@
22 packages=find_packages(),
23 test_suite='uservice_utils.tests',
24 tests_require=[
25+ 'fixtures',
26 'testtools',
27 ],
28 classifiers=[
29
30=== modified file 'uservice_utils/logging.py'
31--- uservice_utils/logging.py 2015-04-01 01:30:30 +0000
32+++ uservice_utils/logging.py 2015-04-02 03:03:13 +0000
33@@ -17,6 +17,7 @@
34
35 """Code to configure logging for micro-services."""
36
37+import collections
38 import logging
39 import logging.handlers
40 import os
41@@ -78,3 +79,62 @@
42 int(config['logstash']['version'])
43 )
44 )
45+
46+
47+class ExtraLogger(logging.Logger):
48+
49+ """A logger that handles passing 'extra' arguments to all logging calls.
50+
51+ Tired of having to write:
52+
53+ logger.info("Some message", extra=extra)
54+
55+ ... everywhere?
56+
57+ Now you can install this class as the Logger class:
58+
59+ >>> import logging
60+ >>> logging.setLoggerClass(ExtraLogger)
61+
62+ Then, to use it, get your logger as usual:
63+
64+ >>> logger = logging.getLogger(__name__)
65+
66+ ...and set your extra arguments once only:
67+
68+ >>> logger.set_extra_args(dict(foo='bar', baz='123'))
69+
70+ And those arguments will be included in all normal log messages:
71+
72+ >>> logger.info("Hello World") # message will contain extra args set above
73+
74+ Extra arguments can be passed to individual logging calls, and will take
75+ priority over any set with the set_extra_args call.
76+
77+ """
78+
79+ def __init__(self, *args, **kwargs):
80+ super().__init__(*args, **kwargs)
81+ self._extra_args = dict()
82+
83+ def set_extra_args(self, extra_args):
84+ """Set the 'extra' arguments you want to be passed to every log message.
85+
86+ :param extra_args: A mapping type that contains the extra arguments you
87+ want to store.
88+ :raises TypeError: if extra_args is not a mapping type.
89+
90+ """
91+ if not isinstance(extra_args, collections.Mapping):
92+ raise TypeError("extra_args must be a mapping")
93+ self._extra_args = extra_args
94+
95+ def makeRecord(self, name, level, fn, lno, msg, args, exc_info,
96+ func=None, user_extra=None, sinfo=None):
97+ extra = self._extra_args.copy()
98+ if user_extra:
99+ extra.update(user_extra)
100+ return super().makeRecord(name, level, fn, lno, msg, args, exc_info,
101+ func, extra, sinfo)
102+
103+
104
105=== modified file 'uservice_utils/tests/test_logging.py'
106--- uservice_utils/tests/test_logging.py 2015-04-01 20:21:23 +0000
107+++ uservice_utils/tests/test_logging.py 2015-04-02 03:03:13 +0000
108@@ -17,21 +17,26 @@
109
110 """Tests for the logging code."""
111
112+import logging
113 import os.path
114 import subprocess
115 import sys
116 import tempfile
117 from textwrap import dedent
118
119+from fixtures import Fixture
120 from testtools import TestCase
121 from testtools.matchers import (
122 Contains,
123 Equals,
124 FileContains,
125+ IsInstance,
126 Not,
127 )
128
129 import uservice_utils
130+import uservice_utils.logging as util_logging
131+
132
133 class LoggingConfigurationTests(TestCase):
134
135@@ -119,3 +124,66 @@
136 self.expectThat(err, Not(Contains("Will not see")))
137 self.expectThat(err, Contains("Will see"))
138
139+
140+class LoggerClassFixture(Fixture):
141+
142+ """A fixture that sets a new logging class for the duration of a test."""
143+
144+ def __init__(self, new_class):
145+ self._new_class = new_class
146+
147+ def setUp(self):
148+ super().setUp()
149+ old_logger_class = logging.getLoggerClass()
150+ logging.setLoggerClass(self._new_class)
151+ self.addCleanup(logging.setLoggerClass, old_logger_class)
152+
153+
154+class TestingLogFilter(logging.Filter):
155+
156+ """A filter that passes everything, but logs everything."""
157+ def __init__(self):
158+ self.log_records = []
159+
160+ def filter(self, record):
161+ self.log_records.append(record)
162+ return 1 # Log this record.
163+
164+
165+class ExtraLoggerTests(TestCase):
166+
167+ def test_can_set_logger_class(self):
168+ self.useFixture(LoggerClassFixture(util_logging.ExtraLogger))
169+ logger = logging.getLogger(__name__)
170+ self.assertThat(logger, IsInstance(util_logging.ExtraLogger))
171+
172+ def create_log_with_filter(self):
173+ self.useFixture(LoggerClassFixture(util_logging.ExtraLogger))
174+ logger = logging.getLogger(__name__)
175+ logger.setLevel(logging.INFO)
176+ filt = TestingLogFilter()
177+ logger.addFilter(filt)
178+ self.addCleanup(logger.removeFilter, filt)
179+ return logger, filt
180+
181+ def test_can_set_extra_details(self):
182+ logger, filt = self.create_log_with_filter()
183+ logger.set_extra_args(dict(foo='bar'))
184+ logger.info("Testing")
185+
186+ self.assertThat(filt.log_records[0].foo, Equals('bar'))
187+
188+ def test_extra_args_can_be_mixed(self):
189+ logger, filt = self.create_log_with_filter()
190+ logger.set_extra_args(dict(foo='bar'))
191+ logger.info("Testing", extra=dict(bar='baz'))
192+
193+ self.assertThat(filt.log_records[0].foo, Equals('bar'))
194+ self.assertThat(filt.log_records[0].bar, Equals('baz'))
195+
196+ def test_log_method_extra_args_take_priority(self):
197+ logger, filt = self.create_log_with_filter()
198+ logger.set_extra_args(dict(foo='bar'))
199+ logger.info("Testing", extra=dict(foo='baz'))
200+
201+ self.assertThat(filt.log_records[0].foo, Equals('baz'))

Subscribers

People subscribed via source and target branches

to all changes: