Skip to content

feat(logging): add opt-in session_id and trace_id correlation to JSON log records via contextvars - #33260

Closed
deepanshululla wants to merge 5 commits into
BerriAI:litellm_oss_stagingfrom
deepanshululla:feature/logging-contextvar-request-correlation
Closed

deepanshululla wants to merge 5 commits into
BerriAI:litellm_oss_stagingfrom
deepanshululla:feature/logging-contextvar-request-correlation

Conversation

@deepanshululla

@deepanshululla deepanshululla commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Relevant issues

Closes #31873

None

Linear ticket

None

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem

Screenshots / Proof of Fix

This PR adds structured logging fields and does not change LLM provider behavior; proof of fix is via the test suite (see Changes).

Type

Refactoring / New Feature

Changes

Adds opt-in request correlation to litellm's JSON logging. When litellm.request_correlation_in_logs = True, every JSON log record emitted through the standard litellm loggers will automatically include trace_id and session_id fields sourced from Python contextvars.

The implementation follows the standard Python logging pattern: a CorrelationContextFilter(logging.Filter) subclass stamps each LogRecord with the contextvar values before the formatter runs. JsonFormatter already has a loop that promotes arbitrary record.__dict__ entries to first-class JSON fields, so no formatter changes are needed. This mirrors the existing SecretRedactionFilter pattern in the same module.

set_trace_id() and set_session_id() are the public API for callers to bind correlation IDs. litellm.litellm_core_utils.litellm_logging.Logging.__init__() already calls both at request start, so every log line emitted during a litellm call inherits the correct IDs with no per-call-site wiring. Contextvars provide natural async isolation: concurrent tasks each see only their own IDs.

_correlation_filter is registered on the module-level stream handler, the JSON exception handler created by _setup_json_exception_handlers(), and any handler passed to _initialize_loggers_with_handler(), so coverage is complete regardless of which code path activates JSON logging.

The test helper _JsonCapture is updated to register CorrelationContextFilter on itself so that unit tests exercise the same filter path production code uses.

A Greptile review identified that set_session_id was only called when litellm_session_id was present in kwargs, meaning a prior request's session ID could persist in the contextvar across sequential calls in the same coroutine. This is fixed: Logging.__init__() now unconditionally calls set_session_id with an empty string as the fallback, so the var is always reset at the start of each request. The corresponding test was updated to assert the reset behavior rather than the previous (incorrect) preservation behavior.

… records via contextvars

Adds two ContextVar instances (session_id_var, trace_id_var) to litellm/_logging.py and
two setter functions (set_session_id, set_trace_id). Logging.__init__() now calls both
setters after assigning litellm_trace_id so every JSON log record emitted within the
async request context carries trace_id and, when provided, session_id — enabling log
correlation in Loki, CloudWatch Logs Insights, and other structured-log sinks without
any changes to individual log call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Deepanshu seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds opt-in session_id and trace_id correlation to JSON log records by introducing two module-level ContextVar[str] instances in litellm/_logging.py and wiring their setters into Logging.__init__(). The JsonFormatter reads the vars on every format() call and injects them as top-level fields only when non-empty and not already supplied via extra={}.

  • litellm/_logging.py: Two new ContextVars (session_id_var, trace_id_var) and public setters; JsonFormatter.format() extended to inject both fields with correct precedence guards.
  • litellm/litellm_core_utils/litellm_logging.py: set_trace_id called unconditionally and set_session_id called conditionally in Logging.__init__(); the conditional path for session_id leaves the var holding a stale value when no session ID is present (discussed in earlier thread).
  • tests/test_litellm/test_logging.py: Seven new tests covering injection, absence, and asyncio task isolation; the test asserting that a pre-existing session_id_var is preserved when no session is provided encodes the stale-value behavior rather than a reset (discussed in earlier thread).

Confidence Score: 4/5

Safe to merge for the proxy use-case, but the conditional set_session_id call leaves session_id_var carrying a stale value from a prior request when no session ID is provided — a cross-call contamination bug flagged in the earlier review thread that is still unresolved.

The ContextVar injection in _logging.py and the trace_id wiring are clean and correct. The concern is litellm_logging.py: skipping set_session_id when litellm_session_id is absent means any second sequential LLM call in the same coroutine without a session ID will emit log records tagged with the first call's session ID. The test that should catch this (test_logging_init_does_not_set_session_id_when_absent) instead asserts the contaminated state, meaning the bug will persist undetected.

litellm/litellm_core_utils/litellm_logging.py (the conditional set_session_id block) and tests/test_litellm/test_logging.py (test_logging_init_does_not_set_session_id_when_absent).

Important Files Changed

Filename Overview
litellm/_logging.py Adds two module-level ContextVars (session_id_var, trace_id_var) with setter helpers and extends JsonFormatter.format() to inject them into JSON log records when non-empty; properly guards with not in json_record to allow caller-supplied extra={} to take precedence.
litellm/litellm_core_utils/litellm_logging.py Wires set_trace_id (unconditional) and set_session_id (conditional on litellm_session_id being present in kwargs) into Logging.__init__(); the conditional set_session_id path leaves session_id_var holding a stale value from a prior call when no session ID is provided (already flagged in earlier review thread).
tests/test_litellm/test_logging.py Adds 7 tests covering ContextVar injection, absence, and asyncio task isolation; test_logging_init_does_not_set_session_id_when_absent asserts the cross-call contamination behaviour rather than the correct reset behaviour (already flagged in earlier review thread). Logger cleanup is also missing across tests.

Reviews (4): Last reviewed commit: "fix(logging): guard session_id/trace_id ..." | Re-trigger Greptile

Comment thread litellm/litellm_core_utils/litellm_logging.py Outdated
Comment thread tests/test_litellm/test_logging.py Outdated
@codecov

codecov Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@deepanshululla

Copy link
Copy Markdown
Contributor Author

@greptileai

@deepanshululla

Copy link
Copy Markdown
Contributor Author

Addressed the review comment in the latest commit (b42250a):

Greptile — overwrite guard for session_id/trace_id: added "session_id" not in json_record and "trace_id" not in json_record guards consistent with the existing guards for component and logger. This means a caller-supplied extra={"session_id": "...", "trace_id": "..."} in a logger.debug() call takes precedence over the ContextVar values, matching the expected behavior for all other extra fields.

@deepanshululla

Copy link
Copy Markdown
Contributor Author

@greptileai

1 similar comment
@deepanshululla

Copy link
Copy Markdown
Contributor Author

@greptileai

@deepanshululla

Copy link
Copy Markdown
Contributor Author

Addressed Greptile feedback: fixed the stale session_id_var cross-call contamination bug. When litellm_session_id is absent, Logging.init now always resets session_id_var to "" instead of leaving it with a prior request's value. Updated the test to assert the correct reset behavior and renamed it to test_logging_init_resets_session_id_to_empty_when_absent.

@deepanshululla

Copy link
Copy Markdown
Contributor Author

Both issues flagged in the Greptile review have been addressed.

Comment 1 (session_id_var cross-call leak): the fix was already committed as b0b13c9. The condition that gated set_session_id was replaced with an unconditional call that passes an empty string when no litellm_session_id is present — so a prior request's session ID never bleeds into a subsequent call's log records in the same coroutine or thread.

Comment 2 (test asserting the wrong value): the corresponding test test_logging_init_resets_session_id_to_empty_when_absent was updated in the same commit to assert session_id_var.get() == '' rather than the preexisting value, correctly capturing the intended reset behaviour as a regression guard.

@deepanshululla

Copy link
Copy Markdown
Contributor Author

Superseding this PR with a new one built on top of litellm_log_correlation (based on litellm_internal_staging) to keep the branch history aligned with the internal staging workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants