Uh oh!
There was an error while loading. Please reload this page.
feat(ingest): stamp AI vendor + session attributes on spans - #517
Merged
Conversation
Classify each span against the known AI agent/framework vendors at ingest and stamp the result into span_attributes: - maple.ai.vendor.id: vendor slug (22 vendors + unknown:* dialect buckets) - maple.ai.vendor.version: identified vendor version, currently always "0" - maple.ai.session.id: the vendor's own session identifier, verbatim Detection is ordered first-match over per-vendor predicates on scope name, span name, span/resource attributes, and event attribute keys. Session IDs are taken only from session-granularity keys; vendors whose instrumentation only emits run/user-scoped IDs get no session attribute. No new columns, indexes, or materialized views - plain span attributes only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the AI vendor/session stamping from the trace row encoder to decode-time enrichment (enrich_trace_request), alongside the maple_org_id resource enrichment. The stamps now live inside the decoded OTLP payload itself, so they reach both write paths: the native row encoder picks them up as ordinary span attributes, and the forward-to-collector path carries them in the re-encoded payload. Also renames the namespace from maple.ai.* to maple_ai.*, and strips any customer-supplied maple_ai.* keys before stamping - the gateway is the authority for this namespace, matching the org enrichment semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…path The first cut rescanned the attribute list once per predicate clause (~30-40 linear scans per span), costing ~220ns on a typical non-AI HTTP span and ~236ns/span mean on a realistic 95%-non-AI batch - far over the ~50ns/span budget for the ingest hot path. Restructure classification around precomputed facts: - Scope and resource facts are computed once per ScopeSpans/ResourceSpans and shared by every span under them. - Each span gets exactly one pass over its attributes: a first-byte dispatch fills a SpanEvidence struct (prefix bits, presence bits, the five value slots predicates compare). The maple_ai.* namespace strip is folded into the same pass and only pays for itself when such a key exists. - Spans with no evidence (the overwhelming majority) exit before any vendor predicate runs; span-name-only detection (haystack, effect_ai) is gated by a first-byte check. - The 22 vendor predicates become pure field reads over the facts, keeping the ordered first-match semantics byte-for-byte (all existing tests pass unchanged). criterion bench (benches/ai_session_bench.rs), Apple Silicon dev machine: non-AI http span, 12 attrs 219ns -> 21ns non-AI db span, 8 attrs 154ns -> 18ns non-AI span, 30 attrs 463ns -> 44ns mastra / vercel / claude 56 / 281 / 23ns -> 46 / 71 / 35ns mixed 100-span stamp path 236ns/span -> 24ns/span Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The root clippy.toml declared lint levels in [workspace.lints.*] tables, which clippy.toml does not support (and apps/ingest is a standalone crate, so workspace lint tables would not apply anyway) - clippy silently ignored the whole file. Split it by mechanism: - Lint LEVELS move into apps/ingest/Cargo.toml [lints.rust]/[lints.clippy], verbatim except string_to_string, which clippy 1.95 removed in favor of implicit_clone. - clippy.toml keeps actual clippy CONFIG: allow unwrap/expect/panic in tests, since those lints target production code and tests assert with them by design. Fallout fixes to keep the build green and the new code clean: - telemetry.rs: the two #[allow(clippy::too_many_arguments)] become #[expect(..., reason)] as allow_attributes now demands. - ai_session.rs + bench: zero warnings under the new lints. Mostly mechanical (to_owned, map_or, semicolons, Debug derive); the deliberate shapes get #[expect] with reasons (evidence-flag structs, the first-byte dispatch table, crewai span-name suffixes misread as file extensions). Pre-existing code still carries ~370 unique warnings (str_to_string alone is ~250); left for a separate cleanup pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…an trace Second optimization round, targeting a per-TRACE budget of 50ns (20+ spans per average trace). Changes, each benched: - Two-byte key screen: a const-built 256 x u64 table maps a key's first byte to a bitmask of admissible second bytes. A non-AI key is rejected with one table load and a bit test - no string comparisons, and none of the comparison literals in the loop. First-byte-only screening still sent http.* into the haystack. probe and server.* into three s-probes; the pair test is what kills those. - Store-free phase 1: the per-span hot loop is now a read-only screen pass (screen_hits) with no SpanEvidence init and no stores. Only a screen hit (superset of the real patterns, false positives possible) pays for the phase-2 absorb into SpanEvidence, which is out-of-line so its comparison literals stay off the hot path. - Same screen construction for scope names and span names. - AI-span stamp path: one Vec::reserve(3) instead of up to three doubling reallocs, and the session String moves into its attribute instead of being copied again. criterion (Apple Silicon dev machine), vs the first optimization round: non-AI http 12 attrs 21ns -> 14.1ns non-AI db 8 attrs 18ns -> 12.3ns non-AI 30 attrs 44ns -> 21.8ns mixed 100-span batch 24ns -> 21.9ns/span 20-span non-AI trace (new) ~480ns -> 136.5ns (~6.8ns/span) 20-span trace w/ 2 AI (new) -> 652ns The 50ns/trace target is not reachable under frozen detection semantics: nearly every vendor has scope-free span-attribute clauses, so every span's attribute keys must be screened, and ~240 attrs/trace x ~0.5ns (String pointer chase + table test) is the measured floor. Closing the gap needs a semantic relaxation - e.g. a trust list of known non-AI instrumentation scopes that skips span scanning - deliberately not taken here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # apps/ingest/src/telemetry.rs
The origin/main merge auto-duplicated the identical [lints.rust]/ [lints.clippy] blocks in Cargo.toml, which cargo rejects as a duplicate key; the dedup was applied locally but missed the merge commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bs/maple into ai2/02-session-write
🍁 Maple PR previewNote Preview resources were removed when this pull request closed. Final commit |
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 freeto 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.
What
The write side of AI agent session tracking, entirely in the Rust ingest gateway. Each span is classified against the known AI agent/framework vendors and, on a match, stamped with:
maple_ai.vendor.idmaple_ai.vendor.version"0"for nowmaple_ai.session.idNon-AI spans are untouched.
How
Stamping happens at decode time (
enrich_trace_request), alongside themaple_org_idresource enrichment, by appending the attributes to each span in the decoded OTLP request. Because it runs before the write path forks, the stamps reach both destinations: the native row encoder picks them up as ordinary span attributes, and the forward-to-collector path carries them inside the re-encoded payload. Customer-suppliedmaple_ai.*keys are stripped first — the gateway is the authority for the namespace, same as formaple_org_id.apps/ingest/src/ai_session.rsholds an ordered first-match table of per-vendor detection predicates over scope name, span name, span/resource attributes, and event attribute keys — covering claude_agent_sdk, dspy, eve, flue, google_adk, haystack, langchain, litellm, llamaindex, mastra, agno, microsoft_agent_framework, openai_agents_sdk, openinference-openai, crewai, pydantic_ai, semantic_kernel, smolagents, strands, effect_ai, spring_ai, and vercel_ai_sdk, plus genericunknown:genai/unknown:openinference/unknown:otherdialect buckets for AI spans no vendor claims.The session ID is the first non-empty session-granularity attribute for the matched vendor (e.g.
gen_ai.conversation.idfor mastra,eve.session.idfor eve,langsmith.metadata.thread_idfor langchain). Vendors whose instrumentation only emits run- or user-scoped IDs (litellm, llamaindex, haystack, semantic_kernel, effect_ai) get no session attribute — either the vendor gives us a real session ID or we don't write one.No new columns, no indexes, no materialized views, no schema version.
Performance
Classification sits on the per-span hot path with a budget of ~50ns per trace (20+ spans average). The structure, in order of what carries the load:
ScopeSpans/ResourceSpans.Stringinstead of re-copying it.criterion bench (
benches/ai_session_bench.rs), Apple Silicon dev machine:The hard floor under these detection semantics is the per-attribute screen itself: nearly every vendor has scope-free span-attribute clauses, so every span's keys must be looked at (~240 attrs x ~0.5ns per 20-span trace). Getting materially below this requires a semantic relaxation (e.g. a trust list of known non-AI instrumentation scopes that skips span scanning), deliberately not taken in this PR.
Tests
stamp_trace_requesttest: AI span stamped, plain HTTP span untouched, spoofed customermaple_ai.*keys stripped.main.rsproving enrichment stamps spans on the shared path before the native/forward fork.🤖 Generated with Claude Code