1+ from __future__ import annotations
2+ import json
3+ import logging
4+ import os
5+ import sys
6+ from logging .handlers import RotatingFileHandler , QueueHandler , QueueListener
7+ import contextvars
8+ from queue import SimpleQueue
9+ from typing import Optional , Dict , Any
10+
11+ _correlation_id = contextvars .ContextVar ("correlation_id" , default = None )
12+
13+ def set_correlation_id (corr_id : Optional [str ]) -> None :
14+ _correlation_id .set (corr_id )
15+
16+ def get_correlation_id () -> Optional [str ]:
17+ return _correlation_id .get ()
18+
19+ class JsonFormatter (logging .Formatter ):
20+ def __init__ (self , static_fields : Optional [Dict [str , Any ]] = None ):
21+ super ().__init__ ()
22+ self .static_fields = static_fields or {}
23+
24+ def format (self , record : logging .LogRecord ) -> str :
25+ payload : Dict [str , Any ] = {
26+ "level" : record .levelname .lower (),
27+ "message" : record .getMessage (),
28+ "logger" : record .name ,
29+ "module" : record .module ,
30+ "function" : record .funcName ,
31+ "line" : record .lineno ,
32+ "process" : record .process ,
33+ "thread" : record .thread ,
34+ }
35+ if self .static_fields :
36+ payload .update (self .static_fields )
37+ corr = getattr (record , "correlation_id" , None )
38+ if corr :
39+ payload ["correlation_id" ] = corr
40+ for k , v in record .__dict__ .items ():
41+ if k in payload or k .startswith ("_" ):
42+ continue
43+ if k in ("args" ,"msg" ,"exc_info" ,"exc_text" ,"stack_info" ,"msecs" ,
44+ "relativeCreated" ,"levelno" ,"pathname" ,"filename" ,
45+ "threadName" ,"processName" ,"created" ,"name" ,"lineno" ,
46+ "funcName" ,"module" ):
47+ continue
48+ if isinstance (v , (str , int , float , bool )) or v is None :
49+ payload [k ] = v
50+ else :
51+ try :
52+ json .dumps (v ); payload [k ] = v
53+ except Exception :
54+ payload [k ] = repr (v )
55+ if record .exc_info :
56+ payload ["exc_info" ] = self .formatException (record .exc_info )
57+ if record .stack_info :
58+ payload ["stack_info" ] = self .formatStack (record .stack_info )
59+ return json .dumps (payload , ensure_ascii = False )
60+
61+ class HumanFormatter (logging .Formatter ):
62+ def __init__ (self , show_logger = True ):
63+ fmt = "%(levelname)s - "
64+ fmt += "%(name)s - " if show_logger else ""
65+ fmt += "%(message)s"
66+ super ().__init__ (fmt )
67+
68+ class CorrelationAdapter (logging .LoggerAdapter ):
69+ def __init__ (self , logger : logging .Logger , static_fields : Optional [Dict [str , Any ]] = None ):
70+ super ().__init__ (logger , extra = static_fields or {})
71+
72+ def process (self , msg , kwargs ):
73+ extra = dict (kwargs .get ("extra" , {}))
74+ if "correlation_id" not in extra :
75+ corr = get_correlation_id ()
76+ if corr :
77+ extra ["correlation_id" ] = corr
78+ for k , v in self .extra .items ():
79+ extra .setdefault (k , v )
80+ kwargs ["extra" ] = extra
81+ return msg , kwargs
82+
83+ def get_logger (
84+ name : str = "BlocksLogger" ,
85+ * ,
86+ env : str = "dev" ,
87+ level : Optional [str ] = None ,
88+ log_file : Optional [str ] = None ,
89+ rotate_max_bytes : int = 2_000_000 ,
90+ rotate_backups : int = 3 ,
91+ json_console : bool = False ,
92+ service : Optional [str ] = None ,
93+ ) -> logging .LoggerAdapter :
94+
95+ eff_level = level .upper () if level else ("DEBUG" if env == "dev" else "INFO" )
96+ lvl = getattr (logging , eff_level , logging .INFO )
97+
98+ logger = logging .getLogger (name )
99+ logger .setLevel (lvl )
100+ logger .propagate = False
101+ if logger .hasHandlers ():
102+ logger .handlers .clear ()
103+
104+ # Formatters
105+ human_fmt = HumanFormatter (show_logger = True )
106+ json_fmt = JsonFormatter (static_fields = {"service" : service or name , "env" : env })
107+
108+ # Handlers reais
109+ handlers = []
110+
111+ console_handler = logging .StreamHandler (sys .stdout )
112+ console_handler .setLevel (lvl )
113+ console_handler .setFormatter (json_fmt if json_console else human_fmt )
114+ handlers .append (console_handler )
115+
116+ if log_file :
117+ os .makedirs (os .path .dirname (log_file ), exist_ok = True )
118+ file_handler = RotatingFileHandler (
119+ log_file , maxBytes = rotate_max_bytes , backupCount = rotate_backups , encoding = "utf-8"
120+ )
121+ file_handler .setLevel (lvl )
122+ file_handler .setFormatter (json_fmt )
123+ handlers .append (file_handler )
124+
125+
126+
127+ log_queue = SimpleQueue ()
128+ queue_handler = QueueHandler (log_queue )
129+ queue_handler .setLevel (lvl )
130+ logger .addHandler (queue_handler )
131+
132+ listener = QueueListener (log_queue , * handlers , respect_handler_level = True )
133+ listener .start ()
134+
135+ return CorrelationAdapter (logger , static_fields = {"service" : service or name , "env" : env })
136+
137+ __all__ = [
138+ "get_logger" , "set_correlation_id" , "get_correlation_id" ,
139+ "JsonFormatter" , "HumanFormatter" , "CorrelationAdapter" ,
0 commit comments