risclog.logging 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,414 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ import os
5
+ import smtplib
6
+ import sys
7
+ import traceback
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from email.mime.multipart import MIMEMultipart
10
+ from email.mime.text import MIMEText
11
+ from functools import partial, wraps
12
+ from pathlib import Path
13
+ from typing import Coroutine
14
+
15
+ import structlog
16
+ from structlog.types import Processor
17
+
18
+
19
+ def rename_event_to_message(_, __, event_dict):
20
+ if 'event' in event_dict:
21
+ event_dict['message'] = event_dict.pop('event')
22
+ keys_at_end = ['referer']
23
+ sorted_keys = sorted(
24
+ event_dict.keys(), key=lambda x: (x in keys_at_end, x)
25
+ )
26
+ sorted_dict = {k: event_dict[k] for k in sorted_keys}
27
+ return sorted_dict
28
+
29
+
30
+ class RiscLoggerSingletonMeta(type):
31
+ _instances = {}
32
+
33
+ def __call__(cls, *args, **kwargs):
34
+ if cls not in cls._instances:
35
+ cls._instances[cls] = super(RiscLoggerSingletonMeta, cls).__call__(
36
+ *args, **kwargs
37
+ )
38
+
39
+ return cls._instances[cls]
40
+
41
+
42
+ class RiscLogger(metaclass=RiscLoggerSingletonMeta):
43
+ def __init__(self, name: str = None) -> None:
44
+ self.logger = structlog.stdlib.get_logger(name)
45
+ self.logger_name = name
46
+
47
+ def __new__(cls, *args, **kwargs):
48
+ cls._configure_logger()
49
+ instance = super().__new__(cls)
50
+
51
+ return instance
52
+
53
+ @classmethod
54
+ def _configure_logger(cls):
55
+ LEVELS = {
56
+ 'CRITICAL': 50,
57
+ 'FATAL': 50,
58
+ 'ERROR': 40,
59
+ 'WARNING': 30,
60
+ 'WARN': 30,
61
+ 'INFO': 20,
62
+ 'DEBUG': 10,
63
+ }
64
+
65
+ log_level = LEVELS.get(os.getenv('LOG_LEVEL'), 20)
66
+
67
+ timestamper = structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S')
68
+ shared_processors: list[Processor] = [
69
+ structlog.contextvars.merge_contextvars,
70
+ structlog.stdlib.add_logger_name,
71
+ structlog.stdlib.add_log_level,
72
+ structlog.stdlib.PositionalArgumentsFormatter(),
73
+ structlog.stdlib.ExtraAdder(),
74
+ timestamper,
75
+ structlog.processors.StackInfoRenderer(),
76
+ rename_event_to_message,
77
+ ]
78
+
79
+ structlog.configure(
80
+ logger_factory=structlog.stdlib.LoggerFactory(),
81
+ processors=shared_processors
82
+ + [
83
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
84
+ ],
85
+ wrapper_class=structlog.stdlib.BoundLogger,
86
+ cache_logger_on_first_use=True,
87
+ )
88
+
89
+ log_renderer: structlog.types.Processor
90
+ log_renderer = structlog.dev.ConsoleRenderer()
91
+ formatter = structlog.stdlib.ProcessorFormatter(
92
+ foreign_pre_chain=shared_processors,
93
+ processors=[
94
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
95
+ log_renderer,
96
+ ],
97
+ )
98
+ # set logger Level from asyncio package to WARNING
99
+ logging.getLogger('asyncio').setLevel(logging.WARNING)
100
+ handler = logging.StreamHandler()
101
+ handler.setFormatter(formatter)
102
+ root_logger = logging.getLogger()
103
+ root_logger.addHandler(handler)
104
+ root_logger.setLevel(log_level)
105
+
106
+ for _log in ['uvicorn', 'uvicorn.error']:
107
+ logging.getLogger(_log).handlers.clear()
108
+ logging.getLogger(_log).propagate = True
109
+
110
+ logging.getLogger('uvicorn.access').handlers.clear()
111
+ logging.getLogger('uvicorn.access').propagate = False
112
+
113
+ def handle_exception(exc_type, exc_value, exc_traceback):
114
+ if issubclass(exc_type, KeyboardInterrupt):
115
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
116
+ return False
117
+
118
+ sys.excepthook = handle_exception
119
+
120
+ async def _async_log(
121
+ self,
122
+ level: str,
123
+ msg: str,
124
+ sender: str,
125
+ function_id: int,
126
+ *args,
127
+ **kwargs,
128
+ ) -> Coroutine:
129
+ await asyncio.sleep(0)
130
+ func = getattr(self.logger, level.lower())
131
+ sender = kwargs.get('sender') if kwargs.get('sender') else sender
132
+ kwargs = {**{'__id': function_id, '__sender': sender}, **kwargs}
133
+ func(msg, *args, **kwargs)
134
+
135
+ def _log(
136
+ self,
137
+ level: str,
138
+ msg: str,
139
+ function_id: int,
140
+ sender: str = 'inline',
141
+ *args,
142
+ **kwargs,
143
+ ) -> Coroutine:
144
+ try:
145
+ loop = asyncio.get_running_loop()
146
+ except RuntimeError:
147
+ loop = None
148
+
149
+ if loop and loop.is_running():
150
+ return self._async_log(
151
+ level=level,
152
+ msg=msg,
153
+ sender=sender,
154
+ function_id=function_id,
155
+ *args,
156
+ **kwargs,
157
+ )
158
+ else:
159
+ return asyncio.run(
160
+ self._async_log(
161
+ level=level,
162
+ msg=msg,
163
+ sender=sender,
164
+ function_id=function_id,
165
+ *args,
166
+ **kwargs,
167
+ )
168
+ )
169
+
170
+ @staticmethod
171
+ def set_caller_id(func):
172
+ def wrapper(*args, **kwargs):
173
+ if 'function_id' not in kwargs:
174
+ caller_frame = inspect.stack()[1]
175
+ caller_name = caller_frame.function
176
+ function_id = id(caller_name)
177
+ kwargs['function_id'] = function_id
178
+
179
+ return func(*args, **kwargs)
180
+
181
+ return wrapper
182
+
183
+ @set_caller_id
184
+ def debug(
185
+ self, msg: str = None, function_id: int = None, *args, **kwargs
186
+ ) -> Coroutine:
187
+ return self._log(
188
+ 'debug', msg, function_id=function_id, *args, **kwargs
189
+ )
190
+
191
+ @set_caller_id
192
+ def info(
193
+ self, msg: str = None, function_id: int = None, *args, **kwargs
194
+ ) -> Coroutine:
195
+ return self._log('info', msg, function_id=function_id, *args, **kwargs)
196
+
197
+ @set_caller_id
198
+ def warning(
199
+ self, msg: str = None, function_id: int = None, *args, **kwargs
200
+ ) -> Coroutine:
201
+ return self._log(
202
+ 'warning', msg, function_id=function_id, *args, **kwargs
203
+ )
204
+
205
+ @set_caller_id
206
+ def fatal(
207
+ self, msg: str = None, function_id: int = None, *args, **kwargs
208
+ ) -> Coroutine:
209
+ return self._log(
210
+ 'fatal', msg, function_id=function_id, *args, **kwargs
211
+ )
212
+
213
+ @set_caller_id
214
+ def critical(
215
+ self, msg: str = None, function_id: int = None, *args, **kwargs
216
+ ) -> Coroutine:
217
+ return self._log(
218
+ 'critical', msg, function_id=function_id, *args, **kwargs
219
+ )
220
+
221
+ @set_caller_id
222
+ def exception(
223
+ self, msg: str = None, function_id: int = None, *args, **kwargs
224
+ ) -> Coroutine:
225
+ return self._log(
226
+ 'error', msg, function_id=function_id, *args, **kwargs
227
+ )
228
+
229
+ @set_caller_id
230
+ def error(
231
+ self, msg: str = None, function_id: int = None, *args, **kwargs
232
+ ) -> Coroutine:
233
+ return self._log(
234
+ 'error', msg, function_id=function_id, *args, **kwargs
235
+ )
236
+
237
+ @classmethod
238
+ def decorator(cls, method=None, send_email=False):
239
+ if method is None:
240
+ return lambda m: cls.decorator(m, send_email)
241
+
242
+ function_id = id(method)
243
+ if not cls._instances:
244
+ logger = cls(name=__name__)
245
+ else:
246
+ logger = cls._instances[cls]
247
+
248
+ if inspect.iscoroutinefunction(method):
249
+
250
+ @wraps(method)
251
+ async def async_wrapper(*args, **kwargs):
252
+ try:
253
+ script = Path(inspect.getfile(method)).name
254
+ structlog.contextvars.bind_contextvars(
255
+ _function=method.__name__,
256
+ _script=script,
257
+ )
258
+
259
+ frame = inspect.currentframe()
260
+ _, _, _, frame_values = inspect.getargvalues(frame)
261
+ args_dict = {f'arg_{i}': arg for i, arg in enumerate(args)}
262
+ params = {**args_dict, **frame_values.get('kwargs')}
263
+
264
+ if params:
265
+ await logger.info(
266
+ f'Method called: "{method.__name__}" with: "{params}"',
267
+ function_id=function_id,
268
+ sender='async_logging_decorator',
269
+ )
270
+ else:
271
+ await logger.info(
272
+ f'Method "{method.__name__}" called with no arguments.',
273
+ sender='async_logging_decorator',
274
+ function_id=function_id,
275
+ )
276
+
277
+ value = await method(*args, **kwargs)
278
+ await logger.info(
279
+ f'Method "{method.__name__}" returned: "{value}"',
280
+ sender='async_logging_decorator',
281
+ function_id=function_id,
282
+ )
283
+ return value
284
+ except Exception as exc:
285
+ message = f'Exception occurred in method: {method.__name__}, exception: {exc}'
286
+ if send_email:
287
+ with ThreadPoolExecutor() as executor:
288
+ message = f'{message}\n\n\n{exception_to_string(excp=exc)}'
289
+ executor.submit(
290
+ partial(
291
+ smtp_email_send,
292
+ message=message,
293
+ logger_name=logger.logger_name,
294
+ )
295
+ )
296
+ await logger.exception(
297
+ message,
298
+ sender='async_logging_decorator',
299
+ function_id=function_id,
300
+ )
301
+ raise exc
302
+ finally:
303
+ structlog.contextvars.unbind_contextvars(
304
+ '_function', '_script'
305
+ )
306
+
307
+ return async_wrapper
308
+ else:
309
+
310
+ @wraps(method)
311
+ def sync_wrapper(*args, **kwargs):
312
+ try:
313
+ script = Path(inspect.getfile(method)).name
314
+ structlog.contextvars.bind_contextvars(
315
+ _function=method.__name__,
316
+ _script=script,
317
+ )
318
+
319
+ frame = inspect.currentframe()
320
+ _, _, _, frame_values = inspect.getargvalues(frame)
321
+ args_dict = {f'arg_{i}': arg for i, arg in enumerate(args)}
322
+ params = {**args_dict, **frame_values.get('kwargs')}
323
+
324
+ if params:
325
+ logger.info(
326
+ f'Method called: "{method.__name__}" with: "{params}"',
327
+ function_id=function_id,
328
+ sender='logging_decorator',
329
+ )
330
+ else:
331
+ logger.info(
332
+ f'Method "{method.__name__}" called with no arguments.',
333
+ sender='logging_decorator',
334
+ function_id=function_id,
335
+ )
336
+
337
+ value = method(*args, **kwargs)
338
+ logger.info(
339
+ f'Method "{method.__name__}" returned: "{value}"',
340
+ sender='logging_decorator',
341
+ function_id=function_id,
342
+ )
343
+ return value
344
+ except Exception as exc:
345
+ message = f'Exception occurred in method: {method.__name__}, exception: {exc}'
346
+ if send_email:
347
+ with ThreadPoolExecutor() as executor:
348
+ message = f'{message}\n\n\n{exception_to_string(excp=exc)}'
349
+ executor.submit(
350
+ partial(
351
+ smtp_email_send,
352
+ message=message,
353
+ logger_name=logger.logger_name,
354
+ )
355
+ )
356
+ logger.exception(
357
+ message,
358
+ sender='logging_decorator',
359
+ function_id=function_id,
360
+ )
361
+ raise exc
362
+ finally:
363
+ structlog.contextvars.unbind_contextvars(
364
+ '_function', '_script'
365
+ )
366
+
367
+ return sync_wrapper
368
+
369
+
370
+ def get_logger(name: str = None):
371
+ if not name:
372
+ frm = inspect.stack()[1]
373
+ mod = inspect.getmodule(frm[0])
374
+ name = mod.__name__
375
+ return RiscLogger(name=name)
376
+
377
+
378
+ def exception_to_string(excp):
379
+ stack = traceback.extract_stack()[:-3] + traceback.extract_tb(
380
+ excp.__traceback__
381
+ )
382
+ pretty = traceback.format_list(stack)
383
+ return ''.join(pretty) + '\n {} {}'.format(excp.__class__, excp)
384
+
385
+
386
+ def smtp_email_send(message: str, logger_name: str) -> None:
387
+ smtp_user = os.getenv('logging_email_smtp_user')
388
+ smtp_password = os.getenv('logging_email_smtp_password')
389
+ email_to = os.getenv('logging_email_to')
390
+ smtp_server = os.getenv('logging_email_smtp_server')
391
+
392
+ if smtp_user and smtp_password and email_to and smtp_server:
393
+ # Email server setup
394
+ smtp_user = smtp_user
395
+ smtp_password = smtp_password
396
+
397
+ # Create the email message
398
+ email_message = MIMEMultipart()
399
+ email_message['From'] = smtp_user
400
+ email_message['To'] = email_to
401
+ email_message['Subject'] = f'Error in {logger_name}'
402
+ email_message.attach(MIMEText(message, 'plain'))
403
+
404
+ # Send the email
405
+ with smtplib.SMTP(host=smtp_server, port=465) as smtp:
406
+ smtp.ehlo()
407
+ smtp.starttls()
408
+ smtp.login(smtp_user, smtp_password)
409
+ smtp.send_message(email_message)
410
+ else:
411
+ logger = get_logger(name=logger_name)
412
+ logger.error(
413
+ 'Emails cannot be sent because one or more environment variables are not set!'
414
+ )
@@ -0,0 +1,13 @@
1
+ import pytest
2
+ from risclog.logging import get_logger
3
+
4
+
5
+ @pytest.fixture(scope='function')
6
+ def setup_logger(monkeypatch):
7
+ monkeypatch.setenv('logging_email_smtp_user', 'user@example.com')
8
+ monkeypatch.setenv('logging_email_smtp_password', 'password')
9
+ monkeypatch.setenv('logging_email_to', 'recipient@example.com')
10
+ monkeypatch.setenv('logging_email_smtp_server', 'smtp.example.com')
11
+
12
+ logger = get_logger('test_logger')
13
+ return logger
File without changes
@@ -0,0 +1,284 @@
1
+ import logging
2
+ import sys
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+ from risclog.logging import RiscLogger, get_logger, rename_event_to_message
7
+
8
+
9
+ def test_logger_singleton(setup_logger):
10
+ logger1 = get_logger('test_logger')
11
+ logger2 = get_logger(__name__)
12
+
13
+ assert logger1 is logger2, 'RiscLogger should be a singleton instance'
14
+
15
+
16
+ def test_debug_log(setup_logger, caplog):
17
+ logger = setup_logger
18
+
19
+ with caplog.at_level(logging.DEBUG):
20
+ logger.debug('This is a debug message')
21
+
22
+ assert 'This is a debug message' in caplog.text
23
+ assert 'test_logger' in caplog.text
24
+ assert 'DEBUG' in caplog.text
25
+
26
+
27
+ def test_info_log(setup_logger, caplog):
28
+ logger = setup_logger
29
+
30
+ with caplog.at_level(logging.INFO):
31
+ logger.info('This is an info message')
32
+
33
+ assert 'This is an info message' in caplog.text
34
+ assert 'test_logger' in caplog.text
35
+ assert 'INFO' in caplog.text
36
+
37
+
38
+ def test_warning_log(setup_logger, caplog):
39
+ logger = setup_logger
40
+
41
+ with caplog.at_level(logging.WARNING):
42
+ logger.warning('This is a warning message')
43
+
44
+ assert 'This is a warning message' in caplog.text
45
+ assert 'test_logger' in caplog.text
46
+ assert 'WARNING' in caplog.text
47
+
48
+
49
+ def test_error_log(setup_logger, caplog):
50
+ logger = setup_logger
51
+
52
+ with caplog.at_level(logging.ERROR):
53
+ logger.error('This is an error message')
54
+
55
+ assert 'This is an error message' in caplog.text
56
+ assert 'test_logger' in caplog.text
57
+ assert 'ERROR' in caplog.text
58
+
59
+
60
+ def test_critical_log(setup_logger, caplog):
61
+ logger = setup_logger
62
+
63
+ with caplog.at_level(logging.CRITICAL):
64
+ logger.critical('This is a critical message')
65
+
66
+ assert 'This is a critical message' in caplog.text
67
+ assert 'test_logger' in caplog.text
68
+ assert 'CRITICAL' in caplog.text
69
+
70
+
71
+ def test_fatal_log(setup_logger, caplog):
72
+ logger = setup_logger
73
+
74
+ with caplog.at_level(logging.FATAL):
75
+ logger.fatal('This is a fatal message')
76
+
77
+ assert 'This is a fatal message' in caplog.text
78
+ assert 'test_logger' in caplog.text
79
+ assert 'CRITICAL' in caplog.text
80
+
81
+
82
+ def test_exception_log(setup_logger, caplog):
83
+ logger = setup_logger
84
+
85
+ with caplog.at_level(logging.INFO):
86
+ logger.exception('This is a exception message')
87
+
88
+ assert 'This is a exception message' in caplog.text
89
+ assert 'test_logger' in caplog.text
90
+ assert 'ERROR' in caplog.text
91
+
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_async_logging_decorator(setup_logger, caplog):
95
+ logger = setup_logger
96
+
97
+ @logger.decorator
98
+ async def async_test_func(arg_0, arg_1):
99
+ return f'Result: {arg_0 + arg_1}'
100
+
101
+ with caplog.at_level(logging.INFO):
102
+ result = await async_test_func(1, 2)
103
+
104
+ assert result == 'Result: 3'
105
+
106
+ log_records = [
107
+ record for record in caplog.records if record.levelname == 'INFO'
108
+ ]
109
+ assert len(log_records) >= 2
110
+
111
+ first_log = log_records[0].msg
112
+ second_log = log_records[1].msg
113
+
114
+ assert (
115
+ 'Method called: "async_test_func" with: "{\'arg_0\': 1, \'arg_1\': 2}"'
116
+ in first_log['message']
117
+ )
118
+ assert (
119
+ 'Method "async_test_func" returned: "Result: 3"'
120
+ in second_log['message']
121
+ )
122
+
123
+
124
+ def test_sync_logging_decorator(setup_logger, caplog):
125
+ logger = setup_logger
126
+
127
+ @logger.decorator()
128
+ def sync_test_func(arg1, arg2):
129
+ return f'Result: {arg1 + arg2}'
130
+
131
+ with caplog.at_level(logging.INFO):
132
+ result = sync_test_func(3, 4)
133
+
134
+ assert result == 'Result: 7'
135
+
136
+ log_records = [
137
+ record for record in caplog.records if record.levelname == 'INFO'
138
+ ]
139
+ assert len(log_records) >= 2
140
+
141
+ first_log = log_records[0].msg
142
+ second_log = log_records[1].msg
143
+
144
+ assert (
145
+ 'Method called: "sync_test_func" with: "{\'arg_0\': 3, \'arg_1\': 4}"'
146
+ in first_log['message']
147
+ )
148
+ assert (
149
+ 'Method "sync_test_func" returned: "Result: 7"'
150
+ in second_log['message']
151
+ )
152
+
153
+
154
+ @patch('risclog.logging.smtp_email_send')
155
+ def test_exception_logging_with_email(mock_smtp_send, setup_logger, caplog):
156
+ logger = setup_logger
157
+
158
+ @logger.decorator(send_email=True)
159
+ def faulty_func():
160
+ raise ValueError('This is an error')
161
+
162
+ with caplog.at_level(logging.ERROR):
163
+ with pytest.raises(ValueError, match='This is an error'):
164
+ faulty_func()
165
+
166
+ assert 'Exception occurred in method: faulty_func' in caplog.text
167
+ assert 'This is an error' in caplog.text
168
+
169
+ assert (
170
+ mock_smtp_send.called
171
+ ), 'smtp_email_send should be called when an exception occurs with send_email=True'
172
+ mock_smtp_send.assert_called_once()
173
+
174
+ args, kwargs = mock_smtp_send.call_args
175
+
176
+ assert (
177
+ len(args) == 0
178
+ ), 'smtp_email_send should not be called with positional arguments'
179
+ assert (
180
+ len(kwargs) == 2
181
+ ), 'smtp_email_send should be called with two keyword arguments'
182
+
183
+ expected_message = 'Exception occurred in method: faulty_func'
184
+ expected_logger_name = 'test_logger'
185
+ assert (
186
+ expected_message in kwargs['message']
187
+ ), f"First keyword argument should be '{expected_message}'"
188
+ assert (
189
+ kwargs['logger_name'] == expected_logger_name
190
+ ), f"Second keyword argument should be '{expected_logger_name}'"
191
+
192
+
193
+ @patch('risclog.logging.smtp_email_send')
194
+ @pytest.mark.asyncio
195
+ async def test_async_exception_logging_with_email(
196
+ mock_smtp_send, setup_logger, caplog
197
+ ):
198
+ logger = setup_logger
199
+
200
+ @logger.decorator(send_email=True)
201
+ async def faulty_async_func():
202
+ raise ValueError('This is an async error')
203
+
204
+ with caplog.at_level(logging.ERROR):
205
+ with pytest.raises(ValueError, match='This is an async error'):
206
+ await faulty_async_func()
207
+
208
+ assert 'Exception occurred in method: faulty_async_func' in caplog.text
209
+ assert 'This is an async error' in caplog.text
210
+
211
+ assert (
212
+ mock_smtp_send.called
213
+ ), 'smtp_email_send should be called when an exception occurs in async function with send_email=True'
214
+ mock_smtp_send.assert_called_once()
215
+ args, kwargs = mock_smtp_send.call_args
216
+
217
+ assert (
218
+ len(args) == 0
219
+ ), 'smtp_email_send should not be called with positional arguments'
220
+ assert (
221
+ len(kwargs) == 2
222
+ ), 'smtp_email_send should be called with two keyword arguments'
223
+
224
+ assert (
225
+ 'This is an async error' in kwargs['message']
226
+ ), 'The error message should be in the email content'
227
+ assert (
228
+ 'test_logger' in kwargs['logger_name']
229
+ ), 'Logger name should be passed to the smtp_email_send function'
230
+
231
+
232
+ def test_rename_event_to_message():
233
+ event_dict = {
234
+ 'event': 'This is an event message',
235
+ 'level': 'info',
236
+ 'referer': 'https://example.com',
237
+ 'user': 'test_user',
238
+ }
239
+ expected_dict = {
240
+ 'level': 'info',
241
+ 'user': 'test_user',
242
+ 'message': 'This is an event message',
243
+ 'referer': 'https://example.com',
244
+ }
245
+ result_dict = rename_event_to_message(None, None, event_dict)
246
+
247
+ assert (
248
+ result_dict == expected_dict
249
+ ), f'Expected {expected_dict} but got {result_dict}'
250
+
251
+
252
+ def test_no_event_key():
253
+ event_dict = {
254
+ 'level': 'info',
255
+ 'user': 'test_user',
256
+ 'referer': 'https://example.com',
257
+ }
258
+ expected_dict = {
259
+ 'level': 'info',
260
+ 'user': 'test_user',
261
+ 'referer': 'https://example.com',
262
+ }
263
+ result_dict = rename_event_to_message(None, None, event_dict)
264
+
265
+ assert (
266
+ result_dict == expected_dict
267
+ ), f'Expected {expected_dict} but got {result_dict}'
268
+
269
+
270
+ def test_empty_dict():
271
+ event_dict = {}
272
+ expected_dict = {}
273
+ result_dict = rename_event_to_message(None, None, event_dict)
274
+
275
+ assert (
276
+ result_dict == expected_dict
277
+ ), f'Expected {expected_dict} but got {result_dict}'
278
+
279
+
280
+ def test_handle_keyboard_interrupt(setup_logger):
281
+ with patch('sys.__excepthook__') as mock_excepthook:
282
+ RiscLogger._configure_logger()
283
+ sys.excepthook(KeyboardInterrupt, None, None)
284
+ mock_excepthook.assert_called_once_with(KeyboardInterrupt, None, None)
@@ -0,0 +1 @@
1
+ import sys, types, os;p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('risclog',));importlib = __import__('importlib.util');__import__('importlib.machinery');m = sys.modules.setdefault('risclog', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('risclog', [os.path.dirname(p)])));m = m or sys.modules.setdefault('risclog', types.ModuleType('risclog'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
@@ -0,0 +1,13 @@
1
+ =======
2
+ Credits
3
+ =======
4
+
5
+ Development Lead
6
+ ----------------
7
+
8
+ * riscLOG Solution GmbH <info@risclog.de>
9
+
10
+ Contributors
11
+ ------------
12
+
13
+ None yet. Why not be the first?
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, riscLOG Solution GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.1
2
+ Name: risclog.logging
3
+ Version: 1.0
4
+ Summary: A logger based on structlog
5
+ Home-page: https://github.com/risclog-solution/risclog.logging
6
+ Author: riscLOG Solution GmbH
7
+ Author-email: info@risclog.de
8
+ License: MIT license
9
+ Keywords: risclog.logging
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: German
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Python: >=3.6
18
+ License-File: LICENSE
19
+ License-File: AUTHORS.rst
20
+ Requires-Dist: structlog
21
+ Provides-Extra: docs
22
+ Requires-Dist: Sphinx ; extra == 'docs'
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest-cache ; extra == 'test'
25
+ Requires-Dist: pytest-cov ; extra == 'test'
26
+ Requires-Dist: pytest-flake8 ; extra == 'test'
27
+ Requires-Dist: pytest-rerunfailures ; extra == 'test'
28
+ Requires-Dist: pytest-sugar ; extra == 'test'
29
+ Requires-Dist: pytest ; extra == 'test'
30
+ Requires-Dist: coverage ; extra == 'test'
31
+ Requires-Dist: flake8 <4 ; extra == 'test'
32
+ Requires-Dist: mock ; extra == 'test'
33
+ Requires-Dist: requests ; extra == 'test'
34
+ Requires-Dist: httpx ; extra == 'test'
35
+ Requires-Dist: pytest-asyncio ; extra == 'test'
36
+
37
+ ===============
38
+ risclog.logging
39
+ ===============
40
+
41
+ .. image:: https://github.com/risclog-solution/risclog.logging/workflows/Test/badge.svg?branch=master
42
+ :target: https://github.com/risclog-solution/risclog.logging/actions?workflow=Test
43
+ :alt: CI Status
44
+
45
+
46
+ .. image:: https://img.shields.io/pypi/v/risclog.logging.svg
47
+ :target: https://pypi.python.org/pypi/risclog.logging
48
+
49
+ .. image:: https://img.shields.io/travis/risclog-solution/risclog.logging.svg
50
+ :target: https://travis-ci.com/risclog-solution/risclog.logging
51
+
52
+ .. image:: https://readthedocs.org/projects/risclog.logging/badge/?version=latest
53
+ :target: https://risclog.logging.readthedocs.io/en/latest/?version=latest
54
+ :alt: Documentation Status
55
+
56
+ The risclog.logging package provides a comprehensive solution for structured logging in Python applications. Risclog.logging uses structlog and logging to generate detailed and formatted log entries. The package supports both synchronous and asynchronous log messages and provides options for automatic e-mail notification of exception errors.
57
+
58
+
59
+ * Free software: MIT license
60
+ * Documentation: https://risclog.logging.readthedocs.io.
61
+
62
+
63
+ Features
64
+ ========
65
+
66
+
67
+ Creating a logger
68
+ -----------------
69
+
70
+ To create a logger, use the get_logger function. This function ensures that you get an instance of RiscLogger that is properly configured.
71
+
72
+ .. code-block:: python
73
+
74
+ from risclog.logging import get_logger
75
+
76
+ # create logger
77
+ logger = get_logger(name='my_logger')
78
+
79
+
80
+ Configuration of the logger
81
+ ---------------------------
82
+
83
+ The logger configuration takes place automatically when the logger instance is created using get_logger. The _configure_logger method sets up structlog and logging to provide logs with timestamps, context variables and formatting. You can customize the configuration as required.
84
+
85
+ The module is configured to automatically read the logging level from an environment variable. By default, the level is set to `INFO`. To adjust this, set the `LOG_LEVEL` environment variable:
86
+
87
+ .. code-block:: bash
88
+
89
+ export LOG_LEVEL=DEBUG
90
+
91
+
92
+ Use the following methods to log messages with different log levels:
93
+
94
+ * Debug-message: logger.debug("This is a debug message")
95
+ * Info-message: logger.info("This is an info message")
96
+ * Warning-message: logger.warning("This is a warning message")
97
+ * Error-message: logger.error("This is an error message")
98
+ * Critical-message: logger.critical("This is a critical message")
99
+ * Fatal-message: logger.fatal("This is a fatal message")
100
+ * Exception-message: logger.exception("This is an exception message")
101
+
102
+
103
+ Asynchronous and synchronous log messages
104
+ -----------------------------------------
105
+
106
+ The risclog.logging package supports both synchronous and asynchronous log messages. If you are working in an asynchronous environment, use the asynchronous versions of the log methods:
107
+
108
+ * Asynchronous debug message: await logger.debug("Async debug message")
109
+ * Asynchronous info message: await logger.info("Async info message")
110
+ * And so on...
111
+
112
+
113
+ Decorator for logging
114
+ ---------------------
115
+
116
+ The decorator decorator can be used to provide methods with automatic logging and optional e-mail notification of exceptions
117
+
118
+ .. code-block:: python
119
+
120
+ from risclog.logging import get_logger
121
+
122
+ logger = get_logger(name='my_logger')
123
+
124
+ @logger.decorator(send_email=True)
125
+ async def some_async_function(x, y):
126
+ return x + y
127
+
128
+ .. code-block:: python
129
+
130
+ from risclog.logging import get_logger
131
+
132
+ logger = get_logger(name='my_logger')
133
+
134
+ @logger.decorator
135
+ def some_sync_function(x, y):
136
+ return x + y
137
+
138
+
139
+ Error handling and e-mail notification
140
+ --------------------------------------
141
+
142
+ If you set the send_email parameter to True, an email notification is automatically sent in the event of an exception. The email is sent asynchronously via a ThreadPoolExecutor and contains the exception details.
143
+
144
+ **To be able to send e-mails, the following environment variables must be set!**
145
+
146
+ * 'logging_email_smtp_user'
147
+ * 'logging_email_smtp_password'
148
+ * 'logging_email_to'
149
+ * 'logging_email_smtp_server'
150
+
151
+
152
+ Example
153
+ -------
154
+
155
+ Here is a complete example showing how to use the risclog.logginng package in an application
156
+
157
+
158
+ .. code-block:: python
159
+
160
+ import os
161
+ import asyncio
162
+ from risclog.logging import get_logger
163
+
164
+
165
+ os.environ["LOG_LEVEL"] = "DEBUG"
166
+
167
+ logger = get_logger("async_debug_example")
168
+
169
+
170
+ @logger.decorator(send_email=True)
171
+ async def fetch_data(url: str):
172
+ await logger.debug(f"Start retrieving data from {url}")
173
+ await asyncio.sleep(2) # Simulates a delay, such as a network request
174
+ await logger.debug(f"Successfully retrieved data from {url}")
175
+ return {"data": f"Sample data from {url}"}
176
+
177
+
178
+ @logger.decorator
179
+ async def main():
180
+ url = "https://example.com"
181
+ await logger.debug(f"Start main function with URL: {url}")
182
+ data = await fetch_data(url)
183
+ await logger.debug(f"Data received: {data}")
184
+
185
+
186
+ if __name__ == "__main__":
187
+ logger.info("Start main function")
188
+ asyncio.run(main())
189
+
190
+
191
+ output:
192
+
193
+ .. code-block:: bash
194
+
195
+ 2024-08-05 11:38:51 [info ] [async_debug_example] __id=4378622064 __sender=inline message=Start main function
196
+ 2024-08-05 11:38:51 [info ] [async_debug_example] __id=4384943584 __sender=async_logging_decorator _function=main _script=example.py message=Method "main" called with no arguments.
197
+ 2024-08-05 11:38:51 [debug ] [async_debug_example] __id=4378552584 __sender=inline _function=main _script=example.py message=Start main function with URL: https://example.com
198
+ 2024-08-05 11:38:51 [info ] [async_debug_example] __id=4384943744 __sender=async_logging_decorator _function=fetch_data _script=example.py message=Method called: "fetch_data" with: "{'arg_0': 'https://example.com'}"
199
+ 2024-08-05 11:38:51 [debug ] [async_debug_example] __id=4366292144 __sender=inline _function=fetch_data _script=example.py message=Start retrieving data from https://example.com
200
+ 2024-08-05 11:38:53 [debug ] [async_debug_example] __id=4366292144 __sender=inline _function=fetch_data _script=example.py message=Successfully retrieved data from https://example.com
201
+ 2024-08-05 11:38:53 [info ] [async_debug_example] __id=4384943744 __sender=async_logging_decorator _function=fetch_data _script=example.py message=Method "fetch_data" returned: "{'data': 'Sample data from https://example.com'}"
202
+ 2024-08-05 11:38:53 [debug ] [async_debug_example] __id=4378552584 __sender=inline message=Data received: {'data': 'Sample data from https://example.com'}
203
+ 2024-08-05 11:38:53 [info ] [async_debug_example] __id=4384943584 __sender=async_logging_decorator message=Method "main" returned: "None"
204
+
205
+
206
+
207
+ Run tests::
208
+
209
+ $ ./pytest
210
+
211
+
212
+ Credits
213
+ =======
214
+
215
+ This package was created with Cookiecutter_ and the `risclog-solution/risclog-cookiecutter-pypackage`_ project template.
216
+
217
+ .. _Cookiecutter: https://github.com/audreyr/cookiecutter
218
+ .. _`risclog-solution/risclog-cookiecutter-pypackage`: https://github.com/risclog-solution/risclog-cookiecutter-pypackage
219
+
220
+
221
+ This package uses AppEnv_ for running tests inside this package.
222
+
223
+ .. _AppEnv: https://github.com/flyingcircusio/appenv
224
+
225
+
226
+ ==============================
227
+ Change log for risclog.logging
228
+ ==============================
229
+
230
+
231
+ 1.0 (2024-08-06)
232
+ ================
233
+
234
+ * initial release
@@ -0,0 +1,12 @@
1
+ risclog.logging-1.0-py3.9-nspkg.pth,sha256=hcuO9Dx6k_WjALKvuGBr2x8pf_5TVBGuMEPDx89xWBE,472
2
+ risclog/logging/__init__.py,sha256=mpToVrvyjXaKzRoaVbof6kGAVWycJaejOuu85lkbmS8,14185
3
+ risclog/logging/conftest.py,sha256=A-pZNNNyRUyj-uXZ7hPvzEl-ssvW-wJFTSy3veSFB4A,454
4
+ risclog/logging/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ risclog/logging/tests/test_logger.py,sha256=-frSVaqDgBmshmo5qtHL_9j47JfztLzDYnp2ujk2SbI,8050
6
+ risclog.logging-1.0.dist-info/AUTHORS.rst,sha256=pVVRKSOAsIIpu2buAkIq8tk9Dy_sY6IhsUSIhHNtWYI,162
7
+ risclog.logging-1.0.dist-info/LICENSE,sha256=oMdbT2VqsrzqxdQaxc2s4GTDfqoaZL5MBuxiVj-ouJA,1079
8
+ risclog.logging-1.0.dist-info/METADATA,sha256=b-oIziAhO3yl_C9yBFZ7rrNVIBTkmlG6CoC1DS8xvRY,8652
9
+ risclog.logging-1.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
10
+ risclog.logging-1.0.dist-info/namespace_packages.txt,sha256=3mNzAyFsNr3rScCOTOeT6a_uYKMR3jp5ucOiCZy5k8g,8
11
+ risclog.logging-1.0.dist-info/top_level.txt,sha256=3mNzAyFsNr3rScCOTOeT6a_uYKMR3jp5ucOiCZy5k8g,8
12
+ risclog.logging-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ risclog
@@ -0,0 +1 @@
1
+ risclog