Skip to content

chore(logging): make log injection config a boolean #14032

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
persist-credentials: false
repository: 'DataDog/system-tests'
# Automatically managed, use scripts/update-system-tests-version to update
ref: '13b08f0faf7673656bd68dad5c142ad04eddee69'
ref: 'e79a35571dedbe2e9bc5f4731a3cf172f589b032'

- name: Checkout dd-trace-py
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down Expand Up @@ -96,7 +96,7 @@ jobs:
persist-credentials: false
repository: 'DataDog/system-tests'
# Automatically managed, use scripts/update-system-tests-version to update
ref: '13b08f0faf7673656bd68dad5c142ad04eddee69'
ref: 'e79a35571dedbe2e9bc5f4731a3cf172f589b032'

- name: Build runner
uses: ./.github/actions/install_runner
Expand Down Expand Up @@ -277,7 +277,7 @@ jobs:
persist-credentials: false
repository: 'DataDog/system-tests'
# Automatically managed, use scripts/update-system-tests-version to update
ref: '13b08f0faf7673656bd68dad5c142ad04eddee69'
ref: 'e79a35571dedbe2e9bc5f4731a3cf172f589b032'
- name: Checkout dd-trace-py
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
Expand Down
11 changes: 5 additions & 6 deletions ddtrace/_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class LogInjectionState(object):
# Log injection is enabled, but not yet configured
ENABLED = "true"
# Log injection is enabled and configured for structured logging
# This value is deprecated, but kept for backwards compatibility
STRUCTURED = "structured"


Expand Down Expand Up @@ -108,17 +109,15 @@ def set_log_formatting():
handler.setFormatter(logging.Formatter(DD_LOG_FORMAT))


def get_log_injection_state(raw_config: Optional[str]) -> str:
def get_log_injection_state(raw_config: Optional[str]) -> bool:
"""Returns the current log injection state."""
if raw_config:
normalized = raw_config.lower().strip()
if normalized == LogInjectionState.STRUCTURED:
return LogInjectionState.STRUCTURED
elif normalized in ("true", "1"):
return LogInjectionState.ENABLED
if normalized == LogInjectionState.STRUCTURED or normalized in ("true", "1"):
return True
elif normalized not in ("false", "0"):
logging.warning(
"Invalid log injection state '%s'. Expected 'true', 'false', or 'structured'. Defaulting to 'false'.",
normalized,
)
return LogInjectionState.DISABLED
return False
2 changes: 1 addition & 1 deletion ddtrace/_trace/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def apm_tracing_rc(lib_config, dd_config):
new_rc_configs["_trace_sampling_rules"] = trace_sampling_rules

if "log_injection_enabled" in lib_config:
new_rc_configs["_logs_injection"] = str(lib_config["log_injection_enabled"]).lower()
new_rc_configs["_logs_injection"] = lib_config["log_injection_enabled"]

if "tracing_tags" in lib_config:
tags = lib_config["tracing_tags"]
Expand Down
3 changes: 1 addition & 2 deletions ddtrace/contrib/internal/logbook/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import ddtrace
from ddtrace import config
from ddtrace._logger import LogInjectionState
from ddtrace.contrib.internal.trace_utils import unwrap as _u
from ddtrace.internal.utils import get_argument_value

Expand All @@ -27,7 +26,7 @@ def _supported_versions() -> Dict[str, str]:

def _w_process_record(func, instance, args, kwargs):
# patch logger to include datadog info before logging
if config._logs_injection != LogInjectionState.DISABLED:
if config._logs_injection:
record = get_argument_value(args, kwargs, 0, "record")
record.extra.update(ddtrace.tracer.get_log_correlation_context())
return func(*args, **kwargs)
Expand Down
11 changes: 4 additions & 7 deletions ddtrace/contrib/internal/logging/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import ddtrace
from ddtrace import config
from ddtrace._logger import LogInjectionState
from ddtrace._logger import set_log_formatting
from ddtrace.contrib.internal.trace_utils import unwrap as _u
from ddtrace.internal.constants import LOG_ATTR_ENV
Expand Down Expand Up @@ -54,15 +53,13 @@ def __init__(self, trace_id: int, span_id: int, service: str, version: str, env:
def _w_makeRecord(func, instance, args, kwargs):
# Get the LogRecord instance for this log
record = func(*args, **kwargs)
if config._logs_injection == LogInjectionState.DISABLED:
# log injection is opt-in for non-structured logging
return record
record.__dict__.update(ddtrace.tracer.get_log_correlation_context())
if config._logs_injection:
record.__dict__.update(ddtrace.tracer.get_log_correlation_context())
return record


def _w_StrFormatStyle_format(func, instance, args, kwargs):
if config._logs_injection != LogInjectionState.ENABLED:
if not config._logs_injection:
return func(*args, **kwargs)
# The format string "dd.service={dd.service}" expects
# the record to have a "dd" property which is an object that
Expand Down Expand Up @@ -103,7 +100,7 @@ def patch():
_w(logging.Logger, "makeRecord", _w_makeRecord)
_w(logging.StrFormatStyle, "_format", _w_StrFormatStyle_format)

if config._logs_injection == LogInjectionState.ENABLED:
if config._logs_injection:
# Only set the formatter is DD_LOGS_INJECTION is set to True. We do not want to modify
# unstructured logs if a user has not enabled logs injection.
# Also, the Datadog log format must be set after the logging module has been patched,
Expand Down
4 changes: 1 addition & 3 deletions ddtrace/contrib/internal/loguru/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import ddtrace
from ddtrace import config
from ddtrace._logger import LogInjectionState
from ddtrace.contrib.internal.trace_utils import unwrap as _u


Expand All @@ -25,12 +24,11 @@ def _supported_versions() -> Dict[str, str]:


def _tracer_injection(event_dict):
if config._logs_injection == LogInjectionState.DISABLED:
if not config._logs_injection:
# log injection is opt-out for structured logging
return event_dict
event_dd_attributes = ddtrace.tracer.get_log_correlation_context()
event_dict.update(event_dd_attributes)

return event_dd_attributes


Expand Down
6 changes: 2 additions & 4 deletions ddtrace/contrib/internal/structlog/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import ddtrace
from ddtrace import config
from ddtrace._logger import LogInjectionState
from ddtrace.contrib.internal.trace_utils import unwrap as _u
from ddtrace.contrib.internal.trace_utils import wrap as _w
from ddtrace.internal.utils import get_argument_value
Expand All @@ -27,9 +26,8 @@ def _supported_versions() -> Dict[str, str]:


def _tracer_injection(_, __, event_dict):
if config._logs_injection == LogInjectionState.DISABLED:
return event_dict
event_dict.update(ddtrace.tracer.get_log_correlation_context())
if config._logs_injection:
event_dict.update(ddtrace.tracer.get_log_correlation_context())
return event_dict


Expand Down
3 changes: 1 addition & 2 deletions ddtrace/settings/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from ddtrace.internal.telemetry import validate_otel_envs
from ddtrace.internal.utils.cache import cachedmethod

from .._logger import LogInjectionState
from .._logger import get_log_injection_state
from ..internal import gitmetadata
from ..internal.constants import _PROPAGATION_BEHAVIOR_DEFAULT
Expand Down Expand Up @@ -378,7 +377,7 @@ def _default_config() -> Dict[str, _ConfigItem]:
modifier=str,
),
"_logs_injection": _ConfigItem(
default=LogInjectionState.STRUCTURED,
default=True,
envs=["DD_LOGS_INJECTION"],
modifier=get_log_injection_state,
),
Expand Down
4 changes: 2 additions & 2 deletions tests/commands/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def test_info_no_configs():
b"Application Security enabled: False",
b"Remote Configuration enabled: False",
b"Debug logging: False",
b"Log injection enabled: structured",
b"Log injection enabled: True",
b"Health metrics enabled: False",
b"Partial flushing enabled: True",
b"Partial flush minimum number of spans: 300",
Expand Down Expand Up @@ -386,7 +386,7 @@ def test_info_w_configs():
b"Remote Configuration enabled: True",
b"IAST enabled (experimental)",
b"Debug logging: True",
b"Log injection enabled: true",
b"Log injection enabled: True",
b"Health metrics enabled: False",
b"Partial flushing enabled: True",
b"Partial flush minimum number of spans: 1000",
Expand Down
4 changes: 2 additions & 2 deletions tests/contrib/logging/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def test_log_strformat_style_format(self):
assert getattr(record, LOG_ATTR_SPAN_ID) == str(span.span_id)


@pytest.mark.parametrize("dd_logs_enabled", ["true", "false", "structured"])
@pytest.mark.parametrize("dd_logs_enabled", ["true", "structured"])
def test_manual_log_formatter_injection(dd_logs_enabled: str, run_python_code_in_subprocess):
code = """
import ddtrace.auto
Expand All @@ -321,7 +321,7 @@ def test_manual_log_formatter_injection(dd_logs_enabled: str, run_python_code_in
"""

env = os.environ.copy()
env["DD_LOGS_ENABLED"] = dd_logs_enabled
env["DD_LOGS_INJECTION"] = dd_logs_enabled
stdout, stderr, status, _ = run_python_code_in_subprocess(code, env=env)
assert status == 0, stderr

Expand Down
8 changes: 6 additions & 2 deletions tests/contrib/logging/test_tracer_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@


# example: '2022-06-10 21:49:26,010 CRITICAL [ddtrace] [test.py:15] - ddtrace critical log\n'
LOG_PATTERN = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} \w{1,} \[\S{1,}\] \[\w{1,}.\w{2}:\d{1,}\] - .{1,}$"
# example: '2025-07-16 16:27:02,708 CRITICAL [ddtrace] [test.py:8] [dd.service=ddtrace_subprocess_dir
# dd.env= dd.version= dd.trace_id=0 dd.span_id=0] - ddtrace critical log\n'
LOG_PATTERN = (
r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} \w{1,} \[\S{1,}\] \[\w{1,}\.\w{2}:\d{1,}\]( \[.*\])? - .{1,}$"
)


def assert_log_files(test_directory, test_log_file, total_file_count):
Expand Down Expand Up @@ -246,7 +250,7 @@ def test_warn_logs_can_go_to_file(run_python_code_in_subprocess, ddtrace_run_pyt
]:
out, err, status, pid = run_in_subprocess(code, env=env)
assert status == 0, err
assert err == b"warning log\n", err.decode()
assert b"warning log\n" in err, err.decode()
assert out == b"", out.decode()
with open(log_file) as file:
first_line = file.readline()
Expand Down
2 changes: 1 addition & 1 deletion tests/contrib/starlette/test_starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def test_incorrect_patching(run_python_code_in_subprocess):
out, err, status, _ = run_python_code_in_subprocess(code)
assert status == 0, err
assert out == b"", err
assert err == b"datadog context not present in ASGI request scope, trace middleware may be missing\n"
assert b"datadog context not present in ASGI request scope, trace middleware may be missing\n" in err, err


# Ignoring span link attributes until values are
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_standard_tags():
assert f.get("dd_version") == ""
assert f.get("debug") is False
assert f.get("enabled_cli") is False
assert f.get("log_injection_enabled") == "structured"
assert f.get("log_injection_enabled") is True
assert f.get("health_metrics_enabled") is False
assert f.get("runtime_metrics_enabled") is False
assert f.get("sampling_rules") == []
Expand Down Expand Up @@ -127,7 +127,7 @@ def test_env_config(self):
f = debug.collect(ddtrace.tracer)
assert f.get("agent_url") == "http://0.0.0.0:4321"
assert f.get("health_metrics_enabled") is True
assert f.get("log_injection_enabled") == "true"
assert f.get("log_injection_enabled") is True
assert f.get("env") == "prod"
assert f.get("dd_version") == "123456"
assert f.get("service") == "service"
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_setting_origin_environment(test_agent_session, run_python_code_in_subpr
} in events_trace_sample_rate

events_logs_injection_enabled = _get_telemetry_config_items(events, "DD_LOGS_INJECTION")
assert {"name": "DD_LOGS_INJECTION", "value": "true", "origin": "env_var"} in events_logs_injection_enabled
assert {"name": "DD_LOGS_INJECTION", "value": True, "origin": "env_var"} in events_logs_injection_enabled

events_trace_header_tags = _get_telemetry_config_items(events, "DD_TRACE_HEADER_TAGS")
assert {
Expand Down Expand Up @@ -80,7 +80,7 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess):
"""
from ddtrace import config, tracer

config._logs_injection = "false"
config._logs_injection = False
config._trace_http_header_tags = {"header": "value"}
config.tags = {"header": "value"}
config._tracing_enabled = False
Expand All @@ -98,7 +98,7 @@ def test_setting_origin_code(test_agent_session, run_python_code_in_subprocess):
events_logs_injection_enabled = _get_telemetry_config_items(events, "DD_LOGS_INJECTION")
assert {
"name": "DD_LOGS_INJECTION",
"value": "false",
"value": False,
"origin": "code",
} in events_logs_injection_enabled

Expand Down
10 changes: 5 additions & 5 deletions tests/internal/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def call_apm_tracing_rc(payloads: Sequence[Payload], g_config):
{
"expected": {
"_trace_sampling_rules": "",
"_logs_injection": "structured",
"_logs_injection": True,
"_trace_http_header_tags": {},
},
"expected_source": {
Expand Down Expand Up @@ -116,8 +116,8 @@ def call_apm_tracing_rc(payloads: Sequence[Payload], g_config):
"expected_source": {"_trace_sampling_rules": "remote_config"},
},
{
"env": {"DD_LOGS_INJECTION": "true"},
"expected": {"_logs_injection": "true"},
"env": {"DD_LOGS_INJECTION": "false"},
"expected": {"_logs_injection": False},
"expected_source": {"_logs_injection": "env_var"},
},
{
Expand Down Expand Up @@ -706,7 +706,7 @@ def test_remoteconfig_debug_logging():
"dd tags, tracing enablement, and HTTP header tags.\nConfigs on startup: "
"sampling_rules: %s, logs_injection: %s, tags: %s, tracing_enabled: %s, trace_http_header_tags: %s",
"",
"structured",
True,
{},
True,
{},
Expand All @@ -721,7 +721,7 @@ def test_remoteconfig_debug_logging():
mock.call(
"Updated HTTP header tags configuration via remote_config: %s", {"x-header-tag-420": "header_tag_420"}
),
mock.call("Updated logs injection configuration via remote_config: %s", "false"),
mock.call("Updated logs injection configuration via remote_config: %s", False),
# 3 payloads should be received generating 3 debug logs.
mock.call(
"APM Tracing Received: %s from the Agent",
Expand Down
4 changes: 2 additions & 2 deletions tests/telemetry/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def test_app_started_event(telemetry_writer, test_agent_session, mock_time):
{"name": "trace_sample_rate", "origin": "default", "value": "1.0"},
{"name": "trace_sampling_rules", "origin": "default", "value": ""},
{"name": "trace_header_tags", "origin": "default", "value": ""},
{"name": "logs_injection_enabled", "origin": "default", "value": "structured"},
{"name": "logs_injection_enabled", "origin": "default", "value": True},
{"name": "trace_tags", "origin": "default", "value": ""},
{"name": "trace_enabled", "origin": "default", "value": "true"},
{"name": "instrumentation_config_id", "origin": "default", "value": ""},
Expand Down Expand Up @@ -419,7 +419,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
{"name": "DD_LLMOBS_INSTRUMENTED_PROXY_URLS", "origin": "default", "value": None},
{"name": "DD_LLMOBS_ML_APP", "origin": "default", "value": None},
{"name": "DD_LLMOBS_SAMPLE_RATE", "origin": "default", "value": 1.0},
{"name": "DD_LOGS_INJECTION", "origin": "env_var", "value": "true"},
{"name": "DD_LOGS_INJECTION", "origin": "env_var", "value": True},
{"name": "DD_METRICS_OTEL_ENABLED", "origin": "default", "value": False},
{"name": "DD_PROFILING_AGENTLESS", "origin": "default", "value": False},
{"name": "DD_PROFILING_API_TIMEOUT", "origin": "default", "value": 10.0},
Expand Down
13 changes: 8 additions & 5 deletions tests/tracer/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import pytest

from ddtrace._logger import LogInjectionState
from ddtrace.settings import HttpConfig
from ddtrace.settings import IntegrationConfig
from ddtrace.settings._config import Config
Expand All @@ -14,20 +13,24 @@ class TestConfig(BaseTestCase):
def test_logs_injection(self):
with self.override_env(dict(DD_LOGS_INJECTION="True")):
config = Config()
self.assertEqual(config._logs_injection, LogInjectionState.ENABLED)
self.assertEqual(config._logs_injection, True)

with self.override_env(dict(DD_LOGS_INJECTION="FALSE")):
config = Config()
self.assertEqual(config._logs_injection, LogInjectionState.DISABLED)
self.assertEqual(config._logs_injection, False)

with self.override_env(dict(DD_LOGS_INJECTION="structured")):
config = Config()
self.assertEqual(config._logs_injection, True)

with self.override_env(dict(), replace_os_env=True):
config = Config()
self.assertEqual(config._logs_injection, LogInjectionState.STRUCTURED)
self.assertEqual(config._logs_injection, True)

with self.override_env(dict(DD_LOGS_INJECTION="nonsense")):
# If the value is not recognized, it should default to DISABLED
config = Config()
self.assertEqual(config._logs_injection, LogInjectionState.DISABLED)
self.assertEqual(config._logs_injection, False)

def test_service(self):
# If none is provided the default should be ``None``
Expand Down
2 changes: 0 additions & 2 deletions tests/tracer/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,8 +1091,6 @@ def test_enable():
)
def test_unfinished_span_warning_log():
"""Test that a warning log is emitted when the tracer is shut down with unfinished spans."""
import ddtrace.auto # noqa

from ddtrace.constants import MANUAL_KEEP_KEY
from ddtrace.trace import tracer

Expand Down
Loading