Skip to content

Commit c17ba89

Browse files
authored
opentelemetry-sdk: deprecate logging handler (open-telemetry#4919)
* opentelemetry-sdk: depcreate logging handler Move config handling to opentelemetry-instrumentation and the handler to opentelemetry-instrumentation-logging * Drop handling of env var * Fix typo * Reinstate the old code * opentelemetry-sdk: stop leaking handler in loggers While at it also fix the noopprovider test case that was working because we were asserting against another handler. * Don't use warnings.deprecated - too new * Update import in examples * Cleanup deprecation messages and test for them * Add changelog * Don't leak state in logging module * Add a warning that OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED is deprecated
1 parent 6ea15ff commit c17ba89

File tree

7 files changed

+111
-22
lines changed

7 files changed

+111
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15+
- `opentelemetry-sdk`: deprecate `LoggingHandler` in favor of `opentelemetry-instrumentation-logging`, see `opentelemetry-instrumentation-logging` documentation
16+
([#4919](https://github.com/open-telemetry/opentelemetry-python/pull/4919))
1517
- `opentelemetry-sdk`: Clarify log processor error handling expectations in documentation
1618
([#4915](https://github.com/open-telemetry/opentelemetry-python/pull/4915))
1719
- bump semantic-conventions to v1.40.0

docs/examples/logs/example.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
66
OTLPLogExporter,
77
)
8-
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
8+
9+
# this is available in the opentelemetry-instrumentation-logging package
10+
from opentelemetry.instrumentation.logging.handler import LoggingHandler
11+
from opentelemetry.sdk._logs import LoggerProvider
912
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
1013
from opentelemetry.sdk.resources import Resource
1114
from opentelemetry.sdk.trace import TracerProvider

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ def _init_logging(
326326
set_event_logger_provider(event_logger_provider)
327327

328328
if setup_logging_handler:
329+
warnings.warn(
330+
"The `OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED` environment variable "
331+
"and the `LoggingHandler` in `opentelemetry-sdk` that it controls are deprecated."
332+
"Install `opentelemetry-instrumentation-logging` package instead.",
333+
DeprecationWarning,
334+
)
335+
329336
# Add OTel handler
330337
handler = LoggingHandler(
331338
level=logging.NOTSET, logger_provider=provider

opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,12 @@ def __init__(
528528
super().__init__(level=level)
529529
self._logger_provider = logger_provider or get_logger_provider()
530530

531+
warnings.warn(
532+
"`LoggingHandler` in `opentelemetry-sdk` is deprecated. Use the "
533+
"handler from `opentelemetry-instrumentation-logging` instead.",
534+
DeprecationWarning,
535+
)
536+
531537
@staticmethod
532538
def _get_attributes(record: logging.LogRecord) -> _ExtendedAttributes:
533539
attributes = {

opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,10 @@ def channel_credential_provider() -> grpc.ChannelCredentials:
714714
Default: False
715715
716716
Note: Logs SDK and its related settings are experimental.
717+
718+
.. warning::
719+
720+
This option is deprecated, instead you should install `opentelemetry-instrumentation-logging`.
717721
"""
718722

719723

opentelemetry-sdk/tests/logs/test_handler.py

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,15 @@
3838

3939
# pylint: disable=too-many-public-methods
4040
class TestLoggingHandler(unittest.TestCase):
41+
def test_warns_when_used(self):
42+
with self.assertWarnsRegex(
43+
DeprecationWarning,
44+
"`LoggingHandler` in `opentelemetry-sdk` is deprecated",
45+
):
46+
LoggingHandler()
47+
4148
def test_handler_default_log_level(self):
42-
processor, logger = set_up_test_logging(logging.NOTSET)
49+
processor, logger, handler = set_up_test_logging(logging.NOTSET)
4350

4451
# Make sure debug messages are ignored by default
4552
logger.debug("Debug message")
@@ -50,8 +57,10 @@ def test_handler_default_log_level(self):
5057
logger.warning("Warning message")
5158
self.assertEqual(processor.emit_count(), 1)
5259

60+
logger.removeHandler(handler)
61+
5362
def test_handler_custom_log_level(self):
54-
processor, logger = set_up_test_logging(logging.ERROR)
63+
processor, logger, handler = set_up_test_logging(logging.ERROR)
5564

5665
with self.assertLogs(level=logging.WARNING):
5766
logger.warning("Warning message test custom log level")
@@ -64,6 +73,8 @@ def test_handler_custom_log_level(self):
6473
logger.critical("No Time For Caution")
6574
self.assertEqual(processor.emit_count(), 2)
6675

76+
logger.removeHandler(handler)
77+
6778
# pylint: disable=protected-access
6879
def test_log_record_emit_noop(self):
6980
noop_logger_provder = NoOpLoggerProvider()
@@ -78,9 +89,10 @@ def test_log_record_emit_noop(self):
7889
with self.assertLogs(level=logging.WARNING):
7990
logger.warning("Warning message")
8091

92+
logger.removeHandler(handler_mock)
93+
8194
def test_log_flush_noop(self):
8295
no_op_logger_provider = NoOpLoggerProvider()
83-
no_op_logger_provider.force_flush = Mock()
8496

8597
logger = logging.getLogger("foo")
8698
handler = LoggingHandler(
@@ -91,11 +103,19 @@ def test_log_flush_noop(self):
91103
with self.assertLogs(level=logging.WARNING):
92104
logger.warning("Warning message")
93105

94-
logger.handlers[0].flush()
95-
no_op_logger_provider.force_flush.assert_not_called()
106+
# the LoggingHandler flush method will call the force_flush method of LoggerProvider in
107+
# a separate thread if present. NoOpLoggerProvider is not supposed to have that
108+
with patch(
109+
"opentelemetry.sdk._logs._internal.threading"
110+
) as threading_mock:
111+
logger.handlers[0].flush()
112+
113+
threading_mock.Thread.assert_not_called()
114+
115+
logger.removeHandler(handler)
96116

97117
def test_log_record_no_span_context(self):
98-
processor, logger = set_up_test_logging(logging.WARNING)
118+
processor, logger, handler = set_up_test_logging(logging.WARNING)
99119

100120
# Assert emit gets called for warning message
101121
with self.assertLogs(level=logging.WARNING):
@@ -115,18 +135,22 @@ def test_log_record_no_span_context(self):
115135
INVALID_SPAN_CONTEXT.trace_flags,
116136
)
117137

138+
logger.removeHandler(handler)
139+
118140
def test_log_record_observed_timestamp(self):
119-
processor, logger = set_up_test_logging(logging.WARNING)
141+
processor, logger, handler = set_up_test_logging(logging.WARNING)
120142

121143
with self.assertLogs(level=logging.WARNING):
122144
logger.warning("Warning message")
123145

124146
record = processor.get_log_record(0)
125147
self.assertIsNotNone(record.log_record.observed_timestamp)
126148

149+
logger.removeHandler(handler)
150+
127151
def test_log_record_user_attributes(self):
128152
"""Attributes can be injected into logs by adding them to the ReadWriteLogRecord"""
129-
processor, logger = set_up_test_logging(logging.WARNING)
153+
processor, logger, handler = set_up_test_logging(logging.WARNING)
130154

131155
# Assert emit gets called for warning message
132156
with self.assertLogs(level=logging.WARNING):
@@ -155,9 +179,11 @@ def test_log_record_user_attributes(self):
155179
isinstance(record.log_record.attributes, BoundedAttributes)
156180
)
157181

182+
logger.removeHandler(handler)
183+
158184
def test_log_record_exception(self):
159185
"""Exception information will be included in attributes"""
160-
processor, logger = set_up_test_logging(logging.ERROR)
186+
processor, logger, handler = set_up_test_logging(logging.ERROR)
161187

162188
try:
163189
raise ZeroDivisionError("division by zero")
@@ -189,9 +215,11 @@ def test_log_record_exception(self):
189215
self.assertTrue("division by zero" in stack_trace)
190216
self.assertTrue(__file__ in stack_trace)
191217

218+
logger.removeHandler(handler)
219+
192220
def test_log_record_recursive_exception(self):
193221
"""Exception information will be included in attributes even though it is recursive"""
194-
processor, logger = set_up_test_logging(logging.ERROR)
222+
processor, logger, handler = set_up_test_logging(logging.ERROR)
195223

196224
try:
197225
raise ZeroDivisionError(
@@ -224,9 +252,11 @@ def test_log_record_recursive_exception(self):
224252
self.assertTrue("division by zero" in stack_trace)
225253
self.assertTrue(__file__ in stack_trace)
226254

255+
logger.removeHandler(handler)
256+
227257
def test_log_exc_info_false(self):
228258
"""Exception information will not be included in attributes"""
229-
processor, logger = set_up_test_logging(logging.NOTSET)
259+
processor, logger, handler = set_up_test_logging(logging.NOTSET)
230260

231261
try:
232262
raise ZeroDivisionError("division by zero")
@@ -251,8 +281,10 @@ def test_log_exc_info_false(self):
251281
record.log_record.attributes,
252282
)
253283

284+
logger.removeHandler(handler)
285+
254286
def test_log_record_exception_with_object_payload(self):
255-
processor, logger = set_up_test_logging(logging.ERROR)
287+
processor, logger, handler = set_up_test_logging(logging.ERROR)
256288

257289
class CustomException(Exception):
258290
def __str__(self):
@@ -287,8 +319,10 @@ def __str__(self):
287319
self.assertTrue("CustomException" in stack_trace)
288320
self.assertTrue(__file__ in stack_trace)
289321

322+
logger.removeHandler(handler)
323+
290324
def test_log_record_trace_correlation(self):
291-
processor, logger = set_up_test_logging(logging.WARNING)
325+
processor, logger, handler = set_up_test_logging(logging.WARNING)
292326

293327
tracer = trace.TracerProvider().get_tracer(__name__)
294328
with tracer.start_as_current_span("test") as span:
@@ -325,8 +359,10 @@ def test_log_record_trace_correlation(self):
325359
span_context.trace_flags,
326360
)
327361

362+
logger.removeHandler(handler)
363+
328364
def test_log_record_trace_correlation_deprecated(self):
329-
processor, logger = set_up_test_logging(logging.WARNING)
365+
processor, logger, handler = set_up_test_logging(logging.WARNING)
330366

331367
tracer = trace.TracerProvider().get_tracer(__name__)
332368
with tracer.start_as_current_span("test") as span:
@@ -349,22 +385,28 @@ def test_log_record_trace_correlation_deprecated(self):
349385
record.log_record.trace_flags, span_context.trace_flags
350386
)
351387

388+
logger.removeHandler(handler)
389+
352390
def test_warning_without_formatter(self):
353-
processor, logger = set_up_test_logging(logging.WARNING)
391+
processor, logger, handler = set_up_test_logging(logging.WARNING)
354392
logger.warning("Test message")
355393

356394
record = processor.get_log_record(0)
357395
self.assertEqual(record.log_record.body, "Test message")
358396

397+
logger.removeHandler(handler)
398+
359399
def test_exception_without_formatter(self):
360-
processor, logger = set_up_test_logging(logging.WARNING)
400+
processor, logger, handler = set_up_test_logging(logging.WARNING)
361401
logger.exception("Test exception")
362402

363403
record = processor.get_log_record(0)
364404
self.assertEqual(record.log_record.body, "Test exception")
365405

406+
logger.removeHandler(handler)
407+
366408
def test_warning_with_formatter(self):
367-
processor, logger = set_up_test_logging(
409+
processor, logger, handler = set_up_test_logging(
368410
logging.WARNING,
369411
formatter=logging.Formatter(
370412
"%(name)s - %(levelname)s - %(message)s"
@@ -377,8 +419,10 @@ def test_warning_with_formatter(self):
377419
record.log_record.body, "foo - WARNING - Test message"
378420
)
379421

422+
logger.removeHandler(handler)
423+
380424
def test_log_body_is_always_string_with_formatter(self):
381-
processor, logger = set_up_test_logging(
425+
processor, logger, handler = set_up_test_logging(
382426
logging.WARNING,
383427
formatter=logging.Formatter(
384428
"%(name)s - %(levelname)s - %(message)s"
@@ -389,17 +433,21 @@ def test_log_body_is_always_string_with_formatter(self):
389433
record = processor.get_log_record(0)
390434
self.assertIsInstance(record.log_record.body, str)
391435

436+
logger.removeHandler(handler)
437+
392438
@patch.dict(os.environ, {"OTEL_SDK_DISABLED": "true"})
393439
def test_handler_root_logger_with_disabled_sdk_does_not_go_into_recursion_error(
394440
self,
395441
):
396-
processor, logger = set_up_test_logging(
442+
processor, logger, handler = set_up_test_logging(
397443
logging.NOTSET, root_logger=True
398444
)
399445
logger.warning("hello")
400446

401447
self.assertEqual(processor.emit_count(), 0)
402448

449+
logger.removeHandler(handler)
450+
403451
@patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "3"})
404452
def test_otel_attribute_count_limit_respected_in_logging_handler(self):
405453
"""Test that OTEL_ATTRIBUTE_COUNT_LIMIT is properly respected by LoggingHandler."""
@@ -439,6 +487,8 @@ def test_otel_attribute_count_limit_respected_in_logging_handler(self):
439487
f"Should have 10 dropped attributes, got {record.dropped_attributes}",
440488
)
441489

490+
logger.removeHandler(handler)
491+
442492
@patch.dict(os.environ, {OTEL_ATTRIBUTE_COUNT_LIMIT: "5"})
443493
def test_otel_attribute_count_limit_includes_code_attributes(self):
444494
"""Test that OTEL_ATTRIBUTE_COUNT_LIMIT applies to all attributes including code attributes."""
@@ -476,9 +526,11 @@ def test_otel_attribute_count_limit_includes_code_attributes(self):
476526
f"Should have 6 dropped attributes, got {record.dropped_attributes}",
477527
)
478528

529+
logger.removeHandler(handler)
530+
479531
def test_logging_handler_without_env_var_uses_default_limit(self):
480532
"""Test that without OTEL_ATTRIBUTE_COUNT_LIMIT, default limit (128) should apply."""
481-
processor, logger = set_up_test_logging(logging.WARNING)
533+
processor, logger, handler = set_up_test_logging(logging.WARNING)
482534

483535
# Create a log record with many attributes (more than default limit of 128)
484536
extra_attrs = {f"attr_{i}": f"value_{i}" for i in range(150)}
@@ -505,6 +557,8 @@ def test_logging_handler_without_env_var_uses_default_limit(self):
505557
f"Should have 25 dropped attributes, got {record.dropped_attributes}",
506558
)
507559

560+
logger.removeHandler(handler)
561+
508562

509563
def set_up_test_logging(level, formatter=None, root_logger=False):
510564
logger_provider = LoggerProvider()
@@ -515,7 +569,7 @@ def set_up_test_logging(level, formatter=None, root_logger=False):
515569
if formatter:
516570
handler.setFormatter(formatter)
517571
logger.addHandler(handler)
518-
return processor, logger
572+
return processor, logger, handler
519573

520574

521575
class FakeProcessor(LogRecordProcessor):

opentelemetry-sdk/tests/test_configurator.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,19 @@ def test_logging_init_exporter_without_handler_setup(self):
843843
getLogger(__name__).error("hello")
844844
self.assertFalse(provider.processors[0].exporter.export_called)
845845

846+
def test_logging_init_with_setup_logging_handler_to_true_warns(self):
847+
resource = Resource.create({})
848+
with self.assertWarnsRegex(
849+
DeprecationWarning,
850+
"and the `LoggingHandler` in `opentelemetry-sdk` that it controls are deprecated",
851+
):
852+
with ResetGlobalLoggingState():
853+
_init_logging(
854+
{"otlp": DummyOTLPLogExporter},
855+
resource=resource,
856+
setup_logging_handler=True,
857+
)
858+
846859
@patch.dict(
847860
environ,
848861
{"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"},

0 commit comments

Comments
 (0)