3434import threading
3535import traceback
3636from time import time_ns
37- from typing import Any , Collection
37+ from typing import Any , Callable , Collection , Optional
3838
3939import structlog
4040
4141from opentelemetry ._logs import (
4242 LogRecord ,
4343 NoOpLogger ,
44- SeverityNumber ,
4544 get_logger ,
4645 get_logger_provider ,
4746)
4847from opentelemetry .context import get_current
4948from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
49+ from opentelemetry .instrumentation .log_utils import std_to_otel
5050from opentelemetry .instrumentation .structlog .package import _instruments
5151from opentelemetry .semconv ._incubating .attributes import (
5252 exception_attributes ,
7777 "fatal" : 50 ,
7878}
7979
80- # Mapping from stdlib log levels to OTel severity numbers
81- _STD_TO_OTEL = {
82- 10 : SeverityNumber .DEBUG ,
83- 11 : SeverityNumber .DEBUG2 ,
84- 12 : SeverityNumber .DEBUG3 ,
85- 13 : SeverityNumber .DEBUG4 ,
86- 14 : SeverityNumber .DEBUG4 ,
87- 15 : SeverityNumber .DEBUG4 ,
88- 16 : SeverityNumber .DEBUG4 ,
89- 17 : SeverityNumber .DEBUG4 ,
90- 18 : SeverityNumber .DEBUG4 ,
91- 19 : SeverityNumber .DEBUG4 ,
92- 20 : SeverityNumber .INFO ,
93- 21 : SeverityNumber .INFO2 ,
94- 22 : SeverityNumber .INFO3 ,
95- 23 : SeverityNumber .INFO4 ,
96- 24 : SeverityNumber .INFO4 ,
97- 25 : SeverityNumber .INFO4 ,
98- 26 : SeverityNumber .INFO4 ,
99- 27 : SeverityNumber .INFO4 ,
100- 28 : SeverityNumber .INFO4 ,
101- 29 : SeverityNumber .INFO4 ,
102- 30 : SeverityNumber .WARN ,
103- 31 : SeverityNumber .WARN2 ,
104- 32 : SeverityNumber .WARN3 ,
105- 33 : SeverityNumber .WARN4 ,
106- 34 : SeverityNumber .WARN4 ,
107- 35 : SeverityNumber .WARN4 ,
108- 36 : SeverityNumber .WARN4 ,
109- 37 : SeverityNumber .WARN4 ,
110- 38 : SeverityNumber .WARN4 ,
111- 39 : SeverityNumber .WARN4 ,
112- 40 : SeverityNumber .ERROR ,
113- 41 : SeverityNumber .ERROR2 ,
114- 42 : SeverityNumber .ERROR3 ,
115- 43 : SeverityNumber .ERROR4 ,
116- 44 : SeverityNumber .ERROR4 ,
117- 45 : SeverityNumber .ERROR4 ,
118- 46 : SeverityNumber .ERROR4 ,
119- 47 : SeverityNumber .ERROR4 ,
120- 48 : SeverityNumber .ERROR4 ,
121- 49 : SeverityNumber .ERROR4 ,
122- 50 : SeverityNumber .FATAL ,
123- 51 : SeverityNumber .FATAL2 ,
124- 52 : SeverityNumber .FATAL3 ,
125- 53 : SeverityNumber .FATAL4 ,
126- }
127-
128-
129- def std_to_otel (levelno : int ) -> SeverityNumber :
130- """
131- Map python log levelno to OTel log severity number.
132- """
133- if levelno < 10 :
134- return SeverityNumber .UNSPECIFIED
135- if levelno > 53 :
136- return SeverityNumber .FATAL4
137- return _STD_TO_OTEL [levelno ]
138-
13980
14081class StructlogHandler :
14182 """
142- A structlog processor that translates structlog events into OpenTelemetry LogRecords.
83+ A structlog handler that translates structlog events into OpenTelemetry LogRecords.
14384
144- This processor should be added to the structlog processor chain to emit logs
85+ This handler should be added to the structlog processor chain to emit logs
14586 to OpenTelemetry. It translates structlog's event dictionary format into the
14687 OpenTelemetry Logs data model.
14788
@@ -150,14 +91,14 @@ class StructlogHandler:
15091 """
15192
15293 def __init__ (self , logger_provider = None ):
153- """Initialize the processor with an optional logger provider."""
94+ """Initialize the handler with an optional logger provider."""
15495 self ._logger_provider = logger_provider or get_logger_provider ()
15596
15697 def __call__ (self , logger , name : str , event_dict : dict ) -> dict :
15798 """
15899 Process a structlog event and emit it as an OpenTelemetry log.
159100
160- This method implements the structlog processor interface. It receives
101+ This method implements the structlog handler interface. It receives
161102 the event dictionary, translates it to an OTel LogRecord, and emits it.
162103
163104 Args:
@@ -296,7 +237,7 @@ class StructlogInstrumentor(BaseInstrumentor):
296237 """
297238 An instrumentor for the structlog logging library.
298239
299- This instrumentor adds an StructlogHandler to the structlog processor
240+ This instrumentor adds a StructlogHandler to the structlog processor
300241 chain, enabling automatic emission of structlog events as OpenTelemetry logs.
301242
302243 Example:
@@ -308,6 +249,7 @@ class StructlogInstrumentor(BaseInstrumentor):
308249 """
309250
310251 _processor = None
252+ _original_configure : Optional [Callable ] = None
311253
312254 def instrumentation_dependencies (self ) -> Collection [str ]:
313255 """Return the required instrumentation dependencies."""
@@ -357,6 +299,29 @@ def _instrument(self, **kwargs):
357299 # Store reference for uninstrumentation
358300 StructlogInstrumentor ._processor = processor
359301
302+ # Wrap structlog.configure so that if user code calls it after
303+ # instrumentation, the handler is re-inserted into the new chain.
304+ StructlogInstrumentor ._original_configure = structlog .configure
305+
306+ def _patched_configure (* args , ** kwargs ):
307+ # If the user is supplying a processors list, ensure our handler
308+ # is included before passing it to the original configure.
309+ if "processors" in kwargs :
310+ processors = list (kwargs ["processors" ])
311+ if not any (
312+ isinstance (p , StructlogHandler ) for p in processors
313+ ):
314+ insert_position = max (len (processors ) - 1 , 0 )
315+ processors .insert (
316+ insert_position , StructlogInstrumentor ._processor
317+ )
318+ kwargs ["processors" ] = processors
319+ original = StructlogInstrumentor ._original_configure
320+ if original is not None :
321+ original (* args , ** kwargs )
322+
323+ structlog .configure = _patched_configure
324+
360325 def _uninstrument (self , ** kwargs ):
361326 """
362327 Remove the StructlogHandler from structlog's processor chain.
@@ -375,7 +340,13 @@ def _uninstrument(self, **kwargs):
375340 if not isinstance (p , StructlogHandler )
376341 ]
377342
378- # Reconfigure structlog
343+ # Restore the original structlog.configure before reconfiguring so
344+ # the patched version does not re-insert the handler.
345+ if StructlogInstrumentor ._original_configure is not None :
346+ structlog .configure = StructlogInstrumentor ._original_configure
347+ StructlogInstrumentor ._original_configure = None
348+
349+ # Reconfigure structlog without the handler
379350 structlog .configure (processors = new_processors )
380351
381352 # Clear reference
0 commit comments