Skip to content

Commit e7f0f9e

Browse files
committed
Enabled log levele and format configuration for auto instrumentation
(SQUASH)
1 parent ac7329c commit e7f0f9e

File tree

4 files changed

+294
-5
lines changed

4 files changed

+294
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
([#4444](https://github.com/open-telemetry/opentelemetry-python/pull/4444))
1818
- Updated `tracecontext-integration-test` gitref to `d782773b2cf2fa4afd6a80a93b289d8a74ca894d`
1919
([#4448](https://github.com/open-telemetry/opentelemetry-python/pull/4448))
20+
- Enable configuration of logging format and level in auto-instrumentation
21+
([#4203](https://github.com/open-telemetry/opentelemetry-python/pull/4203))
2022

2123
## Version 1.30.0/0.51b0 (2025-02-03)
2224

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
4646
OTEL_TRACES_SAMPLER,
4747
OTEL_TRACES_SAMPLER_ARG,
48+
OTEL_LOG_LEVEL,
49+
OTEL_PYTHON_LOG_FORMAT,
4850
)
4951
from opentelemetry.sdk.metrics import MeterProvider
5052
from opentelemetry.sdk.metrics.export import (
@@ -87,6 +89,15 @@
8789

8890
_OTEL_SAMPLER_ENTRY_POINT_GROUP = "opentelemetry_traces_sampler"
8991

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

92103

@@ -130,6 +141,10 @@ def _get_sampler() -> Optional[str]:
130141
def _get_id_generator() -> str:
131142
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)
132143

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

134149
def _get_exporter_entry_point(
135150
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
@@ -251,10 +266,19 @@ def _init_logging(
251266
set_event_logger_provider(event_logger_provider)
252267

253268
if setup_logging_handler:
269+
# Log Handler
270+
root_logger = logging.getLogger()
254271
handler = LoggingHandler(
255-
level=logging.NOTSET, logger_provider=provider
272+
logger_provider=provider
256273
)
257-
logging.getLogger().addHandler(handler)
274+
# Log level
275+
if OTEL_LOG_LEVEL in environ:
276+
handler.setLevel(_get_log_level())
277+
# Log format
278+
if OTEL_PYTHON_LOG_FORMAT in environ:
279+
log_format = environ.get(OTEL_PYTHON_LOG_FORMAT, logging.BASIC_FORMAT)
280+
handler.setFormatter(logging.Formatter(log_format))
281+
root_logger.addHandler(handler)
258282

259283

260284
def _import_exporters(
@@ -447,7 +471,8 @@ class _OTelSDKConfigurator(_BaseConfigurator):
447471
448472
Initializes several crucial OTel SDK components (i.e. TracerProvider,
449473
MeterProvider, Processors...) according to a default implementation. Other
450-
Configurators can subclass and slightly alter this initialization.
474+
Configurators can subclass and slightly alter
475+
this initialization.
451476
452477
NOTE: This class should not be instantiated nor should it become an entry
453478
point on the `opentelemetry-sdk` package. Instead, distros should subclass

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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 256 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@
1414
# type: ignore
1515
# pylint: skip-file
1616

17-
from logging import WARNING, getLogger
17+
from logging import (
18+
DEBUG,
19+
ERROR,
20+
INFO,
21+
NOTSET,
22+
WARNING,
23+
getLogger,
24+
Formatter,
25+
)
1826
from os import environ
1927
from typing import Dict, Iterable, Optional, Sequence
2028
from unittest import TestCase, mock
@@ -31,6 +39,7 @@
3139
_EXPORTER_OTLP_PROTO_HTTP,
3240
_get_exporter_names,
3341
_get_id_generator,
42+
_get_log_level,
3443
_get_sampler,
3544
_import_config_components,
3645
_import_exporters,
@@ -73,6 +82,9 @@
7382
from opentelemetry.util.types import Attributes
7483

7584

85+
CUSTOM_LOG_FORMAT = "CUSTOM FORMAT %(levelname)s:%(name)s:%(message)s"
86+
87+
7688
class Provider:
7789
def __init__(self, resource=None, sampler=None, id_generator=None):
7890
self.sampler = sampler
@@ -663,9 +675,151 @@ def test_logging_init_exporter(self):
663675
getLogger(__name__).error("hello")
664676
self.assertTrue(provider.processor.exporter.export_called)
665677

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

692851
@patch.dict(
693852
environ,
@@ -839,6 +998,101 @@ def test_initialize_components_kwargs(
839998
True,
840999
)
8411000

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

8431097
class TestMetricsInit(TestCase):
8441098
def setUp(self):

0 commit comments

Comments
 (0)