feat(pytest): add _DD_CIVISIBILITY_LOG_INJECTION log prefix injection#18187
Draft
gnufede wants to merge 5 commits into
Draft
feat(pytest): add _DD_CIVISIBILITY_LOG_INJECTION log prefix injection#18187gnufede wants to merge 5 commits into
gnufede wants to merge 5 commits into
Conversation
When _DD_CIVISIBILITY_LOG_INJECTION is set, prepend [dd:trace_id,span_id] to every log message emitted during a test session so logs can be visually correlated with test spans without requiring full agentless log submission. Superseded (no-op) when DD_LOGS_INJECTION=true or DD_AGENTLESS_LOG_SUBMISSION_ENABLED=true, both of which already provide richer log correlation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codeowners resolved as |
|
The makeRecord wrapper was prepending [dd:...] to record.msg, which places it inside %(message)s — after any timestamp/level fields in the format string. Instead, stamp trace/span IDs as record attributes in makeRecord (where the test span is still active) and prepend to the fully-assembled output string in a Formatter.format wrapper, so the prefix is always the very first thing on each log line regardless of the user's format string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmat wrapper The makeRecord wrapper was added to snapshot trace/span IDs before the span could close, but Formatter.format is called synchronously in the same stack frame as the original logger.info() call so the span is always still active. Remove makeRecord and call get_log_correlation_context() directly inside the Formatter.format wrapper instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_log_correlation_context() returns the full 128-bit trace_id as a 32-char hex string for high-bit IDs, but the backend stores the test span's 64-bit integer (matching DDTraceTestContext's truncation). Switch to reading the active span directly and applying % (1 << 64) so the prefixed trace_id always matches the value in the test event payload. Also assign trace_id_raw/span_id_raw to local variables so mypy can narrow int|None → int for the modulo operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TestCILogInjection to test_pytest_log_correlation.py covering the six key behaviours of LogInjectionPatch: - [dd:trace_id,span_id] prefix appears at the start of every formatted log line when _DD_CIVISIBILITY_LOG_INJECTION=true - trace_id in the prefix equals span.trace_id % (1 << 64), matching the 64-bit integer the backend stores for the test event payload - All log lines within one test share the same trace_id/span_id - No prefix when the flag is not set - Patch is not installed (superseded) when DD_LOGS_INJECTION=true - Patch is not installed (superseded) when DD_AGENTLESS_LOG_SUBMISSION_ENABLED=true Each test runs a pytester subprocess so the env var is present during ddtrace plugin initialisation and the Formatter.format wrapper is confined to the subprocess process, with no risk of leaking into the outer test runner's logging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a new opt-in feature to the `ddtrace.testing` pytest plugin: when `_DD_CIVISIBILITY_LOG_INJECTION=true` is set, every log line emitted during a test session is prefixed with `[dd:trace_id,span_id]` at the very start of the line. This lets users visually correlate log output with the test span it came from without needing full agentless log submission.
Supersession: this feature is a no-op when either `DD_LOGS_INJECTION=true` or `DD_AGENTLESS_LOG_SUBMISSION_ENABLED=true` is set — both already provide richer log correlation and would conflict with a plain-text prefix.
Implementation: why `Formatter.format` and not `record.msg` or a root-logger `Filter`
Root-logger `Filter` doesn't work. The natural first attempt was `logging.getLogger().addFilter(filter)`. Python's `Logger.callHandlers()` propagates records to parent-logger handlers directly, bypassing parent-logger filters entirely. A filter on `logging.root` is only invoked for records that originate from the root logger itself (`logging.info(...)`), not from named child loggers like `logging.getLogger(name)`, which covers virtually all real test code.
Modifying `record.msg` in `makeRecord` puts the prefix in the wrong place. Injecting the prefix into `record.msg` embeds it inside `%(message)s`, which appears after any timestamp or level fields in the user's format string — e.g. `15:10:34 INFO mymodule - [dd:...] actual message`. That defeats the goal of having it at the very beginning of the line.
The fix: wrap `logging.Formatter.format`. `Formatter.format` returns the fully-assembled output string, so prepending there guarantees the prefix is always the first thing on the line regardless of the user's format string. Crucially, `Formatter.format` is called synchronously within the same call stack as the original `logger.info(...)` call, so the test span is still active when we read `get_log_correlation_context()` — no need to snapshot IDs earlier in `makeRecord`.
Why `LogInjectionPatch` lives in `ddtrace/testing/` and not in the logging integration
The existing `ddtrace/contrib/internal/logging/patch.py` was considered as a home for this logic, but it was kept separate for three reasons:
Because `_patch_logging()` is not called when only `_DD_CIVISIBILITY_LOG_INJECTION` is active, `LogInjectionPatch` reads the trace context directly via `ddtrace.tracer.get_log_correlation_context()` rather than relying on attributes injected by the logging patch.
Testing
Manual end-to-end test suite added at `customer_repros/ci_log_injection/` (not part of this PR). Covers:
Risks
Additional Notes
No release note — this is an internal/private env var, not a user-facing public API.