Skip to content

Commit ca15b57

Browse files
committed
opentelemetry-instrumentation-logging: don't add out of spec attributes by default (open-telemetry#4112)
* opentelemetry-instrumentation-logging: don't add out of spec attributes by default Add span context attributes to logging module LogRecord only if the instrumentation has been configured to do so via set_logging_format argument or OTEL_PYTHON_LOG_CORRELATION environment variable. * Add changelog * Update doc and cleanup tests * Reduce diff * Add missing test
1 parent a812ef2 commit ca15b57

3 files changed

Lines changed: 131 additions & 80 deletions

File tree

CHANGELOG.md

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

9292
### Breaking changes
9393

94+
- `opentelemetry-instrumentation-logging`: Inject span context attributes into logging LogRecord only if configured to do so
95+
([#4112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4112))
9496
- `opentelemetry-instrumentation-django`: Drop support for Django < 2.0
9597
([#3848](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4083))
9698

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

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,17 @@
9090
class LoggingInstrumentor(BaseInstrumentor): # pylint: disable=empty-docstring
9191
__doc__ = f"""An instrumentor for stdlib logging module.
9292
93-
This instrumentor injects tracing context into logging records and optionally sets the global logging format to the following:
93+
This instrumentor optionally injects tracing context into logging records and sets the global logging format to the following:
9494
9595
.. code-block::
9696
9797
{DEFAULT_LOGGING_FORMAT}
9898
9999
def log_hook(span: Span, record: LogRecord):
100-
if span and span.is_recording():
101-
record.custom_user_attribute_from_log_hook = "some-value"
100+
if span and span.is_recording():
101+
record.custom_user_attribute_from_log_hook = "some-value"
102+
span_ctx = span.get_span_context()
103+
record.from_sampled_span = span_ctx.trace_flags.sampled
102104
103105
Args:
104106
tracer_provider: Tracer provider instance that can be used to fetch a tracer.
@@ -130,32 +132,61 @@ def _instrument(self, **kwargs):
130132

131133
service_name = None
132134

133-
def record_factory(*args, **kwargs):
134-
record = old_factory(*args, **kwargs)
135+
set_logging_format = kwargs.get(
136+
"set_logging_format",
137+
environ.get(OTEL_PYTHON_LOG_CORRELATION, "false").lower()
138+
== "true",
139+
)
135140

136-
record.otelSpanID = "0"
137-
record.otelTraceID = "0"
138-
record.otelTraceSampled = False
141+
if set_logging_format:
142+
log_format = kwargs.get(
143+
"logging_format", environ.get(OTEL_PYTHON_LOG_FORMAT, None)
144+
)
145+
log_format = log_format or DEFAULT_LOGGING_FORMAT
146+
147+
log_level = kwargs.get(
148+
"log_level", LEVELS.get(environ.get(OTEL_PYTHON_LOG_LEVEL))
149+
)
150+
log_level = log_level or logging.INFO
151+
152+
logging.basicConfig(format=log_format, level=log_level)
139153

140-
nonlocal service_name
141-
if service_name is None:
142-
resource = getattr(provider, "resource", None)
143-
if resource:
144-
service_name = (
145-
resource.attributes.get("service.name") or ""
146-
)
147-
else:
148-
service_name = ""
154+
def record_factory(*args, **kwargs):
155+
record = old_factory(*args, **kwargs)
149156

150-
record.otelServiceName = service_name
157+
# this factory is a no-op if log correlation or log hook are not set
158+
if not set_logging_format and not callable(
159+
LoggingInstrumentor._log_hook
160+
):
161+
return record
162+
163+
# out of spec attributes are added to the log record only if log correlation is set
164+
if set_logging_format:
165+
record.otelSpanID = "0"
166+
record.otelTraceID = "0"
167+
record.otelTraceSampled = False
168+
169+
nonlocal service_name
170+
if service_name is None:
171+
resource = getattr(provider, "resource", None)
172+
if resource:
173+
service_name = (
174+
resource.attributes.get("service.name") or ""
175+
)
176+
else:
177+
service_name = ""
178+
179+
record.otelServiceName = service_name
151180

152181
span = get_current_span()
153182
if span != INVALID_SPAN:
154183
ctx = span.get_span_context()
155184
if ctx != INVALID_SPAN_CONTEXT:
156-
record.otelSpanID = format(ctx.span_id, "016x")
157-
record.otelTraceID = format(ctx.trace_id, "032x")
158-
record.otelTraceSampled = ctx.trace_flags.sampled
185+
if set_logging_format:
186+
record.otelSpanID = format(ctx.span_id, "016x")
187+
record.otelTraceID = format(ctx.trace_id, "032x")
188+
record.otelTraceSampled = ctx.trace_flags.sampled
189+
159190
if callable(LoggingInstrumentor._log_hook):
160191
try:
161192
LoggingInstrumentor._log_hook( # pylint: disable=E1102
@@ -168,25 +199,6 @@ def record_factory(*args, **kwargs):
168199

169200
logging.setLogRecordFactory(record_factory)
170201

171-
set_logging_format = kwargs.get(
172-
"set_logging_format",
173-
environ.get(OTEL_PYTHON_LOG_CORRELATION, "false").lower()
174-
== "true",
175-
)
176-
177-
if set_logging_format:
178-
log_format = kwargs.get(
179-
"logging_format", environ.get(OTEL_PYTHON_LOG_FORMAT, None)
180-
)
181-
log_format = log_format or DEFAULT_LOGGING_FORMAT
182-
183-
log_level = kwargs.get(
184-
"log_level", LEVELS.get(environ.get(OTEL_PYTHON_LOG_LEVEL))
185-
)
186-
log_level = log_level or logging.INFO
187-
188-
logging.basicConfig(format=log_format, level=log_level)
189-
190202
def _uninstrument(self, **kwargs):
191203
if LoggingInstrumentor._old_factory:
192204
logging.setLogRecordFactory(LoggingInstrumentor._old_factory)

instrumentation/opentelemetry-instrumentation-logging/tests/test_logging.py

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ def inject_fixtures(self, caplog):
4747

4848
def setUp(self):
4949
super().setUp()
50-
LoggingInstrumentor().instrument(tracer_provider=FakeTracerProvider())
50+
LoggingInstrumentor().instrument(
51+
tracer_provider=FakeTracerProvider(), set_logging_format=True
52+
)
5153

5254
def tearDown(self):
5355
super().tearDown()
@@ -94,16 +96,46 @@ def assert_trace_context_injected(self, span_id, trace_id, trace_sampled):
9496
self.assertEqual(record.otelTraceSampled, trace_sampled)
9597
self.assertEqual(record.otelServiceName, "unknown_service")
9698

97-
def test_trace_context_injection(self):
99+
@mock.patch.dict("os.environ", {"OTEL_PYTHON_LOG_CORRELATION": "true"})
100+
def test_trace_context_injection_with_log_correlation_from_env_var(self):
101+
LoggingInstrumentor().uninstrument()
102+
LoggingInstrumentor().instrument()
98103
with self.tracer.start_as_current_span("s1") as span:
99-
span_id = format(span.get_span_context().span_id, "016x")
100-
trace_id = format(span.get_span_context().trace_id, "032x")
101-
trace_sampled = span.get_span_context().trace_flags.sampled
104+
span_ctx = span.get_span_context()
105+
span_id = format(span_ctx.span_id, "016x")
106+
trace_id = format(span_ctx.trace_id, "032x")
107+
trace_sampled = span_ctx.trace_flags.sampled
102108
self.assert_trace_context_injected(
103109
span_id, trace_id, trace_sampled
104110
)
105111

112+
def test_trace_context_injection_with_log_correlation_instrument_arg(self):
113+
LoggingInstrumentor().uninstrument()
114+
LoggingInstrumentor().instrument(set_logging_format=True)
115+
with self.tracer.start_as_current_span("s1") as span:
116+
span_ctx = span.get_span_context()
117+
span_id = format(span_ctx.span_id, "016x")
118+
trace_id = format(span_ctx.trace_id, "032x")
119+
trace_sampled = span_ctx.trace_flags.sampled
120+
self.assert_trace_context_injected(
121+
span_id, trace_id, trace_sampled
122+
)
123+
124+
def test_no_trace_context_injection_by_default(self):
125+
with self.tracer.start_as_current_span("s1"):
126+
with self.caplog.at_level(level=logging.INFO):
127+
logger = logging.getLogger("test logger")
128+
logger.info("hello")
129+
self.assertEqual(len(self.caplog.records), 1)
130+
record = self.caplog.records[0]
131+
self.assertFalse(hasattr(record, "otelServiceName"))
132+
self.assertFalse(hasattr(record, "otelSpanID"))
133+
self.assertFalse(hasattr(record, "otelTraceID"))
134+
self.assertFalse(hasattr(record, "otelTraceSampled"))
135+
106136
def test_trace_context_injection_without_span(self):
137+
LoggingInstrumentor().uninstrument()
138+
LoggingInstrumentor().instrument(set_logging_format=True)
107139
self.assert_trace_context_injected("0", "0", False)
108140

109141
@mock.patch("logging.basicConfig")
@@ -113,15 +145,13 @@ def test_basic_config_called(self, basic_config_mock):
113145
self.assertFalse(basic_config_mock.called)
114146
LoggingInstrumentor().uninstrument()
115147

116-
env_patch = mock.patch.dict(
148+
with mock.patch.dict(
117149
"os.environ", {"OTEL_PYTHON_LOG_CORRELATION": "true"}
118-
)
119-
env_patch.start()
120-
LoggingInstrumentor().instrument()
121-
basic_config_mock.assert_called_with(
122-
format=DEFAULT_LOGGING_FORMAT, level=logging.INFO
123-
)
124-
env_patch.stop()
150+
):
151+
LoggingInstrumentor().instrument()
152+
basic_config_mock.assert_called_with(
153+
format=DEFAULT_LOGGING_FORMAT, level=logging.INFO
154+
)
125155

126156
@mock.patch("logging.basicConfig")
127157
def test_custom_format_and_level_env(self, basic_config_mock):
@@ -130,20 +160,18 @@ def test_custom_format_and_level_env(self, basic_config_mock):
130160
self.assertFalse(basic_config_mock.called)
131161
LoggingInstrumentor().uninstrument()
132162

133-
env_patch = mock.patch.dict(
163+
with mock.patch.dict(
134164
"os.environ",
135165
{
136166
"OTEL_PYTHON_LOG_CORRELATION": "true",
137167
"OTEL_PYTHON_LOG_FORMAT": "%(message)s %(otelSpanID)s",
138168
"OTEL_PYTHON_LOG_LEVEL": "error",
139169
},
140-
)
141-
env_patch.start()
142-
LoggingInstrumentor().instrument()
143-
basic_config_mock.assert_called_with(
144-
format="%(message)s %(otelSpanID)s", level=logging.ERROR
145-
)
146-
env_patch.stop()
170+
):
171+
LoggingInstrumentor().instrument()
172+
basic_config_mock.assert_called_with(
173+
format="%(message)s %(otelSpanID)s", level=logging.ERROR
174+
)
147175

148176
@mock.patch("logging.basicConfig")
149177
def test_custom_format_and_level_api(self, basic_config_mock): # pylint: disable=no-self-use
@@ -158,15 +186,35 @@ def test_custom_format_and_level_api(self, basic_config_mock): # pylint: disabl
158186
)
159187

160188
def test_log_hook(self):
189+
LoggingInstrumentor().uninstrument()
190+
LoggingInstrumentor().instrument(
191+
log_hook=log_hook,
192+
)
193+
with self.tracer.start_as_current_span("s1"):
194+
with self.caplog.at_level(level=logging.INFO):
195+
logger = logging.getLogger("test logger")
196+
logger.info("hello")
197+
self.assertEqual(len(self.caplog.records), 1)
198+
record = self.caplog.records[0]
199+
self.assertFalse(hasattr(record, "otelServiceName"))
200+
self.assertFalse(hasattr(record, "otelSpanID"))
201+
self.assertFalse(hasattr(record, "otelTraceID"))
202+
self.assertFalse(hasattr(record, "otelTraceSampled"))
203+
self.assertEqual(
204+
record.custom_user_attribute_from_log_hook, "some-value"
205+
)
206+
207+
def test_log_hook_with_set_logging_format(self):
161208
LoggingInstrumentor().uninstrument()
162209
LoggingInstrumentor().instrument(
163210
set_logging_format=True,
164211
log_hook=log_hook,
165212
)
166213
with self.tracer.start_as_current_span("s1") as span:
167-
span_id = format(span.get_span_context().span_id, "016x")
168-
trace_id = format(span.get_span_context().trace_id, "032x")
169-
trace_sampled = span.get_span_context().trace_flags.sampled
214+
span_ctx = span.get_span_context()
215+
span_id = format(span_ctx.span_id, "016x")
216+
trace_id = format(span_ctx.trace_id, "032x")
217+
trace_sampled = span_ctx.trace_flags.sampled
170218
with self.caplog.at_level(level=logging.INFO):
171219
logger = logging.getLogger("test logger")
172220
logger.info("hello")
@@ -181,34 +229,23 @@ def test_log_hook(self):
181229
)
182230

183231
def test_uninstrumented(self):
184-
with self.tracer.start_as_current_span("s1") as span:
185-
span_id = format(span.get_span_context().span_id, "016x")
186-
trace_id = format(span.get_span_context().trace_id, "032x")
187-
trace_sampled = span.get_span_context().trace_flags.sampled
188-
self.assert_trace_context_injected(
189-
span_id, trace_id, trace_sampled
190-
)
191-
192232
LoggingInstrumentor().uninstrument()
193-
194-
self.caplog.clear()
195-
with self.tracer.start_as_current_span("s1") as span:
196-
span_id = format(span.get_span_context().span_id, "016x")
197-
trace_id = format(span.get_span_context().trace_id, "032x")
198-
trace_sampled = span.get_span_context().trace_flags.sampled
233+
with self.tracer.start_as_current_span("s1"):
199234
with self.caplog.at_level(level=logging.INFO):
200235
logger = logging.getLogger("test logger")
201236
logger.info("hello")
202237
self.assertEqual(len(self.caplog.records), 1)
203238
record = self.caplog.records[0]
239+
self.assertFalse(hasattr(record, "otelServiceName"))
204240
self.assertFalse(hasattr(record, "otelSpanID"))
205241
self.assertFalse(hasattr(record, "otelTraceID"))
206-
self.assertFalse(hasattr(record, "otelServiceName"))
207242
self.assertFalse(hasattr(record, "otelTraceSampled"))
208243

209244
def test_no_op_tracer_provider(self):
210245
LoggingInstrumentor().uninstrument()
211-
LoggingInstrumentor().instrument(tracer_provider=NoOpTracerProvider())
246+
LoggingInstrumentor().instrument(
247+
tracer_provider=NoOpTracerProvider(), set_logging_format=True
248+
)
212249

213250
with self.caplog.at_level(level=logging.INFO):
214251
logger = logging.getLogger("test logger")

0 commit comments

Comments
 (0)