Skip to content

fix: serialize guardrail_response to JSON in OTEL traces#28362

Merged
yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
litellm_fix/serialize-guardrail-response-otel
May 21, 2026
Merged

fix: serialize guardrail_response to JSON in OTEL traces#28362
yassin-berriai merged 2 commits into
litellm_internal_stagingfrom
litellm_fix/serialize-guardrail-response-otel

Conversation

@yassin-berriai
Copy link
Copy Markdown
Contributor

@yassin-berriai yassin-berriai commented May 20, 2026

Summary

Guardrail OTEL spans previously set guardrail_response via safe_set_attribute, letting dict payloads reach the exporter as Python repr strings (e.g. "{'id': 'modr-7740', ...}"). Downstream log pipelines could not parse those as JSON, breaking metric creation from guardrail traces.

Serialize guardrail_response with safe_dumps before setting the span attribute — same treatment masked_entity_count already gets two lines above. Drop the attribute entirely when the value is None.

Resolves LIT-3233

Changes

  • litellm/integrations/opentelemetry.py — JSON-serialize guardrail_response, skip on None.
  • tests/test_litellm/integrations/test_opentelemetry.py — update both test_create_guardrail_span_with_valid_info assertions to expect safe_dumps("filtered_content").

Test plan

  • uv run pytest tests/test_litellm/integrations/test_opentelemetry.py -v — 219 passed
  • uv run ruff check litellm/ — clean
  • uv run black --check on both files — clean
  • uv run mypy litellm/integrations/opentelemetry.py --ignore-missing-imports — clean
  • @greptileai review with confidence score >= 4/5

🤖 Generated with Claude Code

@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.
You have signed the CLA already but the status is still pending? Let us recheck it.

@yassin-berriai
Copy link
Copy Markdown
Contributor Author

@greptileai please review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR fixes guardrail_response in OTEL guardrail spans being stored as a Python repr string instead of valid JSON, which broke downstream log pipeline parsing. It serializes the value with safe_dumps (matching the existing treatment of masked_entity_count) and skips the attribute entirely when the value is None.

  • opentelemetry.py: Replaces safe_set_attribute with safe_dumps + direct set_attribute for guardrail_response, guarded by a None check.
  • test_opentelemetry.py: Updates two existing assertions to expect JSON-serialized values; adds test_guardrail_response_dict_is_json_serialized and test_guardrail_response_none_is_skipped to cover the primary bug case and the None-skip behaviour.

Confidence Score: 5/5

Safe to merge — the change is a one-liner in a non-critical observability path, consistent with the existing masked_entity_count pattern, and covered by targeted new tests.

The fix is isolated to the guardrail OTEL span writer, follows an already-established pattern in the same method, introduces no new dependencies, and is backed by two new tests that directly exercise the corrected behaviour plus updated assertions on the existing ones.

No files require special attention.

Important Files Changed

Filename Overview
litellm/integrations/opentelemetry.py Switches guardrail_response from safe_set_attribute to safe_dumps + direct set_attribute, matching the existing pattern for masked_entity_count; adds a None guard to omit the attribute when absent.
tests/test_litellm/integrations/test_opentelemetry.py Updates two existing assertions to expect safe_dumps-serialized values; adds two new focused tests covering dict payloads and the None-skipping behaviour.

Reviews (3): Last reviewed commit: "test: cover dict-serialization and None-..." | Re-trigger Greptile

Comment on lines +1614 to +1618
guardrail_response = guardrail_information.get("guardrail_response")
if guardrail_response is not None:
guardrail_span.set_attribute(
"guardrail_response", safe_dumps(guardrail_response)
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 String values are double-encoded by safe_dumps. When guardrail_response is already a plain string (e.g. "filtered_content"), safe_dumps wraps it in JSON quotes producing '"filtered_content"'. Downstream consumers reading the attribute expecting a bare string will now get an extra layer of quoting. The primary bug was with dict/object payloads; strings didn't need to be re-serialized. Consider only applying safe_dumps when the value is not already a str.

Suggested change
guardrail_response = guardrail_information.get("guardrail_response")
if guardrail_response is not None:
guardrail_span.set_attribute(
"guardrail_response", safe_dumps(guardrail_response)
)
guardrail_response = guardrail_information.get("guardrail_response")
if guardrail_response is not None:
guardrail_span.set_attribute(
"guardrail_response",
guardrail_response if isinstance(guardrail_response, str) else safe_dumps(guardrail_response),
)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR fixes guardrail_response being written to OTEL spans as a Python repr string (via str(dict)) instead of valid JSON, by applying safe_dumps — the same treatment already used for masked_entity_count two lines above. It also drops the attribute entirely when the value is None, which is an improvement over the old path where None would have been serialized as the string "None".

  • opentelemetry.py: Replaces safe_set_attribute(guardrail_response) with a None-guarded guardrail_span.set_attribute("guardrail_response", safe_dumps(guardrail_response)).
  • test_opentelemetry.py: Two assertion sites updated from the bare string to safe_dumps("filtered_content"), correctly reflecting that all guardrail_response values — including plain strings — are now JSON-encoded before being stored in the span.

Confidence Score: 5/5

Safe to merge — a small, targeted serialization fix with no side effects on the broader request path.

The change is minimal and self-contained: one guard-and-serialize block replaces one safe_set_attribute call, matching the pattern already used for masked_entity_count in the same function. The None guard is strictly better than the previous behavior where None would surface as the string "None". Test assertions were updated correctly and no coverage was weakened.

No files require special attention.

Important Files Changed

Filename Overview
litellm/integrations/opentelemetry.py Replaces safe_set_attribute (which produced Python repr strings via str()) with safe_dumps for guardrail_response, and skips the attribute when the value is None — consistent with the existing masked_entity_count pattern two lines above.
tests/test_litellm/integrations/test_opentelemetry.py Two test assertions updated to expect safe_dumps("filtered_content") instead of the bare string, accurately reflecting the new serialization behavior; no test coverage weakened.

Reviews (2): Last reviewed commit: "fix: serialize guardrail_response to JSO..." | Re-trigger Greptile

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

yassin-berriai pushed a commit that referenced this pull request May 20, 2026
Address Greptile feedback on #28362 — add explicit coverage for the
two behavioral guarantees of this fix:

- Dict payloads (the OpenAI moderation case in the report) reach the
  span as a JSON string, not a Python repr.
- ``None`` guardrail_response skips the attribute entirely, so no
  ``"null"`` leaks into traces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yassin-berriai
Copy link
Copy Markdown
Contributor Author

@greptileai I addressed your feedback by adding two new tests: one verifying that dict payloads (the OpenAI moderation case from the report) are JSON-serialized, and one verifying that None skips the attribute entirely. The double-quoting of plain-string responses is intentional — the goal of the fix is that the attribute is always valid JSON so downstream pipelines can parse it uniformly. Please re-review.

@yassin-berriai yassin-berriai enabled auto-merge (squash) May 20, 2026 16:17
yassinkortam and others added 2 commits May 20, 2026 14:55
Guardrail spans previously set the `guardrail_response` attribute via
`safe_set_attribute`, which let dict payloads reach the OTEL exporter as
Python repr strings. Downstream log pipelines could not parse those as
JSON, breaking metric creation from guardrail traces.

Serialize `guardrail_response` with `safe_dumps` before setting the
attribute, matching how `masked_entity_count` is already handled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Greptile feedback on #28362 — add explicit coverage for the
two behavioral guarantees of this fix:

- Dict payloads (the OpenAI moderation case in the report) reach the
  span as a JSON string, not a Python repr.
- ``None`` guardrail_response skips the attribute entirely, so no
  ``"null"`` leaks into traces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yassin-berriai yassin-berriai force-pushed the litellm_fix/serialize-guardrail-response-otel branch from 152bcd2 to e4946b5 Compare May 20, 2026 21:55
@yassin-berriai yassin-berriai merged commit 35520ad into litellm_internal_staging May 21, 2026
112 of 115 checks passed
lorenzbaraldi pushed a commit to lorenzbaraldi/litellm that referenced this pull request May 21, 2026
* fix: serialize guardrail_response to JSON in OTEL traces

Guardrail spans previously set the `guardrail_response` attribute via
`safe_set_attribute`, which let dict payloads reach the OTEL exporter as
Python repr strings. Downstream log pipelines could not parse those as
JSON, breaking metric creation from guardrail traces.

Serialize `guardrail_response` with `safe_dumps` before setting the
attribute, matching how `masked_entity_count` is already handled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: cover dict-serialization and None-skip for guardrail_response

Address Greptile feedback on BerriAI#28362 — add explicit coverage for the
two behavioral guarantees of this fix:

- Dict payloads (the OpenAI moderation case in the report) reach the
  span as a JSON string, not a Python repr.
- ``None`` guardrail_response skips the attribute entirely, so no
  ``"null"`` leaks into traces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yassin Kortam <yassinkortam@g.ucla.edu>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

4 participants