Skip to content

Commit 71b8248

Browse files
Feature/picpay logger module (#29)
* Add new logger module * Configure Picpay Logger settings Co-authored-by: GBgustavodutra <[email protected]>
1 parent 110889f commit 71b8248

File tree

6 files changed

+468
-98
lines changed

6 files changed

+468
-98
lines changed

events_protocol/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.3.0" # pragma: no cover
1+
__version__ = "0.3.1" # pragma: no cover
22

Lines changed: 2 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,2 @@
1-
import json
2-
import logging
3-
import queue
4-
import re
5-
import sys
6-
import typing
7-
from logging.handlers import QueueHandler, QueueListener
8-
9-
from events_protocol.core.context import EventContextHolder
10-
from datetime import datetime as dt
11-
12-
13-
def _get_klass_name(klass: typing.Any) -> str:
14-
# Simple and effective
15-
return re.sub(r"|".join(map(re.escape, ["<class", "'", ">", " "])), "", str(klass))
16-
17-
18-
_logger: logging.LoggerAdapter = None
19-
20-
21-
def _get_logger() -> logging.LoggerAdapter:
22-
global _logger
23-
if _logger:
24-
return _logger
25-
logging.basicConfig(level=logging.INFO)
26-
_queue = queue.Queue(-1)
27-
_queue_handler = QueueHandler(_queue)
28-
_handler = logging.StreamHandler(sys.stdout)
29-
_queue_listener = QueueListener(_queue, _handler)
30-
31-
_logger = logging.getLogger("gb.events_protocol")
32-
if _logger.hasHandlers():
33-
_logger.handlers.clear()
34-
_logger.addHandler(_queue_handler)
35-
_logger.propagate = False
36-
_queue_listener.start()
37-
return _logger
38-
39-
40-
class JsonLogger(logging.LoggerAdapter):
41-
version: str = "NOTDEFINED"
42-
43-
@classmethod
44-
def set_version(cls, version: str):
45-
cls.version = version
46-
47-
def __init__(self, klass=None):
48-
self.logger = _get_logger()
49-
self.klass = _get_klass_name(klass)
50-
51-
def log(self, level, msg, *args, **kwargs):
52-
if self.isEnabledFor(level):
53-
event_context = EventContextHolder.get()
54-
event_info = dict(
55-
EventID=event_context.id,
56-
FlowID=event_context.flow_id,
57-
UserId=event_context.user_id,
58-
UserType=event_context.user_type,
59-
Operation="{}:v{}".format(event_context.event_name, event_context.event_version),
60-
logger=self.klass,
61-
LoggerName=self.logger.name,
62-
ApplicationVersion=self.version,
63-
)
64-
_msg = dict(
65-
timestamp_app=dt.utcnow().astimezone().isoformat(timespec="milliseconds"),
66-
message=msg,
67-
log_type="APPLICATION",
68-
log_level=logging.getLevelName(level),
69-
event=event_info,
70-
)
71-
72-
extra = kwargs.pop("extra", None)
73-
if extra:
74-
if isinstance(extra, dict):
75-
extra = json.dumps(extra)
76-
if not isinstance(extra, str):
77-
extra_type = type(extra)
78-
raise TypeError(
79-
"Extra param needs to be dict or str, not {}".format(extra_type)
80-
)
81-
_msg["extra"] = extra
82-
if level == logging.ERROR and kwargs.get("exc_info"):
83-
args = tuple()
84-
85-
fmt = logging.Formatter()
86-
_exc = sys.exc_info()
87-
_msg["stackTrace"] = fmt.formatException(_exc)
88-
89-
kwargs["exc_info"] = False
90-
msg = json.dumps(_msg)
91-
self.logger.log(level, msg, *args, **kwargs)
1+
from .logger import *
2+
from .picpay_logger import PicpayLogger
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import logging
2+
from logging import StreamHandler
3+
from typing import List
4+
from functools import update_wrapper
5+
6+
class Logger(logging.LoggerAdapter):
7+
"""This class is responsible to be an interface for other classes and
8+
modules use the logging with the Events Protocol format.
9+
10+
Returns
11+
-------
12+
Events Protocol.package.utils.Logger
13+
This class is responsible to be an interface for other classes and modules use the logging with the Events Protocol format
14+
"""
15+
16+
HANDLERS: List[StreamHandler] = [logging.StreamHandler()]
17+
18+
def __init__(
19+
self,
20+
log_name: str,
21+
log_format: str = "$BOLD%(asctime)s$RESET %(name)-12s %(levelname)-18s %(message)s",
22+
date_format: str = "%Y-%m-%d %H:%M:%S",
23+
level: int = logging.INFO,
24+
_custom_formatter: logging.Formatter = None,
25+
) -> None:
26+
"""Custom Logger constructor.
27+
28+
Parameters
29+
----------
30+
log_name : str
31+
The name of the logger instance.
32+
log_format : str, optional
33+
The log format, by default "$BOLD%(asctime)s$RESET %(name)-12s %(levelname)-18s %(message)s"
34+
date_format : _type_, optional
35+
The date format, by default "%Y-%m-%d %H:%M:%S"
36+
level : int, optional
37+
The logger level, you can use the ones predefined inside the logging module, or provide an int according to
38+
the following pattern: CRITICAL/FATAL = 50, ERROR = 40, WARNING = 30, INFO = 20, DEBUG = 10, NOTSET = 0.
39+
For more information: https://docs.python.org/3/library/logging.html#levels, by default 20 (INFO).
40+
_custom_formatter : logging.Formatter, optional
41+
Replaces the Events Protocol ColoredFormatter by the formatter provided and will ignore the log_format parameter.
42+
"""
43+
if _custom_formatter is None:
44+
# The default logging Formatter.
45+
# self.log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
46+
color_format = ColoredFormatter.formatter_message(log_format, True)
47+
self.log_formatter = ColoredFormatter(color_format, datefmt=date_format)
48+
49+
else:
50+
self.log_formatter = _custom_formatter
51+
52+
self.level = level
53+
self.log = self._create_log(log_name)
54+
55+
if not self.log.hasHandlers():
56+
self.log.propagate = False
57+
self.__default_handlers = self.HANDLERS
58+
self.__add_default_handlers()
59+
60+
self.log.setLevel(level)
61+
62+
@staticmethod
63+
def _create_log(log_name: str) -> logging.Logger:
64+
"""Creates the logger object.
65+
66+
Parameters
67+
----------
68+
log_name : str
69+
The name of the logger.
70+
71+
Returns
72+
-------
73+
logging.Logger
74+
return the logging.Logger with the logger name provided.
75+
"""
76+
return logging.getLogger(log_name)
77+
78+
def add_handler(
79+
self,
80+
handler: logging.Handler,
81+
set_formatter: bool = True,
82+
formatter: logging.Formatter = None,
83+
) -> None:
84+
"""Method to add a new handler to the logger with the defined formatter
85+
and level.
86+
87+
Parameters
88+
----------
89+
handler : Handler
90+
The handler class to be added in the logger.
91+
set_formatter : bool, optional
92+
If the level and Formatter should be applied, by default True
93+
formatter: logging.Formatter, optional
94+
The custom formatter to be set in the handler.
95+
"""
96+
if set_formatter:
97+
if formatter is None:
98+
handler.setFormatter(self.log_formatter)
99+
else:
100+
handler.setFormatter(formatter)
101+
handler.setLevel(self.level)
102+
self.log.addHandler(handler)
103+
104+
def __add_default_handlers(self):
105+
"""Method to add the default handlers."""
106+
for handler in self.__default_handlers:
107+
self.add_handler(handler)
108+
109+
@classmethod
110+
def get_logger(
111+
cls,
112+
log_name: str,
113+
log_format: str = "$BOLD%(asctime)s$RESET %(name)-12s %(levelname)-18s %(message)s",
114+
date_format: str = "%Y-%m-%d %H:%M:%S",
115+
level: int = logging.INFO,
116+
_custom_formatter: logging.Formatter = None,
117+
) -> logging.Logger:
118+
"""Get a logger object with Events Protocol Colored Formatter.
119+
120+
Parameters
121+
----------
122+
log_name : str
123+
Name of the logger instance
124+
log_format : str, optional
125+
Format of the logger message, by default "$BOLD%(asctime)s$RESET %(name)-12s %(levelname)-18s %(message)s"
126+
date_format : str, optional
127+
Date format of the logger message, by default "%Y-%m-%d %H:%M:%S"
128+
level : int, optional
129+
The logger level, you can use the ones predefined inside the logging module, or provide an int according to
130+
the following pattern: CRITICAL/FATAL = 50, ERROR = 40, WARNING = 30, INFO = 20, DEBUG = 10, NOTSET = 0.
131+
For more information: https://docs.python.org/3/library/logging.html#levels, by default 20 (INFO).
132+
_custom_formatter : logging.Formatter, optional
133+
Replaces the Events Protocol ColoredFormatter by the formatter provided and will ignore the log_format parameter.
134+
Returns
135+
-------
136+
logging.Logger
137+
The formatted logger.
138+
"""
139+
logger = cls(
140+
log_name=log_name,
141+
log_format=log_format,
142+
date_format=date_format,
143+
level=level,
144+
_custom_formatter=_custom_formatter,
145+
)
146+
return logger.log
147+
148+
class ColoredFormatter(logging.Formatter):
149+
"""Events Protocol's Custom Colored Formatter for Logging.
150+
151+
Returns
152+
-------
153+
Events Protocol.package.utils.ColoredFormatter
154+
This class is responsible for setting the format for Events Protocol Loggings, by the default set timestamp and the module
155+
name with bold and will use the following color scheme:
156+
GREEN: DEBUG
157+
BLUE: INFO
158+
YELLOW: WARNING
159+
RED: ERROR
160+
MAGENTA: CRITICAL
161+
"""
162+
163+
# These are the sequences need to get colored output
164+
RESET_SEQ = "\033[0m"
165+
COLOR_SEQ = "\033[%dm"
166+
BOLD_SEQ = "\033[1m"
167+
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
168+
COLORS = {
169+
"DEBUG": GREEN,
170+
"INFO": BLUE,
171+
"WARNING": YELLOW,
172+
"ERROR": RED,
173+
"CRITICAL": MAGENTA,
174+
}
175+
176+
def __init__(self, msg: str, datefmt: str = "%Y-%m-%d %H:%M:%S", use_color: bool = True):
177+
"""ColoredFormatter class constructor.
178+
179+
Parameters
180+
----------
181+
msg : str
182+
The message to be printed out.
183+
datefmt : str
184+
The date format, by default "%Y-%m-%d %H:%M:%S"
185+
use_color : bool
186+
Flag to signalize if the output should be colored or not.
187+
"""
188+
logging.Formatter.__init__(self, msg, datefmt=datefmt)
189+
self.use_color = use_color
190+
191+
def format(self, record: logging.LogRecord):
192+
"""Method responsible for formatting the record.
193+
194+
Parameters
195+
----------
196+
record: logging.LogRecord
197+
The LogRecord with the message to be printed out.
198+
199+
Returns
200+
-------
201+
str
202+
The message formatted.
203+
"""
204+
levelname = record.levelname
205+
if self.use_color and levelname in self.COLORS:
206+
levelname_color = (
207+
self.COLOR_SEQ % (30 + self.COLORS[levelname]) + levelname + self.RESET_SEQ
208+
)
209+
record.levelname = levelname_color
210+
return logging.Formatter.format(self, record)
211+
212+
@classmethod
213+
def formatter_message(cls, message: str, use_color: bool = True):
214+
"""The method to parse the Events Protocol format string.
215+
216+
Parameters
217+
----------
218+
message : str
219+
The message to be formatted.
220+
use_color : bool
221+
Flag to signalize if the output should be colored or not.
222+
223+
Returns
224+
-------
225+
str
226+
The message formatted.
227+
"""
228+
if use_color:
229+
message = message.replace("$RESET", cls.RESET_SEQ).replace("$BOLD", cls.BOLD_SEQ)
230+
else:
231+
message = message.replace("$RESET", "").replace("$BOLD", "")
232+
return message
233+
234+
235+
def logger_monitor(package_name: str = None, level: int = logging.INFO):
236+
"""A logger decorator to monitor legacy methods and functions.
237+
238+
Parameters
239+
----------
240+
package_name : str, optional.
241+
The name of the package where the function or method is located. To get automatically use the __name__ variable.
242+
level : int, optional.
243+
The level of logger to be shown, by default: logging.INFO
244+
245+
Returns
246+
-------
247+
"""
248+
249+
def inner_function(function):
250+
"""The wrapper for the function or method.
251+
252+
Parameters
253+
----------
254+
function : FunctionType
255+
The function or method to be wrapped.
256+
257+
Returns
258+
-------
259+
The result of the function or method.
260+
"""
261+
262+
def wrapper(*args):
263+
"""The logger wrapper.
264+
265+
Parameters
266+
----------
267+
args : tuple
268+
The arguments of the function
269+
270+
Returns
271+
-------
272+
"""
273+
# Formats the name of the logger. If the package is provided attach it to the beginning of the logger name.
274+
logger_name = ""
275+
if package_name is not None:
276+
logger_name = f"{package_name}."
277+
logger_name += f"{function}".split(" ")[1]
278+
279+
logger = Logger.get_logger(logger_name, level=level)
280+
logger.debug(f"Using the logger monitor decorator in {logger_name}.")
281+
282+
try:
283+
result = function(*args)
284+
return result
285+
286+
except Exception as e:
287+
msg = (
288+
f"An error occurred during the execution of {function} from {package_name}. "
289+
f"\nError Type: {type(e)} {str(e)}"
290+
)
291+
logger.error(msg, exc_info=True)
292+
293+
update_wrapper(wrapper, function)
294+
return wrapper
295+
296+
return inner_function
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from logging import Logger

0 commit comments

Comments
 (0)