Skip to content

Commit 526b640

Browse files
committed
Enabled log levele and format configuration for auto instrumentation
(SQUASH)
1 parent a96a3d6 commit 526b640

File tree

4 files changed

+294
-5
lines changed

4 files changed

+294
-5
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
([#4491](https://github.com/open-telemetry/opentelemetry-python/pull/4491))
1616
- api: Revert record `BaseException` change in `trace_api.use_span()`
1717
([#4494](https://github.com/open-telemetry/opentelemetry-python/pull/4494))
18+
- Enable configuration of logging format and level in auto-instrumentation
19+
([#4203](https://github.com/open-telemetry/opentelemetry-python/pull/4203))
1820

1921
## Version 1.31.0/0.52b0 (2025-03-12)
2022

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

+28-3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
4848
OTEL_TRACES_SAMPLER,
4949
OTEL_TRACES_SAMPLER_ARG,
50+
OTEL_LOG_LEVEL,
51+
OTEL_PYTHON_LOG_FORMAT,
5052
)
5153
from opentelemetry.sdk.metrics import MeterProvider
5254
from opentelemetry.sdk.metrics.export import (
@@ -89,6 +91,15 @@
8991

9092
_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler"
9193

94+
_OTEL_LOG_LEVEL_BY_NAME = {
95+
"notset": logging.NOTSET,
96+
"debug": logging.DEBUG,
97+
"info": logging.INFO,
98+
"warn": logging.WARNING,
99+
"warning": logging.WARNING,
100+
"error": logging.ERROR,
101+
}
102+
92103
_logger = logging.getLogger(__name__)
93104

94105

@@ -132,6 +143,10 @@ def _get_sampler() -> str | None:
132143
def _get_id_generator() -> str:
133144
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)
134145

146+
def _get_log_level() -> int:
147+
#TODO: Refactor to include env var check
148+
return _OTEL_LOG_LEVEL_BY_NAME.get(environ.get(OTEL_LOG_LEVEL, "notset").lower().strip(), logging.INFO)
149+
135150

136151
def _get_exporter_entry_point(
137152
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
@@ -253,10 +268,19 @@ def _init_logging(
253268
set_event_logger_provider(event_logger_provider)
254269

255270
if setup_logging_handler:
271+
# Log Handler
272+
root_logger = logging.getLogger()
256273
handler = LoggingHandler(
257-
level=logging.NOTSET, logger_provider=provider
274+
logger_provider=provider
258275
)
259-
logging.getLogger().addHandler(handler)
276+
# Log level
277+
if OTEL_LOG_LEVEL in environ:
278+
handler.setLevel(_get_log_level())
279+
# Log format
280+
if OTEL_PYTHON_LOG_FORMAT in environ:
281+
log_format = environ.get(OTEL_PYTHON_LOG_FORMAT, logging.BASIC_FORMAT)
282+
handler.setFormatter(logging.Formatter(log_format))
283+
root_logger.addHandler(handler)
260284

261285

262286
def _import_exporters(
@@ -449,7 +473,8 @@ class _OTelSDKConfigurator(_BaseConfigurator):
449473
450474
Initializes several crucial OTel SDK components (i.e. TracerProvider,
451475
MeterProvider, Processors...) according to a default implementation. Other
452-
Configurators can subclass and slightly alter this initialization.
476+
Configurators can subclass and slightly alter
477+
this initialization.
453478
454479
NOTE: This class should not be instantiated nor should it become an entry
455480
point on the `opentelemetry-sdk` package. Instead, distros should subclass

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

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@
5454
Default: "info"
5555
"""
5656

57+
OTEL_PYTHON_LOG_FORMAT = "OTEL_PYTHON_LOG_FORMAT"
58+
"""
59+
.. envvar:: OTEL_LOG_LEVEL
60+
61+
The :envvar:`OTEL_LOG_LEVEL` environment variable sets the log level used by the SDK logger
62+
Default: "logging.BASIC_FORMAT"
63+
"""
64+
5765
OTEL_TRACES_SAMPLER = "OTEL_TRACES_SAMPLER"
5866
"""
5967
.. envvar:: OTEL_TRACES_SAMPLER

opentelemetry-sdk/tests/test_configurator.py

+256-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
# pylint: skip-file
1616
from __future__ import annotations
1717

18-
from logging import WARNING, getLogger
18+
from logging import (
19+
DEBUG,
20+
ERROR,
21+
INFO,
22+
NOTSET,
23+
WARNING,
24+
getLogger,
25+
Formatter,
26+
)
1927
from os import environ
2028
from typing import Iterable, Optional, Sequence
2129
from unittest import TestCase, mock
@@ -32,6 +40,7 @@
3240
_EXPORTER_OTLP_PROTO_HTTP,
3341
_get_exporter_names,
3442
_get_id_generator,
43+
_get_log_level,
3544
_get_sampler,
3645
_import_config_components,
3746
_import_exporters,
@@ -74,6 +83,9 @@
7483
from opentelemetry.util.types import Attributes
7584

7685

86+
CUSTOM_LOG_FORMAT = "CUSTOM FORMAT %(levelname)s:%(name)s:%(message)s"
87+
88+
7789
class Provider:
7890
def __init__(self, resource=None, sampler=None, id_generator=None):
7991
self.sampler = sampler
@@ -665,9 +677,151 @@ def test_logging_init_exporter(self):
665677
getLogger(__name__).error("hello")
666678
self.assertTrue(provider.processor.exporter.export_called)
667679

680+
@patch.dict(environ, {}, clear=True)
681+
def test_otel_log_level_by_name_default(self):
682+
self.assertEqual(_get_log_level(), NOTSET)
683+
684+
@patch.dict(environ, {"OTEL_LOG_LEVEL": "NOTSET "}, clear=True)
685+
def test_otel_log_level_by_name_notset(self):
686+
self.assertEqual(_get_log_level(), NOTSET)
687+
688+
@patch.dict(environ, {"OTEL_LOG_LEVEL": " DeBug "}, clear=True)
689+
def test_otel_log_level_by_name_debug(self):
690+
self.assertEqual(_get_log_level(), DEBUG)
691+
692+
@patch.dict(environ, {"OTEL_LOG_LEVEL": " info "}, clear=True)
693+
def test_otel_log_level_by_name_info(self):
694+
self.assertEqual(_get_log_level(), INFO)
695+
696+
@patch.dict(environ, {"OTEL_LOG_LEVEL": " warn"}, clear=True)
697+
def test_otel_log_level_by_name_warn(self):
698+
self.assertEqual(_get_log_level(), WARNING)
699+
700+
@patch.dict(environ, {"OTEL_LOG_LEVEL": " warnING "}, clear=True)
701+
def test_otel_log_level_by_name_warning(self):
702+
self.assertEqual(_get_log_level(), WARNING)
703+
704+
@patch.dict(environ, {"OTEL_LOG_LEVEL": " eRroR"}, clear=True)
705+
def test_otel_log_level_by_name_error(self):
706+
self.assertEqual(_get_log_level(), ERROR)
707+
708+
668709
@patch.dict(
669710
environ,
670-
{"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"},
711+
{
712+
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
713+
"OTEL_LOG_LEVEL": "CUSTOM_LOG_LEVEL",
714+
},
715+
clear=True,
716+
)
717+
@patch("opentelemetry.sdk._configuration._get_log_level", return_value=39)
718+
def test_logging_init_exporter_level_under(self, log_level_mock):
719+
# log_level_mock.return_value = 39
720+
resource = Resource.create({})
721+
_init_logging(
722+
{"otlp": DummyOTLPLogExporter},
723+
resource=resource,
724+
)
725+
self.assertEqual(self.set_provider_mock.call_count, 1)
726+
provider = self.set_provider_mock.call_args[0][0]
727+
self.assertIsInstance(provider, DummyLoggerProvider)
728+
self.assertIsInstance(provider.resource, Resource)
729+
self.assertEqual(
730+
provider.resource.attributes.get("service.name"),
731+
"otlp-service",
732+
)
733+
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
734+
self.assertIsInstance(
735+
provider.processor.exporter, DummyOTLPLogExporter
736+
)
737+
getLogger(__name__).error("hello")
738+
self.assertTrue(provider.processor.exporter.export_called)
739+
root_logger = getLogger()
740+
handler_present = False
741+
for handler in root_logger.handlers:
742+
if isinstance(handler, LoggingHandler):
743+
handler_present = True
744+
self.assertEqual(handler.level, 39)
745+
self.assertTrue(handler_present)
746+
747+
@patch.dict(
748+
environ,
749+
{
750+
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
751+
"OTEL_LOG_LEVEL": "CUSTOM_LOG_LEVEL",
752+
},
753+
clear=True,
754+
)
755+
@patch("opentelemetry.sdk._configuration._get_log_level", return_value=41)
756+
def test_logging_init_exporter_level_over(self, log_level_mock):
757+
resource = Resource.create({})
758+
_init_logging(
759+
{"otlp": DummyOTLPLogExporter},
760+
resource=resource,
761+
)
762+
self.assertEqual(self.set_provider_mock.call_count, 1)
763+
provider = self.set_provider_mock.call_args[0][0]
764+
self.assertIsInstance(provider, DummyLoggerProvider)
765+
self.assertIsInstance(provider.resource, Resource)
766+
self.assertEqual(
767+
provider.resource.attributes.get("service.name"),
768+
"otlp-service",
769+
)
770+
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
771+
self.assertIsInstance(
772+
provider.processor.exporter, DummyOTLPLogExporter
773+
)
774+
getLogger(__name__).error("hello")
775+
self.assertFalse(provider.processor.exporter.export_called)
776+
root_logger = getLogger()
777+
handler_present = False
778+
for handler in root_logger.handlers:
779+
if isinstance(handler, LoggingHandler):
780+
handler_present = True
781+
self.assertEqual(handler.level, 41)
782+
self.assertTrue(handler_present)
783+
784+
@patch.dict(
785+
environ,
786+
{
787+
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
788+
"OTEL_PYTHON_LOG_FORMAT": CUSTOM_LOG_FORMAT,
789+
},
790+
)
791+
def test_logging_init_exporter_format(self):
792+
resource = Resource.create({})
793+
_init_logging(
794+
{"otlp": DummyOTLPLogExporter},
795+
resource=resource,
796+
)
797+
self.assertEqual(self.set_provider_mock.call_count, 1)
798+
provider = self.set_provider_mock.call_args[0][0]
799+
self.assertIsInstance(provider, DummyLoggerProvider)
800+
self.assertIsInstance(provider.resource, Resource)
801+
self.assertEqual(
802+
provider.resource.attributes.get("service.name"),
803+
"otlp-service",
804+
)
805+
self.assertIsInstance(provider.processor, DummyLogRecordProcessor)
806+
self.assertIsInstance(
807+
provider.processor.exporter, DummyOTLPLogExporter
808+
)
809+
getLogger(__name__).error("hello")
810+
self.assertTrue(provider.processor.exporter.export_called)
811+
root_logger = getLogger()
812+
self.assertEqual(root_logger.level, WARNING)
813+
handler_present = False
814+
for handler in root_logger.handlers:
815+
if isinstance(handler, LoggingHandler):
816+
self.assertEqual(handler.formatter._fmt, CUSTOM_LOG_FORMAT)
817+
handler_present = True
818+
self.assertTrue(handler_present)
819+
820+
@patch.dict(
821+
environ,
822+
{
823+
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service",
824+
},
671825
)
672826
def test_logging_init_exporter_without_handler_setup(self):
673827
resource = Resource.create({})
@@ -690,6 +844,11 @@ def test_logging_init_exporter_without_handler_setup(self):
690844
)
691845
getLogger(__name__).error("hello")
692846
self.assertFalse(provider.processor.exporter.export_called)
847+
root_logger = getLogger()
848+
self.assertEqual(root_logger.level, WARNING)
849+
for handler in root_logger.handlers:
850+
if isinstance(handler, LoggingHandler):
851+
self.fail()
693852

694853
@patch.dict(
695854
environ,
@@ -841,6 +1000,101 @@ def test_initialize_components_kwargs(
8411000
True,
8421001
)
8431002

1003+
@patch.dict(
1004+
environ,
1005+
{
1006+
"OTEL_TRACES_EXPORTER": _EXPORTER_OTLP,
1007+
"OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC,
1008+
"OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP,
1009+
},
1010+
)
1011+
@patch.dict(
1012+
environ,
1013+
{
1014+
"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service, custom.key.1=env-value",
1015+
"OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED": "False",
1016+
},
1017+
)
1018+
@patch("opentelemetry.sdk._configuration.Resource")
1019+
@patch("opentelemetry.sdk._configuration._import_exporters")
1020+
@patch("opentelemetry.sdk._configuration._get_exporter_names")
1021+
@patch("opentelemetry.sdk._configuration._init_tracing")
1022+
@patch("opentelemetry.sdk._configuration._init_logging")
1023+
@patch("opentelemetry.sdk._configuration._init_metrics")
1024+
def test_initialize_components_kwargs_disable_logging_handler(
1025+
self,
1026+
metrics_mock,
1027+
logging_mock,
1028+
tracing_mock,
1029+
exporter_names_mock,
1030+
import_exporters_mock,
1031+
resource_mock,
1032+
):
1033+
exporter_names_mock.return_value = [
1034+
"env_var_exporter_1",
1035+
"env_var_exporter_2",
1036+
]
1037+
import_exporters_mock.return_value = (
1038+
"TEST_SPAN_EXPORTERS_DICT",
1039+
"TEST_METRICS_EXPORTERS_DICT",
1040+
"TEST_LOG_EXPORTERS_DICT",
1041+
)
1042+
resource_mock.create.return_value = "TEST_RESOURCE"
1043+
kwargs = {
1044+
"auto_instrumentation_version": "auto-version",
1045+
"trace_exporter_names": ["custom_span_exporter"],
1046+
"metric_exporter_names": ["custom_metric_exporter"],
1047+
"log_exporter_names": ["custom_log_exporter"],
1048+
"sampler": "TEST_SAMPLER",
1049+
"resource_attributes": {
1050+
"custom.key.1": "pass-in-value-1",
1051+
"custom.key.2": "pass-in-value-2",
1052+
},
1053+
"id_generator": "TEST_GENERATOR",
1054+
}
1055+
_initialize_components(**kwargs)
1056+
1057+
import_exporters_mock.assert_called_once_with(
1058+
[
1059+
"custom_span_exporter",
1060+
"env_var_exporter_1",
1061+
"env_var_exporter_2",
1062+
],
1063+
[
1064+
"custom_metric_exporter",
1065+
"env_var_exporter_1",
1066+
"env_var_exporter_2",
1067+
],
1068+
[
1069+
"custom_log_exporter",
1070+
"env_var_exporter_1",
1071+
"env_var_exporter_2",
1072+
],
1073+
)
1074+
resource_mock.create.assert_called_once_with(
1075+
{
1076+
"telemetry.auto.version": "auto-version",
1077+
"custom.key.1": "pass-in-value-1",
1078+
"custom.key.2": "pass-in-value-2",
1079+
}
1080+
)
1081+
# Resource is checked separates
1082+
tracing_mock.assert_called_once_with(
1083+
exporters="TEST_SPAN_EXPORTERS_DICT",
1084+
id_generator="TEST_GENERATOR",
1085+
sampler="TEST_SAMPLER",
1086+
resource="TEST_RESOURCE",
1087+
)
1088+
metrics_mock.assert_called_once_with(
1089+
"TEST_METRICS_EXPORTERS_DICT",
1090+
"TEST_RESOURCE",
1091+
)
1092+
logging_mock.assert_called_once_with(
1093+
"TEST_LOG_EXPORTERS_DICT",
1094+
"TEST_RESOURCE",
1095+
False,
1096+
)
1097+
8441098

8451099
class TestMetricsInit(TestCase):
8461100
def setUp(self):

0 commit comments

Comments
 (0)