Class: Datadog::OpenFeature::Hooks::SpanEnrichmentHook
- Inherits:
-
Object
- Object
- Datadog::OpenFeature::Hooks::SpanEnrichmentHook
- Defined in:
- lib/datadog/open_feature/hooks/span_enrichment_hook.rb,
lib/datadog/open_feature/hooks/span_enrichment_hook/codec.rb,
lib/datadog/open_feature/hooks/span_enrichment_hook/span_enrichment_state.rb,
lib/datadog/open_feature/hooks/span_enrichment_hook/span_enrichment_state_store.rb
Overview
Captures feature-flag evaluation metadata and writes contract-conformant
ffe_* tags onto the local root APM span when it finishes.
The wire format is a fixed cross-SDK contract. The encoding (ULEB128 delta-varint + base64), the per-trace aggregation limits, the runtime-default detection (missing variant) and the SHA256 subject hashing must be reproduced exactly so the backend decodes tags identically regardless of which language SDK emitted them.
Dispatch:
Enrichment is driven DIRECTLY from `Provider#evaluate` (see `#capture`),
not through an OpenFeature hook. This is the only path that works on
every supported SDK version, so the capture happens on the provider
evaluation thread rather than a hook callback.
Lifecycle:
- `#capture` runs on every evaluation. It resolves the local root span
off the active trace, lazily creates the per-root `SpanEnrichmentState`,
and on the first capture for a trace subscribes to that trace's
`span_before_finish` event. The subscription closure captures the
state strongly and is held by `trace_op.events`, so the state lives
exactly as long as the trace operation (the weak store key) — when the
trace is GC'd both die together (no leak even if the root never finishes).
- When the local root span is about to finish, the accumulated tags are
written via `span.set_tag` and the per-root state is deleted.
- `#shutdown` clears all accumulated state (provider-close cleanup).
Thread-safety:
Concurrent evaluations (on different threads) can target the same active
trace. The GVL does NOT make the compound operations here safe
(fetch-or-create, subscribe-once, mutate-while-encoding), so all store /
subscription / state access is serialized through a single
`Mutex`, and the finish-time encode takes the snapshot under that lock.
When the gate is off the hook is never constructed (see
Component#create_span_enrichment_hook), so there is zero idle per-span
overhead.
Defined Under Namespace
Modules: Codec Classes: SpanEnrichmentState, SpanEnrichmentStateStore
Constant Summary collapse
- MAX_SERIAL_IDS =
Fixed cross-SDK contract limits. No env-var knobs.
200- MAX_SUBJECTS =
10- MAX_EXPERIMENTS_PER_SUBJECT =
20- MAX_DEFAULTS =
5- MAX_DEFAULT_VALUE_LENGTH =
64- TAG_FLAGS_ENC =
"ffe_flags_enc"- TAG_SUBJECTS_ENC =
"ffe_subjects_enc"- TAG_RUNTIME_DEFAULTS =
"ffe_runtime_defaults"
Instance Method Summary collapse
-
#capture(flag_key:, variant:, value:, serial_id:, do_log:, targeting_key:) ⇒ Object
Direct dispatch from the Datadog provider evaluation path.
-
#initialize(span_enrichment_state_store, logger:) ⇒ SpanEnrichmentHook
constructor
A new instance of SpanEnrichmentHook.
-
#shutdown ⇒ Object
Provider-close cleanup: mark the hook inactive and drop all accumulated state.
Constructor Details
#initialize(span_enrichment_state_store, logger:) ⇒ SpanEnrichmentHook
Returns a new instance of SpanEnrichmentHook.
59 60 61 62 63 64 |
# File 'lib/datadog/open_feature/hooks/span_enrichment_hook.rb', line 59 def initialize(span_enrichment_state_store, logger:) @store = span_enrichment_state_store @mutex = Mutex.new @active = true @logger = logger end |
Instance Method Details
#capture(flag_key:, variant:, value:, serial_id:, do_log:, targeting_key:) ⇒ Object
Direct dispatch from the Datadog provider evaluation path. Takes only primitives so it does not depend on any OpenFeature SDK object shape and works on every supported SDK version. Never raises — flag evaluation and the trace pipeline must not be broken by enrichment.
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/datadog/open_feature/hooks/span_enrichment_hook.rb', line 77 def capture(flag_key:, variant:, value:, serial_id:, do_log:, targeting_key:) trace_op = Datadog::Tracing.active_trace return unless trace_op @mutex.synchronize do if serial_id.nil? if variant.nil? || variant.empty? # Runtime default: detected by a missing variant (never a reason enum). state_for(trace_op).add_default(flag_key, value) end else state = state_for(trace_op) state.add_serial_id(serial_id) # Skip empty targeting keys: SHA256('') would collide every # anonymous/missing subject under one bogus hash, corrupting # subject-level attribution. state.add_subject(targeting_key, serial_id) if do_log && targeting_key && !targeting_key.empty? end end rescue => e @logger.debug { "Error capturing span enrichment: #{e.class}: #{e.}" } end |
#shutdown ⇒ Object
Provider-close cleanup: mark the hook inactive and drop all accumulated
state. State is dropped, not flushed, on purpose: each state only
becomes a set of tags when ITS OWN root span finishes, and at
provider-close time those roots are still open — there is no valid
target to flush to, and writing partial tags onto in-flight spans
would emit incomplete data (and double-write once the root finished).
A trace that already subscribed still holds its state via the
span_before_finish closure, so clearing @active (checked in
write_tags_on_root) is what actually prevents a stale write after
shutdown. Per-trace subscriptions die with their trace operations, so
there is nothing else to unsubscribe.
111 112 113 114 115 116 |
# File 'lib/datadog/open_feature/hooks/span_enrichment_hook.rb', line 111 def shutdown @mutex.synchronize do @active = false @store.clear! end end |