diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index c474f0640..35ea6f1f8 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -220,6 +220,8 @@ class Langfuse: Return `None` to leave the batch unchanged. Return `MaskOtelSpansResult` with `OtelSpanPatch` values to delete or replace attributes on selected spans. If a batch contains duplicate trace and span identifiers, Langfuse keeps only the last matching span. If the hook raises or returns an invalid batch result, Langfuse drops the whole export batch. If one returned span patch is invalid, Langfuse drops only that span from the Langfuse export. + Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` attributes written by the SDK are JSON-encoded strings, including on third-party instrumentation spans such as OpenInference (`acme` arrives as `'"acme"'`). Decode them with `json.loads` before applying masking rules and encode replacements with `json.dumps`, for example `OtelSpanPatch(set_attributes={key: json.dumps("[REDACTED]")})`. Values from other sources, such as baggage from an older upstream service, can be raw strings, so catch `json.JSONDecodeError` rather than letting the hook raise. See `MaskOtelSpansFunction` for a full example. + Example: ```python from typing import Optional diff --git a/langfuse/types.py b/langfuse/types.py index d20bd6a55..991b1e0a3 100644 --- a/langfuse/types.py +++ b/langfuse/types.py @@ -277,6 +277,48 @@ def mask_otel_spans( langfuse = Langfuse(mask_otel_spans=mask_otel_spans) ``` + + Values of `langfuse.trace.metadata.*` and `langfuse.observation.metadata.*` + attributes written by the SDK are JSON-encoded strings. This includes spans + from third-party instrumentations such as OpenInference, because propagated + metadata is copied onto them. A string value `acme` arrives as `'"acme"'` + and the number `3` as `'3'`. Decode each value with `json.loads` before + applying masking rules, and encode replacements with `json.dumps`. Values + that reach a span another way, such as baggage from an older or + non-Langfuse upstream service, can be raw strings. Guard the decode, + because an exception from the hook drops the whole export batch: + + ```python + import json + + METADATA_PREFIXES = ( + "langfuse.trace.metadata.", + "langfuse.observation.metadata.", + ) + + def mask_otel_spans( + *, params: MaskOtelSpansParams + ) -> Optional[MaskOtelSpansResult]: + patches = {} + + for identifier, span in params.spans.items(): + updates = {} + for key, value in span.attributes.items(): + if not key.startswith(METADATA_PREFIXES): + continue + if not isinstance(value, str): + continue + try: + decoded = json.loads(value) + except json.JSONDecodeError: + decoded = value + if decoded == "acme": + updates[key] = json.dumps("[REDACTED]") + if updates: + patches[identifier] = OtelSpanPatch(set_attributes=updates) + + return MaskOtelSpansResult(span_patches=patches) + ``` """ def __call__(