Prometheus — Interfaces#

Interface catalog#

storage.Storage#

  • Package: github.com/prometheus/prometheus/storage
  • File: storage/interface.go:82
  • Methods: Embeds SampleAndChunkQueryable (→ Queryable + ChunkQueryable), Appendable, AppendableV2, plus StartTime() (int64, error) and Close() error
  • Purpose: Top-level contract for a complete time-series storage backend. Combines the full read path (samples and chunks) with both write paths (v1 and v2 appenders) and lifecycle methods.
  • Implementations: tsdb.DB (local TSDB), remote.Storage (remote read/write), storage.fanoutStorage (fanout multiplexer), agent WAL storage
  • Design quality: Good compositional design — Storage is a convenience “god interface” for wiring, but its constituent pieces (Queryable, Appendable) are small and used independently. The presence of both Appendable and AppendableV2 reflects an in-progress migration; this is a transient design smell that is explicitly documented as ETA Q2 2026.

storage.Queryable#

  • Package: github.com/prometheus/prometheus/storage
  • File: storage/interface.go:108
  • Methods: Querier(mint, maxt int64) (Querier, error)
  • Purpose: Single-method factory interface for opening a time-bounded read view over a storage. This is the primary seam between the PromQL engine and any storage backend. Any project (Thanos, Cortex, Mimir) can plug in its own backend by implementing this one method.
  • Implementations: tsdb.DB, fanoutStorage, remote.Storage, storage.QueryableFunc (adapter for plain functions), MockQueryable (testing)
  • Design quality: Exemplary ISP application. One method, one responsibility. The QueryableFunc adapter (following the http.HandlerFunc pattern) shows the interface is designed for easy ad-hoc implementation. The SelectHints struct passed deeper into Querier.Select() carries optional optimization hints, keeping the core interface clean.

storage.Appender / AppenderV2#

  • Package: github.com/prometheus/prometheus/storage
  • File: storage/interface.go:291, storage/interface_append.go:156
  • Methods (Appender v1): Embeds AppenderTransaction (Commit() error, Rollback() error), ExemplarAppender, HistogramAppender, MetadataUpdater, StartTimestampAppender; plus Append(ref SeriesRef, l labels.Labels, t int64, v float64) (SeriesRef, error) and SetOptions(*AppendOptions)
  • Methods (AppenderV2): Embeds AppenderTransaction; single Append(ref SeriesRef, ls labels.Labels, st, t int64, v float64, h *histogram.Histogram, fh *histogram.FloatHistogram, opts AppendV2Options) (SeriesRef, error)
  • Purpose: Batched, transactional write interface for ingesting samples (floats, histograms, exemplars, metadata). AppenderV2 unifies all sample types into a single Append call with an options struct, replacing the fragmented v1 interface that had separate methods for each type.
  • Implementations: tsdb.headAppender, remote.sampleAndMetadataQueue, fanoutAppender, various test fakes
  • Design quality: The migration from v1 to v2 is an interesting case study in interface evolution. V1 grew organically — a separate interface was added each time a new data type (exemplars, histograms, metadata, start timestamps) was introduced, leading to a wide composite interface. V2 collapses these into a single method with a struct for optional data. The SeriesRef caching pattern (return a ref on first append; pass it back to skip label lookups on subsequent appends) is a clever performance optimization built into the contract.

storage.Querier / storage.SeriesSet / storage.Series#

  • Package: github.com/prometheus/prometheus/storage
  • File: storage/interface.go:123, storage/interface.go:414, storage/interface.go:483
  • Methods (Querier): Embeds LabelQuerier (LabelValues, LabelNames, Close); Select(ctx context.Context, sortSeries bool, hints *SelectHints, matchers ...*labels.Matcher) SeriesSet
  • Methods (SeriesSet): Next() bool, At() Series, Err() error, Warnings() annotations.Annotations
  • Methods (Series): Embeds Labels (Labels() labels.Labels) and SampleIterable (Iterator(chunkenc.Iterator) chunkenc.Iterator)
  • Purpose: Three-tier iterator hierarchy for reading samples: Querier opens a scan, SeriesSet iterates over matched series, Series iterates over samples within a series. The chain mirrors the physical layout of TSDB (label index → posting list → chunk iterator).
  • Implementations: tsdb.blockQuerier, tsdb.headQuerier, storage.mergeQuerier (for fan-in across blocks), remote read querier
  • Design quality: Clean iterator protocol following the Go standard. The Warnings() method on SeriesSet is notable — it allows non-fatal advisory messages (e.g., “metric X is stale”) to propagate alongside results without contaminating the error channel. The chunkenc.Iterator passed to Series.Iterator() is a re-use parameter — callers can pass back the previous iterator to avoid allocation; implementations may or may not honor it. This contract is documented at the interface level.

discovery.Discoverer#

  • Package: github.com/prometheus/prometheus/discovery
  • File: discovery/discovery.go:35
  • Methods: Run(ctx context.Context, up chan<- []*targetgroup.Group)
  • Purpose: The extension point for all 30+ service discovery mechanisms. Each provider (Kubernetes, Consul, EC2, DNS, etc.) is a Discoverer. The contract is minimal: run until context cancels, push target group updates to the provided channel.
  • Implementations: One implementation per provider: kubernetes.Discovery, consul.Discovery, ec2.Discovery, dns.Discovery, file.Discovery, staticDiscoverer, etc.
  • Design quality: Near-perfect ISP. A single method makes it trivially easy to implement new providers. The asymmetric channel direction (chan<-) makes ownership explicit — providers push, the manager pulls. The note that implementations must NOT close the channel on return is an important contract detail documented in the interface comment.

discovery.Config#

  • Package: github.com/prometheus/prometheus/discovery
  • File: discovery/discovery.go:89
  • Methods: Name() string, NewDiscoverer(DiscovererOptions) (Discoverer, error), NewDiscovererMetrics(prometheus.Registerer, RefreshMetricsInstantiator) DiscovererMetrics
  • Purpose: Factory interface for discovery providers. Each SD mechanism provides a Config that knows its own name, how to construct a Discoverer from options, and how to register its Prometheus metrics. The discovery package uses reflection-based YAML (de)serialization to build a Configs slice from unmarshaled config files without knowing the concrete types at compile time.
  • Implementations: One Config implementation per provider, registered via discovery.RegisterConfig()
  • Design quality: The three-method design bundles construction, naming, and observability. The reflection-based YAML dispatch (Configs.UnmarshalYAML) is the most unusual piece — it uses reflect.StructOf to dynamically build a struct type covering all registered configs, enabling discovery plugins to be added without touching the YAML unmarshaling logic.

promql.QueryEngine#

  • Package: github.com/prometheus/prometheus/promql
  • File: promql/engine.go:125
  • Methods: NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error), NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error)
  • Purpose: Abstracts the PromQL execution engine. Introduced after the *promql.Engine concrete type already existed, primarily so it can be replaced, wrapped, or mocked. The concrete Engine struct satisfies this interface. Thanos’s streaming engine and the community-developed FrostDB engine both implement it.
  • Implementations: *promql.Engine (built-in), Thanos streaming engine, test fakes
  • Design quality: Two methods covering the two query modes (instant and range) is the right decomposition. The QueryOpts parameter carries per-query tuning (lookback delta, per-step stats) as an interface rather than a struct, allowing downstream implementations to define their own options without breaking the signature. The returned Query interface (not a *Result) enables lazy evaluation and cancellation.

promql.Query#

  • Package: github.com/prometheus/prometheus/promql
  • File: promql/engine.go:143
  • Methods: Exec(ctx context.Context) *Result, Close(), Statement() parser.Statement, Stats() *stats.Statistics, Cancel(), String() string
  • Purpose: Represents a prepared but not yet executed PromQL query. Separates query preparation from evaluation; the caller controls when execution happens and can cancel or inspect it.
  • Implementations: *query (unexported concrete type returned by Engine.NewInstantQuery/NewRangeQuery)
  • Design quality: Well-designed for observability and resource management. Stats() exposes timing breakdown per evaluation phase. Close() reclaims pooled result slices. The Cancel() method provides explicit cooperative cancellation on top of context cancellation.

rules.Rule#

  • Package: github.com/prometheus/prometheus/rules
  • File: rules/rule.go:38
  • Methods: Name() string, Labels() labels.Labels, Eval(ctx, queryOffset, evaluationTime, queryFunc, externalURL, limit) (Vector, error), String() string, Query() parser.Expr, SetLastError/LastError(), SetHealth/Health(), SetEvaluationDuration/GetEvaluationDuration(), SetEvaluationTimestamp/GetEvaluationTimestamp(), SetDependentRules/NoDependentRules/DependentRules(), SetDependencyRules/NoDependencyRules/DependencyRules()
  • Purpose: Common contract for RecordingRule and AlertingRule. The rules.Manager operates on []Rule slices without knowing which type it holds. The dependency-tracking methods (SetDependentRules, NoDependentRules) support the concurrent rule evaluation optimization — rules with no dependents can execute in parallel.
  • Implementations: *RecordingRule, *AlertingRule
  • Design quality: The interface has grown large (15+ methods) as features were added. The setter/getter pairs for health, duration, and timestamp are essentially mutable state accessors; this is more struct-like than idiomatic interface design. The dependency tracking methods are notably specific to one optimization — they arguably belong on a separate interface. This is the weakest interface design in the codebase, but it reflects the single-implementation reality (there are only ever two Rule types).

tsdb/chunkenc.Chunk / Iterator / Appender#

  • Package: github.com/prometheus/prometheus/tsdb/chunkenc
  • File: tsdb/chunkenc/chunk.go:72,108,128
  • Methods (Chunk): Bytes() []byte, Encoding() Encoding, Appender() (Appender, error), NumSamples() int, Compact(), Reset(stream []byte), plus Iterable (→ Iterator(Iterator) Iterator)
  • Methods (Appender): Append(st, t int64, v float64), AppendHistogram(prev, st, t, h, appendOnly) (Chunk, bool, Appender, error), AppendFloatHistogram(...)
  • Methods (Iterator): Next() ValueType, Seek(t int64) ValueType, At() (int64, float64), AtHistogram(*Histogram) (int64, *Histogram), AtFloatHistogram(*FloatHistogram) (int64, *FloatHistogram), AtT() int64, Err() error
  • Purpose: Codec abstraction for compressed time-series chunks. Chunk is the container; Appender writes into it; Iterator reads from it. Encoding types (XOR for floats, delta/gorilla variants for histograms) are hidden behind these interfaces.
  • Implementations: XORChunk (Gorilla float encoding), HistogramChunk, FloatHistogramChunk
  • Design quality: Clean separation of read and write paths. The AppendHistogram return signature is unusual — it may return a new Chunk when the current one overflows or needs recoding, making chunk splitting explicit and caller-controlled. The Iterator re-use parameter (pass back previous iterator) is a deliberate allocation-reduction pattern.

Interface patterns#

  • Size distribution: Most interfaces are small (1–3 methods). storage.Storage and rules.Rule are the outliers at 8+ effective methods, and both are justified: Storage is a composition interface for wiring, Rule is genuinely fat but has only two concrete implementations. Average across all architecturally significant interfaces is ~3 methods.

  • Embedding: Used pervasively and thoughtfully.

    • Storage embeds SampleAndChunkQueryable, Appendable, AppendableV2
    • SampleAndChunkQueryable embeds Queryable and ChunkQueryable
    • Querier embeds LabelQuerier
    • Series embeds Labels and SampleIterable
    • RefreshMetricsManager embeds DiscovererMetrics and RefreshMetricsInstantiator
    • This “interface pyramid” pattern allows components to declare exactly the capability they need (e.g., the PromQL engine needs Queryable, not Storage).
  • Implicit satisfaction: Interfaces are consistently defined at the consumer side. storage.Queryable is defined in the storage package and consumed by promql. web/api/v1 defines its own TargetRetriever, RulesRetriever, AlertmanagerRetriever interfaces for the parts of scrape.Manager, rules.Manager, and notifier.Manager it needs — classic dependency inversion. discovery.Config is defined in discovery and implemented by each provider package.

  • Stdlib interfaces used:

    • io.Closer — embedded by LabelQuerier, QueryTracker, QueryLogger
    • slog.Handler — embedded by QueryLogger
    • The standard Next() bool / At() T / Err() error iterator protocol (used by SeriesSet, ChunkSeriesSet, tsdb/chunks.Iterator, tsdb/index.Postings) is consistent with bufio.Scanner and database/sql.Rows conventions
  • Adapter functions: storage.QueryableFunc (func → Queryable) and storage.AppenderV2AsLimitedV1 (v2 → v1 compatibility shim) follow the http.HandlerFunc pattern, reducing ceremony for simple implementations.


Key abstractions#

  1. storage.Queryable — The single most architecturally important interface. Decouples the PromQL engine from all storage implementations. One method, swappable backend. This is why Thanos/Cortex/Mimir could reuse Prometheus’s query engine.

  2. storage.Appendable / storage.AppendableV2 — The write-side counterpart to Queryable. Everything that produces metrics (scrape engine, rule evaluator, OTLP receiver, remote write receiver) writes through Appendable, allowing fanout, filtering, and backend swapping without producer changes. The v1→v2 migration is visible here as an in-progress API evolution with explicit deprecation warnings.

  3. discovery.Discoverer — The plugin interface that enabled 30+ discovery providers to be added over Prometheus’s lifetime without modifying core code. One method, context-driven, channel-based output. Textbook extensibility design.

  4. promql.QueryEngine — Added retroactively to allow the concrete *Engine to be replaced. Enables Thanos’s distributed query engine, FrostDB, and other alternative engines to slot into the same Prometheus web handler without any changes to the API layer.

  5. tsdb/chunkenc.Chunk + Iterator — The low-level codec abstraction. Isolates all encoding complexity (Gorilla XOR, histogram delta coding) behind stable read/write contracts. The Iterator re-use parameter is a performance contract encoded into the interface signature itself.


Interface-driven extensibility#

Prometheus uses interfaces for three distinct extensibility patterns:

Backend swapping (storage seam): storage.Queryable and storage.Appendable are the primary extension points. The entire storage stack — TSDB, remote storage, agent WAL — is wired together in main.go via these two interfaces. External projects swap the backend by providing their own implementations.

Plugin registration (service discovery): discovery.Discoverer + discovery.Config form a factory plugin system. New SD providers implement Config, register via discovery.RegisterConfig(), and are automatically available in YAML configuration. The reflection-based YAML dispatch means zero changes to the config unmarshaling logic per new provider.

Engine replacement (query engine): promql.QueryEngine allows replacing the built-in evaluation engine. This was added specifically to support Thanos’s streaming engine and alternative implementations. The web API layer (web/api/v1) depends on QueryEngine, not *Engine, making the substitution seamless.

Consumer-side interfaces (web API isolation): web/api/v1 defines narrow interfaces (TargetRetriever, RulesRetriever, ScrapePoolsRetriever, TSDBAdminStats) for the specific methods it needs from each manager. This keeps the HTTP handler testable with lightweight fakes rather than fully instantiated subsystems, and makes the dependency graph explicit.