feat: flatten dict values passed to set_span_attribute - #21
Open
shreyas-n-harness wants to merge 3 commits into
Open
feat: flatten dict values passed to set_span_attribute#21shreyas-n-harness wants to merge 3 commits into
shreyas-n-harness wants to merge 3 commits into
Conversation
Passing a dict to set_span_attribute silently dropped the value: OTel rejects mapping attribute values outright. Callers had to json.dumps by hand, which left the backend with an opaque string that cannot be queried or aggregated per field. Dict values are now accepted and expanded into dot-notation attributes (agent.action=generate) so every leaf stays individually queryable. Nothing is serialized on the caller's thread: the dict is parked in a thread-safe registry keyed by span context and flattened by a new FlattenDictSpanProcessor at on_end. That processor sits outermost in the pipeline so GenAI scrubbing and attribute-based exclusion match on the flattened keys. Bounded by design — depth 3, 32 leaf attributes per dict, explicit attributes win on key collision — and disabled with HARNESS_SPAN_ATTRIBUTE_FLATTEN_ENABLED=false. Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
Only explicit false disables flattening; unset or any other env value leaves it on so customers get queryable flat keys without configuring HARNESS_SPAN_ATTRIBUTE_FLATTEN_ENABLED=true. Co-authored-by: Cursor <cursoragent@cursor.com>
| | Nesting deeper than 3 levels | JSON string at the depth-3 key | | ||
| | Flattened key already set on the span | skipped — the explicit value wins | | ||
|
|
||
| At most 32 leaf attributes are emitted per dictionary; the rest are dropped with a debug |
Collaborator
There was a problem hiding this comment.
this should be configurable?
Collaborator
Author
There was a problem hiding this comment.
Makes sense, updated
Add HARNESS_SPAN_ATTRIBUTE_FLATTEN_MAX_DEPTH and MAX_LEAVES (defaults 3 and 32) so callers can tune safety rails without disabling flattening. Co-authored-by: Cursor <cursoragent@cursor.com>
shreyas-n-harness
force-pushed
the
feat/span-attribute-dict-flatten
branch
from
August 6, 2026 08:27
aebe55c to
3c64a97
Compare
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.
Why
set_span_attribute("agent", {"action": "generate", "name": "devops"})silently dropped the value — OpenTelemetry rejects mapping attribute values outright. The workaround wasjson.dumps()at the call site, which left the backend with an opaque string that cannot be queried or aggregated per field.What
Dict (
Mapping) values are now accepted byset_span_attribute/set_span_attributesand expanded into dot-notation attributes, so every leaf stays individually queryable:Nothing is serialized on the caller's thread. The dict is parked in a thread-safe registry keyed by span context, and a new
FlattenDictSpanProcessordrains it aton_endand writes the flat keys onto the span. The processor sits outermost in the pipeline (wrappingGenAiPayloadScrubSpanProcessor) so payload scrubbing and attribute-based exclusion match on the flattened keys.The registry is keyed by span context rather than object identity because
Span.end()handson_enda freshReadableSpansnapshot, not the recordingSpanthe enrichment helper saw. It is also bounded (2048 spans, oldest evicted) so a span that never ends cannot leak its entry.Flatten rules
str/bool/int/floatleafNoneleafstr(value)At most 32 leaf attributes per dict; the remainder is dropped with a debug log.
Configuration
Flattening is enabled by default. No env var is required — dict values are flattened automatically. To disable, set an explicit opt-out:
HARNESS_SPAN_ATTRIBUTE_FLATTEN_ENABLEDfalsedisables flattening; any other value (including empty) leaves it enabled. When disabled, OTel rejects dict values as before.HARNESS_SPAN_ATTRIBUTE_FLATTEN_RAW_JSONtrueadditionally stores the whole dict as JSON under the original keyLegacy
HA_/AT_/TA_prefixes are also supported for both flags.Changes
src/harness_sdk/flatten_dict_registry.py— thread-safe, bounded span-to-pending-dicts registry plus env flag helpers (opt-out semantics)src/harness_sdk/flatten_dict_span_processor.py— decoratorSpanProcessorthat flattens aton_endsrc/harness_sdk/span_enrichment.py— acceptMappingvalues and route them to the registrysrc/harness_sdk/plugins/builtin/pipeline.py— wrap the chain outermost when flattening is enabledsrc/harness_sdk/agent_init.py— same wrapping for the console-exporter pathREADME.md/AGENTS.md/CLAUDE.md— document behavior, rules, and env flagsExisting non-dict behavior is unchanged: scalars and sequences still go straight to
span.set_attribute.Test plan
python -m pytest test/flatten_dict_span_processor_test.py test/span_enrichment_test.py test/plugins/builtin/test_pipeline.py -q— 32 passed./scripts/run-unit-tests.sh— 270 passed; the 13 failures (openai, grpc) reproduce identically onmainand are unrelatedpylint src/harness_sdk --disable=C,R --ignore-patterns=config_pb2.py— 10.00/10set_span_attribute("agent", {"action": "generate", "name": "devops"})exported flattened keys to QA ingest