Class: Datadog::OpenFeature::Hooks::SpanEnrichmentHook

Inherits:
Object
  • Object
show all
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

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.

Parameters:

  • flag_key (String)

    the evaluated flag key

  • variant (String, nil)

    the resolved variant (nil/empty => runtime default)

  • value (Object)

    the resolved value (used for runtime-default capture)

  • serial_id (Integer, nil)

    the split serial id, when assigned

  • do_log (Boolean)

    whether logging/exposure is authorized for this subject

  • targeting_key (String, nil)

    the raw targeting key (hashed before emit)



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.message}" }
end

#shutdownObject

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