|
1 # This Source Code Form is subject to the terms of the Mozilla Public |
|
2 # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 # You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 import datetime |
|
6 import json |
|
7 import socket |
|
8 import threading |
|
9 import time |
|
10 import unittest |
|
11 |
|
12 import mozfile |
|
13 |
|
14 import mozlog |
|
15 |
|
16 class ListHandler(mozlog.Handler): |
|
17 """Mock handler appends messages to a list for later inspection.""" |
|
18 |
|
19 def __init__(self): |
|
20 mozlog.Handler.__init__(self) |
|
21 self.messages = [] |
|
22 |
|
23 def emit(self, record): |
|
24 self.messages.append(self.format(record)) |
|
25 |
|
26 class TestLogging(unittest.TestCase): |
|
27 """Tests behavior of basic mozlog api.""" |
|
28 |
|
29 def test_logger_defaults(self): |
|
30 """Tests the default logging format and behavior.""" |
|
31 |
|
32 default_logger = mozlog.getLogger('default.logger') |
|
33 self.assertEqual(default_logger.name, 'default.logger') |
|
34 self.assertEqual(len(default_logger.handlers), 1) |
|
35 self.assertTrue(isinstance(default_logger.handlers[0], |
|
36 mozlog.StreamHandler)) |
|
37 |
|
38 f = mozfile.NamedTemporaryFile() |
|
39 list_logger = mozlog.getLogger('file.logger', |
|
40 handler=mozlog.FileHandler(f.name)) |
|
41 self.assertEqual(len(list_logger.handlers), 1) |
|
42 self.assertTrue(isinstance(list_logger.handlers[0], |
|
43 mozlog.FileHandler)) |
|
44 f.close() |
|
45 |
|
46 self.assertRaises(ValueError, mozlog.getLogger, |
|
47 'file.logger', handler=ListHandler()) |
|
48 |
|
49 def test_timestamps(self): |
|
50 """Verifies that timestamps are included when asked for.""" |
|
51 log_name = 'test' |
|
52 handler = ListHandler() |
|
53 handler.setFormatter(mozlog.MozFormatter()) |
|
54 log = mozlog.getLogger(log_name, handler=handler) |
|
55 log.info('no timestamp') |
|
56 self.assertTrue(handler.messages[-1].startswith('%s ' % log_name)) |
|
57 handler.setFormatter(mozlog.MozFormatter(include_timestamp=True)) |
|
58 log.info('timestamp') |
|
59 # Just verify that this raises no exceptions. |
|
60 datetime.datetime.strptime(handler.messages[-1][:23], |
|
61 '%Y-%m-%d %H:%M:%S,%f') |
|
62 |
|
63 class TestStructuredLogging(unittest.TestCase): |
|
64 """Tests structured output in mozlog.""" |
|
65 |
|
66 def setUp(self): |
|
67 self.handler = ListHandler() |
|
68 self.handler.setFormatter(mozlog.JSONFormatter()) |
|
69 self.logger = mozlog.MozLogger('test.Logger') |
|
70 self.logger.addHandler(self.handler) |
|
71 self.logger.setLevel(mozlog.DEBUG) |
|
72 |
|
73 def check_messages(self, expected, actual): |
|
74 """Checks actual for equality with corresponding fields in actual. |
|
75 The actual message should contain all fields in expected, and |
|
76 should be identical, with the exception of the timestamp field. |
|
77 The actual message should contain no fields other than the timestamp |
|
78 field and those present in expected.""" |
|
79 |
|
80 self.assertTrue(isinstance(actual['_time'], (int, long))) |
|
81 |
|
82 for k, v in expected.items(): |
|
83 self.assertEqual(v, actual[k]) |
|
84 |
|
85 for k in actual.keys(): |
|
86 if k != '_time': |
|
87 self.assertTrue(expected.get(k) is not None) |
|
88 |
|
89 def test_structured_output(self): |
|
90 self.logger.log_structured('test_message', |
|
91 {'_level': mozlog.INFO, |
|
92 '_message': 'message one'}) |
|
93 self.logger.log_structured('test_message', |
|
94 {'_level': mozlog.INFO, |
|
95 '_message': 'message two'}) |
|
96 self.logger.log_structured('error_message', |
|
97 {'_level': mozlog.ERROR, |
|
98 'diagnostic': 'unexpected error'}) |
|
99 |
|
100 message_one_expected = {'_namespace': 'test.Logger', |
|
101 '_level': 'INFO', |
|
102 '_message': 'message one', |
|
103 'action': 'test_message'} |
|
104 message_two_expected = {'_namespace': 'test.Logger', |
|
105 '_level': 'INFO', |
|
106 '_message': 'message two', |
|
107 'action': 'test_message'} |
|
108 message_three_expected = {'_namespace': 'test.Logger', |
|
109 '_level': 'ERROR', |
|
110 'diagnostic': 'unexpected error', |
|
111 'action': 'error_message'} |
|
112 |
|
113 message_one_actual = json.loads(self.handler.messages[0]) |
|
114 message_two_actual = json.loads(self.handler.messages[1]) |
|
115 message_three_actual = json.loads(self.handler.messages[2]) |
|
116 |
|
117 self.check_messages(message_one_expected, message_one_actual) |
|
118 self.check_messages(message_two_expected, message_two_actual) |
|
119 self.check_messages(message_three_expected, message_three_actual) |
|
120 |
|
121 def test_unstructured_conversion(self): |
|
122 """ Tests that logging to a logger with a structured formatter |
|
123 via the traditional logging interface works as expected. """ |
|
124 self.logger.info('%s %s %d', 'Message', 'number', 1) |
|
125 self.logger.error('Message number 2') |
|
126 self.logger.debug('Message with %s', 'some extras', |
|
127 extra={'params': {'action': 'mozlog_test_output', |
|
128 'is_failure': False}}) |
|
129 message_one_expected = {'_namespace': 'test.Logger', |
|
130 '_level': 'INFO', |
|
131 '_message': 'Message number 1'} |
|
132 message_two_expected = {'_namespace': 'test.Logger', |
|
133 '_level': 'ERROR', |
|
134 '_message': 'Message number 2'} |
|
135 message_three_expected = {'_namespace': 'test.Logger', |
|
136 '_level': 'DEBUG', |
|
137 '_message': 'Message with some extras', |
|
138 'action': 'mozlog_test_output', |
|
139 'is_failure': False} |
|
140 |
|
141 message_one_actual = json.loads(self.handler.messages[0]) |
|
142 message_two_actual = json.loads(self.handler.messages[1]) |
|
143 message_three_actual = json.loads(self.handler.messages[2]) |
|
144 |
|
145 self.check_messages(message_one_expected, message_one_actual) |
|
146 self.check_messages(message_two_expected, message_two_actual) |
|
147 self.check_messages(message_three_expected, message_three_actual) |
|
148 |
|
149 def message_callback(self): |
|
150 if len(self.handler.messages) == 3: |
|
151 message_one_expected = {'_namespace': 'test.Logger', |
|
152 '_level': 'DEBUG', |
|
153 '_message': 'socket message one', |
|
154 'action': 'test_message'} |
|
155 message_two_expected = {'_namespace': 'test.Logger', |
|
156 '_level': 'DEBUG', |
|
157 '_message': 'socket message two', |
|
158 'action': 'test_message'} |
|
159 message_three_expected = {'_namespace': 'test.Logger', |
|
160 '_level': 'DEBUG', |
|
161 '_message': 'socket message three', |
|
162 'action': 'test_message'} |
|
163 |
|
164 message_one_actual = json.loads(self.handler.messages[0]) |
|
165 |
|
166 message_two_actual = json.loads(self.handler.messages[1]) |
|
167 |
|
168 message_three_actual = json.loads(self.handler.messages[2]) |
|
169 |
|
170 self.check_messages(message_one_expected, message_one_actual) |
|
171 self.check_messages(message_two_expected, message_two_actual) |
|
172 self.check_messages(message_three_expected, message_three_actual) |
|
173 |
|
174 def test_log_listener(self): |
|
175 connection = '127.0.0.1', 0 |
|
176 self.log_server = mozlog.LogMessageServer(connection, |
|
177 self.logger, |
|
178 message_callback=self.message_callback, |
|
179 timeout=0.5) |
|
180 |
|
181 message_string_one = json.dumps({'_message': 'socket message one', |
|
182 'action': 'test_message', |
|
183 '_level': 'DEBUG'}) |
|
184 message_string_two = json.dumps({'_message': 'socket message two', |
|
185 'action': 'test_message', |
|
186 '_level': 'DEBUG'}) |
|
187 |
|
188 message_string_three = json.dumps({'_message': 'socket message three', |
|
189 'action': 'test_message', |
|
190 '_level': 'DEBUG'}) |
|
191 |
|
192 message_string = message_string_one + '\n' + \ |
|
193 message_string_two + '\n' + \ |
|
194 message_string_three + '\n' |
|
195 |
|
196 server_thread = threading.Thread(target=self.log_server.handle_request) |
|
197 server_thread.start() |
|
198 |
|
199 host, port = self.log_server.server_address |
|
200 |
|
201 sock = socket.socket() |
|
202 sock.connect((host, port)) |
|
203 |
|
204 # Sleeps prevent listener from receiving entire message in a single call |
|
205 # to recv in order to test reconstruction of partial messages. |
|
206 sock.sendall(message_string[:8]) |
|
207 time.sleep(.01) |
|
208 sock.sendall(message_string[8:32]) |
|
209 time.sleep(.01) |
|
210 sock.sendall(message_string[32:64]) |
|
211 time.sleep(.01) |
|
212 sock.sendall(message_string[64:128]) |
|
213 time.sleep(.01) |
|
214 sock.sendall(message_string[128:]) |
|
215 |
|
216 server_thread.join() |
|
217 |
|
218 class Loggable(mozlog.LoggingMixin): |
|
219 """Trivial class inheriting from LoggingMixin""" |
|
220 pass |
|
221 |
|
222 class TestLoggingMixin(unittest.TestCase): |
|
223 """Tests basic use of LoggingMixin""" |
|
224 |
|
225 def test_mixin(self): |
|
226 loggable = Loggable() |
|
227 self.assertTrue(not hasattr(loggable, "_logger")) |
|
228 loggable.log(mozlog.INFO, "This will instantiate the logger") |
|
229 self.assertTrue(hasattr(loggable, "_logger")) |
|
230 self.assertEqual(loggable._logger.name, "test_logger.Loggable") |
|
231 |
|
232 self.assertRaises(ValueError, loggable.set_logger, |
|
233 "not a logger") |
|
234 |
|
235 logger = mozlog.MozLogger('test.mixin') |
|
236 handler = ListHandler() |
|
237 logger.addHandler(handler) |
|
238 loggable.set_logger(logger) |
|
239 self.assertTrue(isinstance(loggable._logger.handlers[0], |
|
240 ListHandler)) |
|
241 self.assertEqual(loggable._logger.name, "test.mixin") |
|
242 |
|
243 loggable.log(mozlog.WARN, 'message for "log" method') |
|
244 loggable.info('message for "info" method') |
|
245 loggable.error('message for "error" method') |
|
246 loggable.log_structured('test_message', |
|
247 params={'_message': 'message for ' + \ |
|
248 '"log_structured" method'}) |
|
249 |
|
250 expected_messages = ['message for "log" method', |
|
251 'message for "info" method', |
|
252 'message for "error" method', |
|
253 'message for "log_structured" method'] |
|
254 |
|
255 actual_messages = loggable._logger.handlers[0].messages |
|
256 self.assertEqual(expected_messages, actual_messages) |
|
257 |
|
258 if __name__ == '__main__': |
|
259 unittest.main() |