33import pathlib
44from collections .abc import Iterator
55from contextlib import contextmanager
6- from contextvars import ContextVar
76from typing import get_args , Literal
87
8+ import structlog
9+ from structlog .contextvars import (
10+ bind_contextvars ,
11+ get_contextvars ,
12+ merge_contextvars ,
13+ reset_contextvars ,
14+ )
15+
916LoggingLevel = Literal [
1017 "INFO" ,
1118 "DEBUG" ,
1522 "CRITICAL" ,
1623]
1724
18-
19- _LOG_CONTEXT : ContextVar [dict [str , object ] | None ] = ContextVar ("clp_log_context" , default = None )
20-
21- _LOG_CONTEXT_FIELD_ORDER = (
22- "job_id" ,
23- "query_job_type" ,
24- "task_id" ,
25- "archive_id" ,
26- "dataset" ,
27- "partition_id" ,
28- "celery_task_id" ,
29- "clp_subprocess_pid" ,
30- "query" ,
31- "query_hash" ,
32- )
25+ _TIMESTAMP_PROCESSOR = structlog .processors .TimeStamper (fmt = "iso" , utc = True , key = "timestamp" )
26+ _JSON_RENDERER = structlog .processors .JSONRenderer ()
3327
3428
3529@contextmanager
@@ -40,43 +34,43 @@ def bind_log_context(**fields: object) -> Iterator[None]:
4034 Fields with ``None`` values are ignored. Nested contexts merge with outer contexts and restore
4135 the previous context on exit.
4236 """
43- current_context = _LOG_CONTEXT .get () or {}
44- new_context = current_context .copy ()
45- new_context .update ({key : value for key , value in fields .items () if value is not None })
46- token = _LOG_CONTEXT .set (new_context )
37+ tokens = bind_contextvars (** {key : value for key , value in fields .items () if value is not None })
4738 try :
4839 yield
4940 finally :
50- _LOG_CONTEXT . reset ( token )
41+ reset_contextvars ( ** tokens )
5142
5243
5344def get_log_context () -> dict [str , object ]:
5445 """Returns a copy of the active CLP log correlation context."""
55- return ( _LOG_CONTEXT . get () or {}). copy ()
46+ return get_contextvars ()
5647
5748
58- class ClpContextFormatter (logging .Formatter ):
59- """Appends active CLP correlation fields to log records ."""
49+ class _ClpJsonFormatter (logging .Formatter ):
50+ """Formats log records as JSON and includes active CLP correlation fields."""
6051
6152 def format (self , record : logging .LogRecord ) -> str :
62- message = super ().format (record )
63- context = get_log_context ()
64- if not context :
65- return message
66-
67- fields = [f"{ key } ={ context [key ]} " for key in _LOG_CONTEXT_FIELD_ORDER if key in context ]
68- fields .extend (
69- f"{ key } ={ value } "
70- for key , value in sorted (context .items ())
71- if key not in _LOG_CONTEXT_FIELD_ORDER
72- )
73- if len (fields ) > 0 :
74- message = f"{ message } [{ ' ' .join (fields )} ]"
75- return message
53+ method_name = record .levelname .lower ()
54+ event_dict : dict [str , object ] = {
55+ "event" : record .getMessage (),
56+ "logger" : record .name ,
57+ "level" : method_name ,
58+ }
59+ if record .exc_info :
60+ event_dict ["exception" ] = self .formatException (record .exc_info )
61+ if record .stack_info :
62+ event_dict ["stack" ] = self .formatStack (record .stack_info )
63+
64+ merge_contextvars (None , method_name , event_dict )
65+ _TIMESTAMP_PROCESSOR (None , method_name , event_dict )
66+ rendered_log = _JSON_RENDERER (None , method_name , event_dict )
67+ if isinstance (rendered_log , bytes ):
68+ return rendered_log .decode ("utf-8" )
69+ return rendered_log
7670
7771
7872def get_logging_formatter () -> logging .Formatter :
79- return ClpContextFormatter ( "%(asctime)s %(name)s [%(levelname)s] %(message)s" )
73+ return _ClpJsonFormatter ( )
8074
8175
8276def set_formatter_on_handlers (logger : logging .Logger ) -> None :
0 commit comments