feat(metrics): publish build_info metric (#5643)

* feat(metrics): publish build_info metric

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(metrics): publish build_info metric

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(metrics): publish build_info metric

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

* feat(metrics): publish build_info metric

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
This commit is contained in:
Ivan Ka 2025-07-14 20:42:27 +01:00 committed by GitHub
parent faeee1226e
commit b93d1e9abb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 84 additions and 34 deletions

View File

@ -37,7 +37,6 @@ import (
var ( var (
registryErrorsTotal = metrics.NewCounterWithOpts( registryErrorsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
Name: "errors_total", Name: "errors_total",
Help: "Number of Registry errors.", Help: "Number of Registry errors.",
@ -45,7 +44,6 @@ var (
) )
sourceErrorsTotal = metrics.NewCounterWithOpts( sourceErrorsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
Name: "errors_total", Name: "errors_total",
Help: "Number of Source errors.", Help: "Number of Source errors.",
@ -53,7 +51,6 @@ var (
) )
sourceEndpointsTotal = metrics.NewGaugeWithOpts( sourceEndpointsTotal = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
Name: "endpoints_total", Name: "endpoints_total",
Help: "Number of Endpoints in all sources", Help: "Number of Endpoints in all sources",
@ -61,7 +58,6 @@ var (
) )
registryEndpointsTotal = metrics.NewGaugeWithOpts( registryEndpointsTotal = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
Name: "endpoints_total", Name: "endpoints_total",
Help: "Number of Endpoints in the registry", Help: "Number of Endpoints in the registry",
@ -69,7 +65,6 @@ var (
) )
lastSyncTimestamp = metrics.NewGaugeWithOpts( lastSyncTimestamp = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
Name: "last_sync_timestamp_seconds", Name: "last_sync_timestamp_seconds",
Help: "Timestamp of last successful sync with the DNS provider", Help: "Timestamp of last successful sync with the DNS provider",
@ -77,7 +72,6 @@ var (
) )
lastReconcileTimestamp = metrics.NewGaugeWithOpts( lastReconcileTimestamp = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
Name: "last_reconcile_timestamp_seconds", Name: "last_reconcile_timestamp_seconds",
Help: "Timestamp of last attempted sync with the DNS provider", Help: "Timestamp of last attempted sync with the DNS provider",
@ -85,7 +79,6 @@ var (
) )
controllerNoChangesTotal = metrics.NewCounterWithOpts( controllerNoChangesTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
Name: "no_op_runs_total", Name: "no_op_runs_total",
Help: "Number of reconcile loops ending up with no changes on the DNS provider side.", Help: "Number of reconcile loops ending up with no changes on the DNS provider side.",
@ -108,7 +101,6 @@ var (
registryRecords = metrics.NewGaugedVectorOpts( registryRecords = metrics.NewGaugedVectorOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
Name: "records", Name: "records",
Help: "Number of registry records partitioned by label name (vector).", Help: "Number of registry records partitioned by label name (vector).",
@ -118,7 +110,6 @@ var (
sourceRecords = metrics.NewGaugedVectorOpts( sourceRecords = metrics.NewGaugedVectorOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
Name: "records", Name: "records",
Help: "Number of source records partitioned by label name (vector).", Help: "Number of source records partitioned by label name (vector).",
@ -128,7 +119,6 @@ var (
verifiedRecords = metrics.NewGaugedVectorOpts( verifiedRecords = metrics.NewGaugedVectorOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
Name: "verified_records", Name: "verified_records",
Help: "Number of DNS records that exists both in source and registry (vector).", Help: "Number of DNS records that exists both in source and registry (vector).",
@ -138,7 +128,6 @@ var (
consecutiveSoftErrors = metrics.NewGaugeWithOpts( consecutiveSoftErrors = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
Name: "consecutive_soft_errors", Name: "consecutive_soft_errors",
Help: "Number of consecutive soft errors in reconciliation loop.", Help: "Number of consecutive soft errors in reconciliation loop.",

View File

@ -20,6 +20,7 @@ curl https://localhost:7979/metrics
| Name | Metric Type | Subsystem | Help | | Name | Metric Type | Subsystem | Help |
|:---------------------------------|:------------|:------------|:------------------------------------------------------| |:---------------------------------|:------------|:------------|:------------------------------------------------------|
| build_info | Gauge | | A metric with a constant '1' value labeled with 'version' and 'revision' of external_dns and the 'go_version', 'os' and the 'arch' used the build. |
| consecutive_soft_errors | Gauge | controller | Number of consecutive soft errors in reconciliation loop. | | consecutive_soft_errors | Gauge | controller | Number of consecutive soft errors in reconciliation loop. |
| last_reconcile_timestamp_seconds | Gauge | controller | Timestamp of last attempted sync with the DNS provider | | last_reconcile_timestamp_seconds | Gauge | controller | Timestamp of last attempted sync with the DNS provider |
| last_sync_timestamp_seconds | Gauge | controller | Timestamp of last successful sync with the DNS provider | | last_sync_timestamp_seconds | Gauge | controller | Timestamp of last successful sync with the DNS provider |

View File

@ -37,7 +37,7 @@ func TestComputeMetrics(t *testing.T) {
t.Errorf("Expected not empty metrics registry, got %d", len(reg.Metrics)) t.Errorf("Expected not empty metrics registry, got %d", len(reg.Metrics))
} }
assert.Len(t, reg.Metrics, 19) assert.Len(t, reg.Metrics, 20)
} }
func TestGenerateMarkdownTableRenderer(t *testing.T) { func TestGenerateMarkdownTableRenderer(t *testing.T) {

View File

@ -17,25 +17,44 @@ limitations under the License.
package metrics package metrics
import ( import (
"runtime" "fmt"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/version"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns" cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns"
) )
const (
Namespace = "external_dns"
)
var ( var (
RegisterMetric = NewMetricsRegister() RegisterMetric = NewMetricsRegister()
) )
func init() {
RegisterMetric.MustRegister(NewGaugeFuncMetric(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "build_info",
Help: fmt.Sprintf(
"A metric with a constant '1' value labeled with 'version' and 'revision' of %s and the 'go_version', 'os' and the 'arch' used the build.",
Namespace,
),
ConstLabels: prometheus.Labels{
"version": cfg.Version,
"revision": version.GetRevision(),
"go_version": version.GoVersion,
"os": version.GoOS,
"arch": version.GoArch,
},
}))
}
func NewMetricsRegister() *MetricRegistry { func NewMetricsRegister() *MetricRegistry {
reg := prometheus.WrapRegistererWith( reg := prometheus.WrapRegistererWith(
prometheus.Labels{ prometheus.Labels{},
"version": cfg.Version,
"arch": runtime.GOARCH,
"go_version": runtime.Version(),
},
prometheus.DefaultRegisterer) prometheus.DefaultRegisterer)
return &MetricRegistry{ return &MetricRegistry{
Registerer: reg, Registerer: reg,
@ -54,7 +73,7 @@ func NewMetricsRegister() *MetricRegistry {
// } // }
func (m *MetricRegistry) MustRegister(cs IMetric) { func (m *MetricRegistry) MustRegister(cs IMetric) {
switch v := cs.(type) { switch v := cs.(type) {
case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric: case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric, GaugeFuncMetric:
if _, exists := m.mName[cs.Get().FQDN]; exists { if _, exists := m.mName[cs.Get().FQDN]; exists {
return return
} else { } else {
@ -70,6 +89,8 @@ func (m *MetricRegistry) MustRegister(cs IMetric) {
m.Registerer.MustRegister(metric.Gauge) m.Registerer.MustRegister(metric.Gauge)
case CounterVecMetric: case CounterVecMetric:
m.Registerer.MustRegister(metric.CounterVec) m.Registerer.MustRegister(metric.CounterVec)
case GaugeFuncMetric:
m.Registerer.MustRegister(metric.GaugeFunc)
} }
log.Debugf("Register metric: %s", cs.Get().FQDN) log.Debugf("Register metric: %s", cs.Get().FQDN)
default: default:

View File

@ -88,6 +88,7 @@ func (g GaugeVecMetric) SetWithLabels(value float64, lvs ...string) {
} }
func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric { func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric {
opts.Namespace = Namespace
return GaugeMetric{ return GaugeMetric{
Metric: Metric{ Metric: Metric{
Type: "gauge", Type: "gauge",
@ -104,6 +105,7 @@ func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric {
// NewGaugedVectorOpts creates a new GaugeVec based on the provided GaugeOpts and // NewGaugedVectorOpts creates a new GaugeVec based on the provided GaugeOpts and
// partitioned by the given label names. // partitioned by the given label names.
func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVecMetric { func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVecMetric {
opts.Namespace = Namespace
return GaugeVecMetric{ return GaugeVecMetric{
Metric: Metric{ Metric: Metric{
Type: "gauge", Type: "gauge",
@ -118,6 +120,7 @@ func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVe
} }
func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric { func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric {
opts.Namespace = Namespace
return CounterMetric{ return CounterMetric{
Metric: Metric{ Metric: Metric{
Type: "counter", Type: "counter",
@ -132,6 +135,7 @@ func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric {
} }
func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) CounterVecMetric { func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) CounterVecMetric {
opts.Namespace = Namespace
return CounterVecMetric{ return CounterVecMetric{
Metric: Metric{ Metric: Metric{
Type: "counter", Type: "counter",
@ -144,3 +148,31 @@ func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) Cou
CounterVec: prometheus.NewCounterVec(opts, labelNames), CounterVec: prometheus.NewCounterVec(opts, labelNames),
} }
} }
type GaugeFuncMetric struct {
Metric
GaugeFunc prometheus.GaugeFunc
}
func (g GaugeFuncMetric) Get() *Metric {
return &g.Metric
}
func NewGaugeFuncMetric(opts prometheus.GaugeOpts) GaugeFuncMetric {
return GaugeFuncMetric{
Metric: Metric{
Type: "gauge",
Name: opts.Name,
FQDN: func() string {
if opts.Subsystem != "" {
return fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name)
}
return opts.Name
}(),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
},
GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }),
}
}

View File

@ -17,18 +17,17 @@ limitations under the License.
package metrics package metrics
import ( import (
"reflect"
"testing" "testing"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestNewGaugeWithOpts(t *testing.T) { func TestNewGaugeWithOpts(t *testing.T) {
opts := prometheus.GaugeOpts{ opts := prometheus.GaugeOpts{
Name: "test_gauge", Name: "test_gauge",
Namespace: "test_namespace",
Subsystem: "test_subsystem", Subsystem: "test_subsystem",
Help: "This is a test gauge", Help: "This is a test gauge",
} }
@ -37,7 +36,7 @@ func TestNewGaugeWithOpts(t *testing.T) {
assert.Equal(t, "gauge", gaugeMetric.Type) assert.Equal(t, "gauge", gaugeMetric.Type)
assert.Equal(t, "test_gauge", gaugeMetric.Name) assert.Equal(t, "test_gauge", gaugeMetric.Name)
assert.Equal(t, "test_namespace", gaugeMetric.Namespace) assert.Equal(t, Namespace, gaugeMetric.Namespace)
assert.Equal(t, "test_subsystem", gaugeMetric.Subsystem) assert.Equal(t, "test_subsystem", gaugeMetric.Subsystem)
assert.Equal(t, "This is a test gauge", gaugeMetric.Help) assert.Equal(t, "This is a test gauge", gaugeMetric.Help)
assert.Equal(t, "test_subsystem_test_gauge", gaugeMetric.FQDN) assert.Equal(t, "test_subsystem_test_gauge", gaugeMetric.FQDN)
@ -47,7 +46,6 @@ func TestNewGaugeWithOpts(t *testing.T) {
func TestNewCounterWithOpts(t *testing.T) { func TestNewCounterWithOpts(t *testing.T) {
opts := prometheus.CounterOpts{ opts := prometheus.CounterOpts{
Name: "test_counter", Name: "test_counter",
Namespace: "test_namespace",
Subsystem: "test_subsystem", Subsystem: "test_subsystem",
Help: "This is a test counter", Help: "This is a test counter",
} }
@ -56,7 +54,7 @@ func TestNewCounterWithOpts(t *testing.T) {
assert.Equal(t, "counter", counterMetric.Type) assert.Equal(t, "counter", counterMetric.Type)
assert.Equal(t, "test_counter", counterMetric.Name) assert.Equal(t, "test_counter", counterMetric.Name)
assert.Equal(t, "test_namespace", counterMetric.Namespace) assert.Equal(t, Namespace, counterMetric.Namespace)
assert.Equal(t, "test_subsystem", counterMetric.Subsystem) assert.Equal(t, "test_subsystem", counterMetric.Subsystem)
assert.Equal(t, "This is a test counter", counterMetric.Help) assert.Equal(t, "This is a test counter", counterMetric.Help)
assert.Equal(t, "test_subsystem_test_counter", counterMetric.FQDN) assert.Equal(t, "test_subsystem_test_counter", counterMetric.FQDN)
@ -77,7 +75,7 @@ func TestNewCounterVecWithOpts(t *testing.T) {
assert.Equal(t, "counter", counterVecMetric.Type) assert.Equal(t, "counter", counterVecMetric.Type)
assert.Equal(t, "test_counter_vec", counterVecMetric.Name) assert.Equal(t, "test_counter_vec", counterVecMetric.Name)
assert.Equal(t, "test_namespace", counterVecMetric.Namespace) assert.Equal(t, Namespace, counterVecMetric.Namespace)
assert.Equal(t, "test_subsystem", counterVecMetric.Subsystem) assert.Equal(t, "test_subsystem", counterVecMetric.Subsystem)
assert.Equal(t, "This is a test counter vector", counterVecMetric.Help) assert.Equal(t, "This is a test counter vector", counterVecMetric.Help)
assert.Equal(t, "test_subsystem_test_counter_vec", counterVecMetric.FQDN) assert.Equal(t, "test_subsystem_test_counter_vec", counterVecMetric.FQDN)
@ -113,3 +111,20 @@ func TestGaugeV_SetWithLabels(t *testing.T) {
assert.Len(t, m.Label, 2) assert.Len(t, m.Label, 2)
} }
func TestNewBuildInfoCollector(t *testing.T) {
metric := NewGaugeFuncMetric(prometheus.GaugeOpts{
Namespace: Namespace,
Name: "build_info",
ConstLabels: prometheus.Labels{
"version": "0.0.1",
"goversion": "1.24",
"arch": "arm64",
},
})
desc := metric.GaugeFunc.Desc()
assert.Equal(t, "external_dns_build_info", reflect.ValueOf(desc).Elem().FieldByName("fqName").String())
assert.Contains(t, desc.String(), "version=\"0.0.1\"")
}

View File

@ -31,7 +31,6 @@ import (
var ( var (
cachedRecordsCallsTotal = metrics.NewCounterVecWithOpts( cachedRecordsCallsTotal = metrics.NewCounterVecWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "provider", Subsystem: "provider",
Name: "cache_records_calls", Name: "cache_records_calls",
Help: "Number of calls to the provider cache Records list.", Help: "Number of calls to the provider cache Records list.",
@ -42,7 +41,6 @@ var (
) )
cachedApplyChangesCallsTotal = metrics.NewCounterWithOpts( cachedApplyChangesCallsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns",
Subsystem: "provider", Subsystem: "provider",
Name: "cache_apply_changes_calls", Name: "cache_apply_changes_calls",
Help: "Number of calls to the provider cache ApplyChanges.", Help: "Number of calls to the provider cache ApplyChanges.",

View File

@ -43,7 +43,6 @@ const (
var ( var (
recordsErrorsGauge = metrics.NewGaugeWithOpts( recordsErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
Name: "records_errors_total", Name: "records_errors_total",
Help: "Errors with Records method", Help: "Errors with Records method",
@ -51,7 +50,6 @@ var (
) )
recordsRequestsGauge = metrics.NewGaugeWithOpts( recordsRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
Name: "records_requests_total", Name: "records_requests_total",
Help: "Requests with Records method", Help: "Requests with Records method",
@ -59,7 +57,6 @@ var (
) )
applyChangesErrorsGauge = metrics.NewGaugeWithOpts( applyChangesErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
Name: "applychanges_errors_total", Name: "applychanges_errors_total",
Help: "Errors with ApplyChanges method", Help: "Errors with ApplyChanges method",
@ -67,7 +64,6 @@ var (
) )
applyChangesRequestsGauge = metrics.NewGaugeWithOpts( applyChangesRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
Name: "applychanges_requests_total", Name: "applychanges_requests_total",
Help: "Requests with ApplyChanges method", Help: "Requests with ApplyChanges method",
@ -75,7 +71,6 @@ var (
) )
adjustEndpointsErrorsGauge = metrics.NewGaugeWithOpts( adjustEndpointsErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
Name: "adjustendpoints_errors_total", Name: "adjustendpoints_errors_total",
Help: "Errors with AdjustEndpoints method", Help: "Errors with AdjustEndpoints method",
@ -83,7 +78,6 @@ var (
) )
adjustEndpointsRequestsGauge = metrics.NewGaugeWithOpts( adjustEndpointsRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
Name: "adjustendpoints_requests_total", Name: "adjustendpoints_requests_total",
Help: "Requests with AdjustEndpoints method", Help: "Requests with AdjustEndpoints method",