feat(compat): complete prometheus/client_golang surface for CoreDNS

Add the prometheus-compatible symbols CoreDNS (and the kubernetes client)
reference so a mechanical prometheus -> luxfi/metric migration compiles and
behaves unchanged:

- HistogramOpts native (sparse) histogram fields (NativeHistogramBucketFactor et al)
- package MustRegister/Unregister; AlreadyRegisteredError (returned on duplicate
  registration so re-registration on reload is tolerated)
- With/AutoFactory (promauto-style registerer-scoped constructors)
- GoCollectorOption/WithGoCollectorRuntimeMetrics/MetricsAll; NewGoCollector(opts...)
- {Counter,Gauge,Histogram,Summary}Vec.Delete(Labels)
- Register treats the no-op Go/process collectors as no-ops rather than errors
This commit is contained in:
z
2026-07-18 09:40:44 -07:00
parent 0b512e3bd9
commit 2862b43f71
6 changed files with 280 additions and 4 deletions
+12
View File
@@ -55,6 +55,9 @@ func (c *curriedCounterVec) WithLabelValues(values ...string) Counter {
func (c *curriedCounterVec) MustCurryWith(labels Labels) CounterVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedCounterVec) Delete(labels Labels) bool {
return c.base.Delete(mergeLabels(c.fixed, labels))
}
func (c *curriedCounterVec) Reset() { c.base.Reset() }
// --- gauge ---
@@ -78,6 +81,9 @@ func (c *curriedGaugeVec) WithLabelValues(values ...string) Gauge {
func (c *curriedGaugeVec) MustCurryWith(labels Labels) GaugeVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedGaugeVec) Delete(labels Labels) bool {
return c.base.Delete(mergeLabels(c.fixed, labels))
}
func (c *curriedGaugeVec) Reset() { c.base.Reset() }
// --- histogram ---
@@ -101,6 +107,9 @@ func (c *curriedHistogramVec) WithLabelValues(values ...string) Histogram {
func (c *curriedHistogramVec) MustCurryWith(labels Labels) HistogramVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedHistogramVec) Delete(labels Labels) bool {
return c.base.Delete(mergeLabels(c.fixed, labels))
}
func (c *curriedHistogramVec) Reset() { c.base.Reset() }
// --- summary ---
@@ -124,4 +133,7 @@ func (c *curriedSummaryVec) WithLabelValues(values ...string) Summary {
func (c *curriedSummaryVec) MustCurryWith(labels Labels) SummaryVec {
return c.base.MustCurryWith(mergeLabels(c.fixed, labels))
}
func (c *curriedSummaryVec) Delete(labels Labels) bool {
return c.base.Delete(mergeLabels(c.fixed, labels))
}
func (c *curriedSummaryVec) Reset() { c.base.Reset() }
+11 -2
View File
@@ -27,8 +27,17 @@ func NewProcessCollector(opts ProcessCollectorOpts) Collector {
return &processCollector{opts: opts}
}
// NewGoCollector creates a new Go collector (no-op for now).
func NewGoCollector() Collector {
// NewGoCollector creates a new Go collector (no-op for now). It accepts the
// Prometheus GoCollectorOption surface (e.g. WithGoCollectorRuntimeMetrics) so
// callers migrating from prometheus/client_golang compile unchanged; the options
// are applied to the collector's config and the no-op collector ignores them.
func NewGoCollector(opts ...GoCollectorOption) Collector {
cfg := goCollectorConfig{}
for _, o := range opts {
if o != nil {
o(&cfg)
}
}
return &goCollector{}
}
+25
View File
@@ -34,6 +34,19 @@ type HistogramOpts struct {
Help string
ConstLabels Labels
Buckets []float64
// Native (sparse) histogram configuration. These mirror the Prometheus
// HistogramOpts surface so callers written against prometheus/client_golang
// (CoreDNS, the kubernetes client) compile and configure unchanged. A
// non-zero NativeHistogramBucketFactor requests a native histogram; the
// remaining fields tune bucket count, the zero bucket, and the reset cadence.
NativeHistogramBucketFactor float64
NativeHistogramZeroThreshold float64
NativeHistogramMaxBucketNumber uint32
NativeHistogramMinResetDuration time.Duration
NativeHistogramMaxZeroThreshold float64
NativeHistogramMaxExemplars int
NativeHistogramExemplarTTL time.Duration
}
// SummaryOpts configures a summary metric.
@@ -107,6 +120,9 @@ type CounterVec interface {
With(Labels) Counter
WithLabelValues(...string) Counter
MustCurryWith(Labels) CounterVec
// Delete removes the child with the exact label set, returning true if it
// was present. Mirrors prometheus.CounterVec.Delete.
Delete(Labels) bool
Reset()
}
@@ -115,6 +131,9 @@ type GaugeVec interface {
With(Labels) Gauge
WithLabelValues(...string) Gauge
MustCurryWith(Labels) GaugeVec
// Delete removes the child with the exact label set, returning true if it
// was present. Mirrors prometheus.GaugeVec.Delete.
Delete(Labels) bool
Reset()
}
@@ -123,6 +142,9 @@ type HistogramVec interface {
With(Labels) Histogram
WithLabelValues(...string) Histogram
MustCurryWith(Labels) HistogramVec
// Delete removes the child with the exact label set, returning true if it
// was present. Mirrors prometheus.HistogramVec.Delete.
Delete(Labels) bool
Reset()
}
@@ -131,6 +153,9 @@ type SummaryVec interface {
With(Labels) Summary
WithLabelValues(...string) Summary
MustCurryWith(Labels) SummaryVec
// Delete removes the child with the exact label set, returning true if it
// was present. Mirrors prometheus.SummaryVec.Delete.
Delete(Labels) bool
Reset()
}
+122 -2
View File
@@ -684,6 +684,70 @@ func (hpr *registry) deregisterLabeled(name string) {
delete(hpr.summaries, name)
}
// deregisterLabeled{Counter,Gauge,Histogram,Summary}One drops a SINGLE
// label-permutation child (identified by its labels key) for the named metric,
// returning true if it was present. Backs {Counter,Gauge,Histogram,Summary}Vec.
// Delete — the single-series analogue of deregisterLabeled/Reset.
func (hpr *registry) deregisterLabeledCounterOne(name, key string) bool {
hpr.mu.Lock()
defer hpr.mu.Unlock()
m := hpr.counters[name]
if m == nil {
return false
}
_, ok := m[key]
delete(m, key)
if len(m) == 0 {
delete(hpr.counters, name)
}
return ok
}
func (hpr *registry) deregisterLabeledGaugeOne(name, key string) bool {
hpr.mu.Lock()
defer hpr.mu.Unlock()
m := hpr.gauges[name]
if m == nil {
return false
}
_, ok := m[key]
delete(m, key)
if len(m) == 0 {
delete(hpr.gauges, name)
}
return ok
}
func (hpr *registry) deregisterLabeledHistogramOne(name, key string) bool {
hpr.mu.Lock()
defer hpr.mu.Unlock()
m := hpr.histograms[name]
if m == nil {
return false
}
_, ok := m[key]
delete(m, key)
if len(m) == 0 {
delete(hpr.histograms, name)
}
return ok
}
func (hpr *registry) deregisterLabeledSummaryOne(name, key string) bool {
hpr.mu.Lock()
defer hpr.mu.Unlock()
m := hpr.summaries[name]
if m == nil {
return false
}
_, ok := m[key]
delete(m, key)
if len(m) == 0 {
delete(hpr.summaries, name)
}
return ok
}
// NewCounter creates and registers a counter.
func (hpr *registry) NewCounter(name, help string) Counter {
counter := newCounter(name, help)
@@ -739,6 +803,14 @@ func (hpr *registry) Registry() Registry {
// Register is a compatibility no-op. Metrics are registered on creation.
func (hpr *registry) Register(c Collector) error {
// The Go and process collectors are exposition-only shims in this library
// (no sparse runtime/process series yet); registering them is a no-op rather
// than an "unsupported type" error, so prometheus-style
// MustRegister(NewGoCollector(...)) succeeds.
switch c.(type) {
case *goCollector, *processCollector:
return nil
}
name, typ, ok := collectorIdentity(c)
if !ok {
return fmt.Errorf("unsupported collector type %T", c)
@@ -909,6 +981,17 @@ func (v *counterVec) Reset() {
v.counters = make(map[string]Counter)
}
// Delete removes the child with the exact label set. Mirrors prometheus.
func (v *counterVec) Delete(labels Labels) bool {
key := labelsKeyFromLabels(labels)
v.mu.Lock()
_, existed := v.counters[key]
delete(v.counters, key)
v.mu.Unlock()
regExisted := v.registry.deregisterLabeledCounterOne(v.name, key)
return existed || regExisted
}
// gaugeVec is a labeled gauge collection.
type gaugeVec struct {
registry *registry
@@ -959,6 +1042,17 @@ func (v *gaugeVec) Reset() {
v.gauges = make(map[string]Gauge)
}
// Delete removes the child with the exact label set. Mirrors prometheus.
func (v *gaugeVec) Delete(labels Labels) bool {
key := labelsKeyFromLabels(labels)
v.mu.Lock()
_, existed := v.gauges[key]
delete(v.gauges, key)
v.mu.Unlock()
regExisted := v.registry.deregisterLabeledGaugeOne(v.name, key)
return existed || regExisted
}
// histogramVec is a labeled histogram collection.
type histogramVec struct {
registry *registry
@@ -1011,6 +1105,17 @@ func (v *histogramVec) Reset() {
v.histograms = make(map[string]Histogram)
}
// Delete removes the child with the exact label set. Mirrors prometheus.
func (v *histogramVec) Delete(labels Labels) bool {
key := labelsKeyFromLabels(labels)
v.mu.Lock()
_, existed := v.histograms[key]
delete(v.histograms, key)
v.mu.Unlock()
regExisted := v.registry.deregisterLabeledHistogramOne(v.name, key)
return existed || regExisted
}
// summaryVec is a labeled summary collection.
type summaryVec struct {
registry *registry
@@ -1067,6 +1172,17 @@ func (v *summaryVec) Reset() {
v.summaries = make(map[string]Summary)
}
// Delete removes the child with the exact label set. Mirrors prometheus.
func (v *summaryVec) Delete(labels Labels) bool {
key := labelsKeyFromLabels(labels)
v.mu.Lock()
_, existed := v.summaries[key]
delete(v.summaries, key)
v.mu.Unlock()
regExisted := v.registry.deregisterLabeledSummaryOne(v.name, key)
return existed || regExisted
}
func labelsFromValues(labelNames []string, values []string) Labels {
labels := make(Labels, len(labelNames))
for i, name := range labelNames {
@@ -1131,8 +1247,12 @@ func labelsToLabelPairs(labels Labels) []LabelPair {
func (hpr *registry) registerName(name string, typ MetricType) error {
hpr.mu.Lock()
defer hpr.mu.Unlock()
if existing, ok := hpr.registered[name]; ok {
return fmt.Errorf("metric %q already registered as %s", name, existing.String())
if _, ok := hpr.registered[name]; ok {
// Return the typed AlreadyRegisteredError (mirrors prometheus.Registry)
// so callers that tolerate re-registration — CoreDNS re-runs setup on
// every `reload` — can detect the duplicate and continue instead of
// treating it as a fatal error.
return AlreadyRegisteredError{}
}
hpr.registered[name] = typ
return nil
+4
View File
@@ -71,6 +71,7 @@ type noopCounterVec struct{}
func (n *noopCounterVec) With(Labels) Counter { return &noopCounter{} }
func (n *noopCounterVec) WithLabelValues(...string) Counter { return &noopCounter{} }
func (n *noopCounterVec) MustCurryWith(Labels) CounterVec { return n }
func (n *noopCounterVec) Delete(Labels) bool { return false }
func (n *noopCounterVec) Reset() {}
// noopGaugeVec is a gauge vector that does nothing.
@@ -79,6 +80,7 @@ type noopGaugeVec struct{}
func (n *noopGaugeVec) With(Labels) Gauge { return &noopGauge{} }
func (n *noopGaugeVec) WithLabelValues(...string) Gauge { return &noopGauge{} }
func (n *noopGaugeVec) MustCurryWith(Labels) GaugeVec { return n }
func (n *noopGaugeVec) Delete(Labels) bool { return false }
func (n *noopGaugeVec) Reset() {}
// noopHistogramVec is a histogram vector that does nothing.
@@ -87,6 +89,7 @@ type noopHistogramVec struct{}
func (n *noopHistogramVec) With(Labels) Histogram { return &noopHistogram{} }
func (n *noopHistogramVec) WithLabelValues(...string) Histogram { return &noopHistogram{} }
func (n *noopHistogramVec) MustCurryWith(Labels) HistogramVec { return n }
func (n *noopHistogramVec) Delete(Labels) bool { return false }
func (n *noopHistogramVec) Reset() {}
// noopSummaryVec is a summary vector that does nothing.
@@ -95,6 +98,7 @@ type noopSummaryVec struct{}
func (n *noopSummaryVec) With(Labels) Summary { return &noopSummary{} }
func (n *noopSummaryVec) WithLabelValues(...string) Summary { return &noopSummary{} }
func (n *noopSummaryVec) MustCurryWith(Labels) SummaryVec { return n }
func (n *noopSummaryVec) Delete(Labels) bool { return false }
func (n *noopSummaryVec) Reset() {}
// noopRegistry provides a registry that gathers nothing.
+106
View File
@@ -0,0 +1,106 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package metric
// This file completes the prometheus/client_golang compatibility surface so
// code migrated mechanically from prometheus (e.g. CoreDNS) compiles and
// behaves against luxfi/metric unchanged. Every symbol here mirrors a
// prometheus/client_golang (or client_golang/prometheus/{promauto,collectors})
// export; the backing implementation is luxfi/metric's own registry.
// AlreadyRegisteredError is returned by a registry's Register when a collector
// with the same fully-qualified name is already registered. It mirrors
// prometheus.AlreadyRegisteredError so callers that tolerate re-registration
// (CoreDNS re-runs plugin setup on every `reload`) can type-assert on it and
// continue rather than failing. The Collector fields are populated when
// available; callers typically only test the error's dynamic type.
type AlreadyRegisteredError struct {
ExistingCollector Collector
NewCollector Collector
}
// Error implements error. The message matches prometheus/client_golang's.
func (AlreadyRegisteredError) Error() string {
return "duplicate metrics collector registration attempted"
}
// MustRegister registers the given collectors on the default registerer and
// panics on error. Mirrors prometheus.MustRegister.
func MustRegister(cs ...Collector) { DefaultRegisterer.MustRegister(cs...) }
// Unregister removes a previously registered collector from the default
// registerer, returning true if it was present. Mirrors prometheus.Unregister.
func Unregister(c Collector) bool { return DefaultRegisterer.Unregister(c) }
// GoRuntimeMetricsRule selects which runtime/metrics metrics the Go collector
// exposes. Mirrors collectors.GoRuntimeMetricsRule. Matcher is the metric-name
// pattern (kept as the source string; the no-op Go collector does not evaluate
// it).
type GoRuntimeMetricsRule struct {
Matcher string
}
// MetricsAll enables every available runtime/metrics metric. Mirrors
// collectors.MetricsAll.
var MetricsAll = GoRuntimeMetricsRule{Matcher: "/.*"}
// goCollectorConfig accumulates GoCollectorOption settings.
type goCollectorConfig struct {
runtimeRules []GoRuntimeMetricsRule
}
// GoCollectorOption configures the Go collector. Mirrors
// collectors.GoCollectorOption.
type GoCollectorOption func(*goCollectorConfig)
// WithGoCollectorRuntimeMetrics enables the runtime/metrics metrics matched by
// the given rules. Mirrors collectors.WithGoCollectorRuntimeMetrics.
func WithGoCollectorRuntimeMetrics(rules ...GoRuntimeMetricsRule) GoCollectorOption {
return func(c *goCollectorConfig) { c.runtimeRules = append(c.runtimeRules, rules...) }
}
// Factory registers newly-created metrics on a specific Registerer, mirroring
// promauto.Factory. Obtain one with With.
type AutoFactory struct {
r Registerer
}
// With returns a Factory that registers every metric it creates on r. Mirrors
// promauto.With. A nil r targets the default registerer.
func With(r Registerer) AutoFactory {
if r == nil {
r = DefaultRegisterer
}
return AutoFactory{r: r}
}
// NewCounterVec creates a counter vector registered on the factory's registerer.
func (f AutoFactory) NewCounterVec(opts CounterOpts, labelNames []string) CounterVec {
return f.r.NewCounterVec(prefixedName(AppendNamespace(opts.Namespace, opts.Subsystem), opts.Name), opts.Help, labelNames)
}
// NewGaugeVec creates a gauge vector registered on the factory's registerer.
func (f AutoFactory) NewGaugeVec(opts GaugeOpts, labelNames []string) GaugeVec {
return f.r.NewGaugeVec(prefixedName(AppendNamespace(opts.Namespace, opts.Subsystem), opts.Name), opts.Help, labelNames)
}
// NewHistogramVec creates a histogram vector registered on the factory's registerer.
func (f AutoFactory) NewHistogramVec(opts HistogramOpts, labelNames []string) HistogramVec {
return f.r.NewHistogramVec(prefixedName(AppendNamespace(opts.Namespace, opts.Subsystem), opts.Name), opts.Help, labelNames, opts.Buckets)
}
// NewCounter creates a counter registered on the factory's registerer.
func (f AutoFactory) NewCounter(opts CounterOpts) Counter {
return f.r.NewCounter(prefixedName(AppendNamespace(opts.Namespace, opts.Subsystem), opts.Name), opts.Help)
}
// NewGauge creates a gauge registered on the factory's registerer.
func (f AutoFactory) NewGauge(opts GaugeOpts) Gauge {
return f.r.NewGauge(prefixedName(AppendNamespace(opts.Namespace, opts.Subsystem), opts.Name), opts.Help)
}
// NewHistogram creates a histogram registered on the factory's registerer.
func (f AutoFactory) NewHistogram(opts HistogramOpts) Histogram {
return f.r.NewHistogram(prefixedName(AppendNamespace(opts.Namespace, opts.Subsystem), opts.Name), opts.Help, opts.Buckets)
}