Skip to content

[Proposal] Feature flag distribution layer & observability #390

Description

@aepfli

Summary

OpenFeature standardizes how a single flag is evaluated and how that one evaluation is reported to telemetry (Appendix D → OpenTelemetry feature_flag.*). It says nothing about the distribution layer: the set of flags actually available to a service — which flags exist, their default values, whether they're enabled or disabled, whether they're simple or targeted, their variants, flag-set membership, and version.

That gap is exactly where vendors fill the vacuum with proprietary dashboards and exports. This proposal is to define a provider-neutral data model for the available-flag inventory and an emitter-agnostic way to observe it, so the OpenFeature thesis extends past evaluation: any flag management tool × any observability tool, as long as both speak the standard. You should be able to see the full picture of your flag system from open standards rather than a per-vendor approach, and swap either side without losing that picture.

Background / problem

What the spec covers today is strictly per-evaluation:

  • The evaluation API resolves one flag at a time: resolve*Value(key, default, ctx)evaluation details + flag metadata.
  • Appendix D maps that one evaluation onto OpenTelemetry feature-flag event records (feature_flag.key, feature_flag.result.variant, feature_flag.result.reason, feature_flag.set.id, feature_flag.version, feature_flag.provider.name, …).

To analyze a flag system — audit what exists, spot stale or always-default flags, see which flags carry targeting, correlate config across services — you need the inventory, not a stream of individual lookups. That inventory exists today only implicitly, and never as something you can observe through a standard:

  • The in-memory provider's flag-set schema already describes flags as variants + defaultVariant + optional contextEvaluator (targeting) + disabled + flagMetadata (see appendix-a-included-utilities.md and assets/gherkin/test-flags.json). This is effectively a distribution-layer schema that we use for testing but never standardized as a thing to expose or observe.
  • OFREP defines bulk evaluation — a natural inventory source.
  • flagSetId and version already live in flag metadata.
  • PROVIDER_CONFIGURATION_CHANGED already signals that the inventory changed.

So the building blocks exist; what's missing is a standard that ties them into a describable, observable distribution layer.

Why "who emits it" is the hard part

The obvious instinct — "the SDK should emit it" or "the vendor should emit it" — breaks down:

  • The SDK/provider only reliably knows the flags it has touched. In the dynamic-context paradigm it resolves flags lazily and may never see the full set; in the static-context paradigm it gets a bulk-evaluated set for one context, not the definitions or targeting.
  • The management tool / source knows everything — but in a GitOps / declarative setup the "source" is a YAML file in a git repo with no runtime to emit anything. The component that knows the inventory isn't always a running process.

The conclusion that makes this tractable: define the distribution layer as a neutral data model / semantic convention, decoupled from who emits it. One schema; multiple possible producers. That's also what keeps it vendor-neutral — the same shape whether it comes from a SaaS backend, a sync service, an SDK, or a build step.

Proposed distribution data model

The model is two-level: a flag set carries its own identity/metadata and contains the per-flag entries. This avoids repeating set-wide values on every flag, and matches how the rest of the spec already treats flagSetId/version as describing a flag set rather than an individual flag.

Flag-set metadata (describes the set as a whole):

Field Meaning Existing basis
flagSetId logical identifier for the flag set Appendix D flag metadata
version version string for the flag set Appendix D flag metadata
provider/source identity which system this set came from feature_flag.provider.name
set metadata arbitrary additional set-level properties

Per-flag (one entry per flag in the set):

Field Meaning Existing basis
key flag identifier flag key
type boolean / string / number / structure flag type
defaultVariant the variant key served when no targeting matches in-memory provider schema
default value value of the default variant (privacy caveat below) in-memory provider schema
variant keys the set of variant names (not values) — optional in-memory provider schema
disabled enabled vs disabled in-memory provider schema (disabled: true)
complexity simple vs targeted — derivable from presence of targeting/contextEvaluator in-memory provider schema
flagMetadata arbitrary additional per-flag properties flag metadata

Two intentional reductions from the in-memory schema:

  • Variant values are excluded; only variant keys are kept (and even those are optional). The default value is enough to detect "the default was served" (compare it against the feature_flag.result.variant already in evaluation telemetry). Variant keys are cheap and non-sensitive and let you spot dead variants / multivariate cardinality. Variant values would inherit the same volume and privacy problems Appendix D already flags for feature_flag.result.value, for little analytical gain.
  • "Complexity" need not be a stored field — it's derivable from whether targeting rules are present (how the in-memory provider expresses it via contextEvaluator).

The intent throughout is the same vocabulary as evaluation telemetry, the in-memory schema, and OFREP — not a third, incompatible shape.

Emission responsibility — the heart of this proposal

One neutral schema, several legitimate producers. Rather than mandate one, recommend by proximity to source-of-truth:

  • Flag management backend / SaaS — emits the authoritative inventory directly. Best fidelity.
  • Sync service (e.g. flagd gRPC sync) — already holds the full flag set in transit; a natural emitter.
  • Provider / SDK — emits what it can observe; the fallback when nothing upstream emits. Covers the inventory the app actually touches even if it's partial.
  • GitOps / build-time emitter — for declarative sources, a CI/build step reads the flag definitions from the repo and emits the inventory, since there's no runtime owner.

Recommendation: the component nearest the source-of-truth SHOULD emit; the SDK MAY emit as a fallback; no single emitter is mandated. Because the schema is identical regardless of producer, an observability tool consumes it the same way no matter where it originated — which is the whole point.

Telemetry representation (open question — for discussion)

Deliberately not deciding the signal type yet; enumerating options and trade-offs for the thread:

  • Logs / events per flag definition — mirrors how OTel models evaluations today; rich, good for "describe each flag." Volume on large flag sets.
  • Metrics (gauges per flag) — e.g. a gauge for enabled/disabled, a dimension for complexity. Great for dashboards/alerting; weak for rich definition detail.
  • Resource-level attributes — flag-set / provider identity as resource attributes that decorate the above.
  • Combinations of the above.

Whatever we pick, it should be proposed upstream to OpenTelemetry semantic conventions, the same path the evaluation conventions took (see Appendix D history). This is the key decision I want maintainer input on.

Relationship to existing work

Out of scope / future

  • An SDK API surface to expose the inventory to application code (a possible follow-up spec section).
  • Governance / access-control concerns around exposing the full inventory.
  • Concrete spec requirements — those come as a separate PR against open-feature/spec once the model, emission approach, and signal type are agreed here.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions