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.
- risclog/logging/__init__.py +414 -0
- risclog/logging/conftest.py +13 -0
- risclog/logging/tests/__init__.py +0 -0
- risclog/logging/tests/test_logger.py +284 -0
- risclog.logging-1.0-py3.9-nspkg.pth +1 -0
- risclog.logging-1.0.dist-info/AUTHORS.rst +13 -0
- risclog.logging-1.0.dist-info/LICENSE +21 -0
- risclog.logging-1.0.dist-info/METADATA +234 -0
- risclog.logging-1.0.dist-info/RECORD +12 -0
- risclog.logging-1.0.dist-info/WHEEL +5 -0
- risclog.logging-1.0.dist-info/namespace_packages.txt +1 -0
- risclog.logging-1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
risclog
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
risclog
|