Skip to content

Commit 5af30d0

Browse files
comments
1 parent ddae8b5 commit 5af30d0

File tree

19 files changed

+174
-429
lines changed

19 files changed

+174
-429
lines changed

docs-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ redis>=2.6
4747
remoulade>=0.50
4848
sqlalchemy>=1.0
4949
starlette~=0.50
50-
structlog~=21.1
50+
structlog>=21.1
5151
tornado>=5.1.1
5252
tortoise-orm>=0.17.0
5353

instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/handler.py

Lines changed: 1 addition & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
LoggerProvider,
2727
LogRecord,
2828
NoOpLogger,
29-
SeverityNumber,
3029
get_logger,
3130
get_logger_provider,
3231
)
3332
from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES
3433
from opentelemetry.context import get_current
34+
from opentelemetry.instrumentation.log_utils import std_to_otel
3535
from opentelemetry.semconv._incubating.attributes import code_attributes
3636
from opentelemetry.semconv.attributes import exception_attributes
3737
from opentelemetry.util.types import _ExtendedAttributes
@@ -244,63 +244,3 @@ def flush(self) -> None:
244244
# details see https://github.com/open-telemetry/opentelemetry-python/pull/4636.
245245
thread = threading.Thread(target=self._logger_provider.force_flush) # type: ignore[reportAttributeAccessIssue]
246246
thread.start()
247-
248-
249-
_STD_TO_OTEL = {
250-
10: SeverityNumber.DEBUG,
251-
11: SeverityNumber.DEBUG2,
252-
12: SeverityNumber.DEBUG3,
253-
13: SeverityNumber.DEBUG4,
254-
14: SeverityNumber.DEBUG4,
255-
15: SeverityNumber.DEBUG4,
256-
16: SeverityNumber.DEBUG4,
257-
17: SeverityNumber.DEBUG4,
258-
18: SeverityNumber.DEBUG4,
259-
19: SeverityNumber.DEBUG4,
260-
20: SeverityNumber.INFO,
261-
21: SeverityNumber.INFO2,
262-
22: SeverityNumber.INFO3,
263-
23: SeverityNumber.INFO4,
264-
24: SeverityNumber.INFO4,
265-
25: SeverityNumber.INFO4,
266-
26: SeverityNumber.INFO4,
267-
27: SeverityNumber.INFO4,
268-
28: SeverityNumber.INFO4,
269-
29: SeverityNumber.INFO4,
270-
30: SeverityNumber.WARN,
271-
31: SeverityNumber.WARN2,
272-
32: SeverityNumber.WARN3,
273-
33: SeverityNumber.WARN4,
274-
34: SeverityNumber.WARN4,
275-
35: SeverityNumber.WARN4,
276-
36: SeverityNumber.WARN4,
277-
37: SeverityNumber.WARN4,
278-
38: SeverityNumber.WARN4,
279-
39: SeverityNumber.WARN4,
280-
40: SeverityNumber.ERROR,
281-
41: SeverityNumber.ERROR2,
282-
42: SeverityNumber.ERROR3,
283-
43: SeverityNumber.ERROR4,
284-
44: SeverityNumber.ERROR4,
285-
45: SeverityNumber.ERROR4,
286-
46: SeverityNumber.ERROR4,
287-
47: SeverityNumber.ERROR4,
288-
48: SeverityNumber.ERROR4,
289-
49: SeverityNumber.ERROR4,
290-
50: SeverityNumber.FATAL,
291-
51: SeverityNumber.FATAL2,
292-
52: SeverityNumber.FATAL3,
293-
53: SeverityNumber.FATAL4,
294-
}
295-
296-
297-
def std_to_otel(levelno: int) -> SeverityNumber:
298-
"""
299-
Map python log levelno as defined in https://docs.python.org/3/library/logging.html#logging-levels
300-
to OTel log severity number as defined here: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber
301-
"""
302-
if levelno < 10:
303-
return SeverityNumber.UNSPECIFIED
304-
if levelno > 53:
305-
return SeverityNumber.FATAL4
306-
return _STD_TO_OTEL[levelno]

instrumentation/opentelemetry-instrumentation-structlog/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ dependencies = [
3131

3232
[project.optional-dependencies]
3333
instruments = [
34-
"structlog ~= 21.1",
34+
"structlog >= 21.1",
3535
]
3636

3737
[project.entry-points.opentelemetry_instrumentor]

instrumentation/opentelemetry-instrumentation-structlog/src/opentelemetry/instrumentation/structlog/__init__.py

Lines changed: 38 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@
3434
import threading
3535
import traceback
3636
from time import time_ns
37-
from typing import Any, Collection
37+
from typing import Any, Callable, Collection, Optional
3838

3939
import structlog
4040

4141
from opentelemetry._logs import (
4242
LogRecord,
4343
NoOpLogger,
44-
SeverityNumber,
4544
get_logger,
4645
get_logger_provider,
4746
)
4847
from opentelemetry.context import get_current
4948
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
49+
from opentelemetry.instrumentation.log_utils import std_to_otel
5050
from opentelemetry.instrumentation.structlog.package import _instruments
5151
from opentelemetry.semconv._incubating.attributes import (
5252
exception_attributes,
@@ -77,71 +77,12 @@
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

14081
class 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

instrumentation/opentelemetry-instrumentation-structlog/tests/test_structlog.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ def setUp(self):
4545
SimpleLogRecordProcessor(self.exporter)
4646
)
4747

48-
# Configure structlog with OTel processor
48+
# Configure structlog with OTel handler
4949
self.processor = StructlogHandler(logger_provider=self.logger_provider)
5050
structlog.configure(
5151
processors=[
52+
structlog.stdlib.add_log_level,
5253
self.processor,
5354
structlog.dev.ConsoleRenderer(),
5455
]
@@ -352,7 +353,7 @@ def tearDown(self):
352353
self.exporter.clear()
353354

354355
def test_instrument_adds_processor(self):
355-
"""Test that instrument() adds the OTel processor to the chain."""
356+
"""Test that instrument() adds the OTel handler to the chain."""
356357
# Configure structlog with a simple processor chain
357358
structlog.configure(
358359
processors=[
@@ -374,13 +375,13 @@ def test_instrument_adds_processor(self):
374375
self.assertEqual(len(new_processors), initial_count + 1)
375376

376377
# Check that a StructlogHandler is in the chain
377-
has_otel_processor = any(
378+
has_otel_handler = any(
378379
isinstance(p, StructlogHandler) for p in new_processors
379380
)
380-
self.assertTrue(has_otel_processor)
381+
self.assertTrue(has_otel_handler)
381382

382383
def test_uninstrument_removes_processor(self):
383-
"""Test that uninstrument() removes the OTel processor."""
384+
"""Test that uninstrument() removes the OTel handler."""
384385
# Configure structlog
385386
structlog.configure(
386387
processors=[
@@ -393,7 +394,7 @@ def test_uninstrument_removes_processor(self):
393394
logger_provider=self.logger_provider
394395
)
395396

396-
# Verify processor was added
397+
# Verify handler was added
397398
config_after_instrument = structlog.get_config()["processors"]
398399
has_otel = any(
399400
isinstance(p, StructlogHandler) for p in config_after_instrument
@@ -403,7 +404,7 @@ def test_uninstrument_removes_processor(self):
403404
# Uninstrument
404405
StructlogInstrumentor().uninstrument()
405406

406-
# Verify processor was removed
407+
# Verify handler was removed
407408
config_after_uninstrument = structlog.get_config()["processors"]
408409
has_otel = any(
409410
isinstance(p, StructlogHandler) for p in config_after_uninstrument
@@ -461,6 +462,37 @@ def test_custom_logger_provider(self):
461462
self.assertEqual(len(logs), 1)
462463
self.assertEqual(logs[0].log_record.body, "test message")
463464

465+
def test_configure_after_instrument_preserves_handler(self):
466+
"""Test that calling structlog.configure() after instrumentation preserves the handler."""
467+
StructlogInstrumentor().instrument(
468+
logger_provider=self.logger_provider
469+
)
470+
471+
# Simulate user code calling structlog.configure after instrumentation
472+
structlog.configure(
473+
processors=[
474+
structlog.dev.ConsoleRenderer(),
475+
]
476+
)
477+
478+
processors = structlog.get_config()["processors"]
479+
has_otel_handler = any(
480+
isinstance(p, StructlogHandler) for p in processors
481+
)
482+
self.assertTrue(has_otel_handler)
483+
484+
def test_uninstrument_restores_configure(self):
485+
"""Test that uninstrument() restores the original structlog.configure."""
486+
original_configure = structlog.configure
487+
488+
StructlogInstrumentor().instrument(
489+
logger_provider=self.logger_provider
490+
)
491+
self.assertIsNot(structlog.configure, original_configure)
492+
493+
StructlogInstrumentor().uninstrument()
494+
self.assertIs(structlog.configure, original_configure)
495+
464496
def test_instrumentation_dependencies(self):
465497
"""Test that instrumentation_dependencies returns the correct value."""
466498
instrumentor = StructlogInstrumentor()

0 commit comments

Comments
 (0)