chore(docs): generate docs/monitoring/metrics.md file (#5117)

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

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

* chore(docs): generate docs/monitoring/metrics.md file

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.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-03-04 13:05:44 +00:00 committed by GitHub
parent 5f26223b6c
commit 1e8e5e06c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1072 additions and 179 deletions

View File

@ -47,7 +47,7 @@ golangci-lint-verify: golangci-lint
.PHONY: go-lint .PHONY: go-lint
go-lint: golangci-lint go-lint: golangci-lint
gofmt -l -s -w . gofmt -l -s -w .
golangci-lint run --timeout=30m ./... golangci-lint run --timeout=30m --fix ./...
#? licensecheck: Run the to check for license headers #? licensecheck: Run the to check for license headers
.PHONY: licensecheck .PHONY: licensecheck
@ -149,7 +149,6 @@ clean:
@rm -rf build @rm -rf build
@go clean -cache @go clean -cache
.PHONY: release.staging .PHONY: release.staging
#? release.staging: Builds and push container images to the staging bucket. #? release.staging: Builds and push container images to the staging bucket.
release.staging: test release.staging: test
@ -167,6 +166,11 @@ ko:
generate-flags-documentation: generate-flags-documentation:
go run internal/gen/docs/flags/main.go go run internal/gen/docs/flags/main.go
.PHONY: generate-metrics-documentation
#? generate-metrics-documentation: Generate documentation (docs/monitoring/metrics.md)
generate-metrics-documentation:
go run internal/gen/docs/metrics/main.go
#? pre-commit-install: Install pre-commit hooks #? pre-commit-install: Install pre-commit hooks
pre-commit-install: pre-commit-install:
@pre-commit install @pre-commit install

View File

@ -27,6 +27,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/metrics"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/registry" "sigs.k8s.io/external-dns/registry"
@ -34,7 +35,7 @@ import (
) )
var ( var (
registryErrorsTotal = prometheus.NewCounter( registryErrorsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
@ -42,7 +43,7 @@ var (
Help: "Number of Registry errors.", Help: "Number of Registry errors.",
}, },
) )
sourceErrorsTotal = prometheus.NewCounter( sourceErrorsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
@ -50,7 +51,7 @@ var (
Help: "Number of Source errors.", Help: "Number of Source errors.",
}, },
) )
sourceEndpointsTotal = prometheus.NewGauge( sourceEndpointsTotal = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
@ -58,7 +59,7 @@ var (
Help: "Number of Endpoints in all sources", Help: "Number of Endpoints in all sources",
}, },
) )
registryEndpointsTotal = prometheus.NewGauge( registryEndpointsTotal = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
@ -66,7 +67,7 @@ var (
Help: "Number of Endpoints in the registry", Help: "Number of Endpoints in the registry",
}, },
) )
lastSyncTimestamp = prometheus.NewGauge( lastSyncTimestamp = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
@ -74,7 +75,7 @@ var (
Help: "Timestamp of last successful sync with the DNS provider", Help: "Timestamp of last successful sync with the DNS provider",
}, },
) )
lastReconcileTimestamp = prometheus.NewGauge( lastReconcileTimestamp = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
@ -82,7 +83,7 @@ var (
Help: "Timestamp of last attempted sync with the DNS provider", Help: "Timestamp of last attempted sync with the DNS provider",
}, },
) )
controllerNoChangesTotal = prometheus.NewCounter( controllerNoChangesTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
@ -90,21 +91,21 @@ var (
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.",
}, },
) )
deprecatedRegistryErrors = prometheus.NewCounter( deprecatedRegistryErrors = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Subsystem: "registry", Subsystem: "registry",
Name: "errors_total", Name: "errors_total",
Help: "Number of Registry errors.", Help: "Number of Registry errors.",
}, },
) )
deprecatedSourceErrors = prometheus.NewCounter( deprecatedSourceErrors = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Subsystem: "source", Subsystem: "source",
Name: "errors_total", Name: "errors_total",
Help: "Number of Source errors.", Help: "Number of Source errors.",
}, },
) )
registryARecords = prometheus.NewGauge( registryARecords = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
@ -112,7 +113,7 @@ var (
Help: "Number of Registry A records.", Help: "Number of Registry A records.",
}, },
) )
registryAAAARecords = prometheus.NewGauge( registryAAAARecords = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "registry", Subsystem: "registry",
@ -120,7 +121,7 @@ var (
Help: "Number of Registry AAAA records.", Help: "Number of Registry AAAA records.",
}, },
) )
sourceARecords = prometheus.NewGauge( sourceARecords = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
@ -128,7 +129,7 @@ var (
Help: "Number of Source A records.", Help: "Number of Source A records.",
}, },
) )
sourceAAAARecords = prometheus.NewGauge( sourceAAAARecords = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "source", Subsystem: "source",
@ -136,7 +137,7 @@ var (
Help: "Number of Source AAAA records.", Help: "Number of Source AAAA records.",
}, },
) )
verifiedARecords = prometheus.NewGauge( verifiedARecords = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
@ -144,7 +145,7 @@ var (
Help: "Number of DNS A-records that exists both in source and registry.", Help: "Number of DNS A-records that exists both in source and registry.",
}, },
) )
verifiedAAAARecords = prometheus.NewGauge( verifiedAAAARecords = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "controller", Subsystem: "controller",
@ -155,21 +156,21 @@ var (
) )
func init() { func init() {
prometheus.MustRegister(registryErrorsTotal) metrics.RegisterMetric.MustRegister(registryErrorsTotal)
prometheus.MustRegister(sourceErrorsTotal) metrics.RegisterMetric.MustRegister(sourceErrorsTotal)
prometheus.MustRegister(sourceEndpointsTotal) metrics.RegisterMetric.MustRegister(sourceEndpointsTotal)
prometheus.MustRegister(registryEndpointsTotal) metrics.RegisterMetric.MustRegister(registryEndpointsTotal)
prometheus.MustRegister(lastSyncTimestamp) metrics.RegisterMetric.MustRegister(lastSyncTimestamp)
prometheus.MustRegister(lastReconcileTimestamp) metrics.RegisterMetric.MustRegister(lastReconcileTimestamp)
prometheus.MustRegister(deprecatedRegistryErrors) metrics.RegisterMetric.MustRegister(deprecatedRegistryErrors)
prometheus.MustRegister(deprecatedSourceErrors) metrics.RegisterMetric.MustRegister(deprecatedSourceErrors)
prometheus.MustRegister(controllerNoChangesTotal) metrics.RegisterMetric.MustRegister(controllerNoChangesTotal)
prometheus.MustRegister(registryARecords) metrics.RegisterMetric.MustRegister(registryARecords)
prometheus.MustRegister(registryAAAARecords) metrics.RegisterMetric.MustRegister(registryAAAARecords)
prometheus.MustRegister(sourceARecords) metrics.RegisterMetric.MustRegister(sourceARecords)
prometheus.MustRegister(sourceAAAARecords) metrics.RegisterMetric.MustRegister(sourceAAAARecords)
prometheus.MustRegister(verifiedARecords) metrics.RegisterMetric.MustRegister(verifiedARecords)
prometheus.MustRegister(verifiedAAAARecords) metrics.RegisterMetric.MustRegister(verifiedAAAARecords)
} }
// Controller is responsible for orchestrating the different components. // Controller is responsible for orchestrating the different components.
@ -203,7 +204,7 @@ type Controller struct {
// RunOnce runs a single iteration of a reconciliation loop. // RunOnce runs a single iteration of a reconciliation loop.
func (c *Controller) RunOnce(ctx context.Context) error { func (c *Controller) RunOnce(ctx context.Context) error {
lastReconcileTimestamp.SetToCurrentTime() lastReconcileTimestamp.Gauge.SetToCurrentTime()
c.runAtMutex.Lock() c.runAtMutex.Lock()
c.lastRunAt = time.Now() c.lastRunAt = time.Now()
@ -211,30 +212,30 @@ func (c *Controller) RunOnce(ctx context.Context) error {
records, err := c.Registry.Records(ctx) records, err := c.Registry.Records(ctx)
if err != nil { if err != nil {
registryErrorsTotal.Inc() registryErrorsTotal.Counter.Inc()
deprecatedRegistryErrors.Inc() deprecatedRegistryErrors.Counter.Inc()
return err return err
} }
registryEndpointsTotal.Set(float64(len(records))) registryEndpointsTotal.Gauge.Set(float64(len(records)))
regARecords, regAAAARecords := countAddressRecords(records) regARecords, regAAAARecords := countAddressRecords(records)
registryARecords.Set(float64(regARecords)) registryARecords.Gauge.Set(float64(regARecords))
registryAAAARecords.Set(float64(regAAAARecords)) registryAAAARecords.Gauge.Set(float64(regAAAARecords))
ctx = context.WithValue(ctx, provider.RecordsContextKey, records) ctx = context.WithValue(ctx, provider.RecordsContextKey, records)
endpoints, err := c.Source.Endpoints(ctx) endpoints, err := c.Source.Endpoints(ctx)
if err != nil { if err != nil {
sourceErrorsTotal.Inc() sourceErrorsTotal.Counter.Inc()
deprecatedSourceErrors.Inc() deprecatedSourceErrors.Counter.Inc()
return err return err
} }
sourceEndpointsTotal.Set(float64(len(endpoints))) sourceEndpointsTotal.Gauge.Set(float64(len(endpoints)))
srcARecords, srcAAAARecords := countAddressRecords(endpoints) srcARecords, srcAAAARecords := countAddressRecords(endpoints)
sourceARecords.Set(float64(srcARecords)) sourceARecords.Gauge.Set(float64(srcARecords))
sourceAAAARecords.Set(float64(srcAAAARecords)) sourceAAAARecords.Gauge.Set(float64(srcAAAARecords))
vARecords, vAAAARecords := countMatchingAddressRecords(endpoints, records) vARecords, vAAAARecords := countMatchingAddressRecords(endpoints, records)
verifiedARecords.Set(float64(vARecords)) verifiedARecords.Gauge.Set(float64(vARecords))
verifiedAAAARecords.Set(float64(vAAAARecords)) verifiedAAAARecords.Gauge.Set(float64(vAAAARecords))
endpoints, err = c.Registry.AdjustEndpoints(endpoints) endpoints, err = c.Registry.AdjustEndpoints(endpoints)
if err != nil { if err != nil {
return fmt.Errorf("adjusting endpoints: %w", err) return fmt.Errorf("adjusting endpoints: %w", err)
@ -256,16 +257,16 @@ func (c *Controller) RunOnce(ctx context.Context) error {
if plan.Changes.HasChanges() { if plan.Changes.HasChanges() {
err = c.Registry.ApplyChanges(ctx, plan.Changes) err = c.Registry.ApplyChanges(ctx, plan.Changes)
if err != nil { if err != nil {
registryErrorsTotal.Inc() registryErrorsTotal.Counter.Inc()
deprecatedRegistryErrors.Inc() deprecatedRegistryErrors.Counter.Inc()
return err return err
} }
} else { } else {
controllerNoChangesTotal.Inc() controllerNoChangesTotal.Counter.Inc()
log.Info("All records are already up to date") log.Info("All records are already up to date")
} }
lastSyncTimestamp.SetToCurrentTime() lastSyncTimestamp.Gauge.SetToCurrentTime()
return nil return nil
} }

View File

@ -234,8 +234,8 @@ func TestRunOnce(t *testing.T) {
// Validate that the mock source was called. // Validate that the mock source was called.
source.AssertExpectations(t) source.AssertExpectations(t)
// check the verified records // check the verified records
assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedARecords)) assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedARecords.Gauge))
assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedAAAARecords)) assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedAAAARecords.Gauge))
} }
// TestRun tests that Run correctly starts and stops // TestRun tests that Run correctly starts and stops
@ -268,8 +268,8 @@ func TestRun(t *testing.T) {
// Validate that the mock source was called. // Validate that the mock source was called.
source.AssertExpectations(t) source.AssertExpectations(t)
// check the verified records // check the verified records
assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedARecords)) assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedARecords.Gauge))
assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedAAAARecords)) assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedAAAARecords.Gauge))
} }
func valueFromMetric(metric prometheus.Gauge) uint64 { func valueFromMetric(metric prometheus.Gauge) uint64 {
@ -520,7 +520,7 @@ func TestVerifyARecords(t *testing.T) {
}, },
[]*plan.Changes{}, []*plan.Changes{},
) )
assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords)) assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords.Gauge))
testControllerFiltersDomains( testControllerFiltersDomains(
t, t,
@ -564,8 +564,8 @@ func TestVerifyARecords(t *testing.T) {
}, },
}}, }},
) )
assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords)) assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords.Gauge))
assert.Equal(t, math.Float64bits(0), valueFromMetric(verifiedAAAARecords)) assert.Equal(t, math.Float64bits(0), valueFromMetric(verifiedAAAARecords.Gauge))
} }
func TestVerifyAAAARecords(t *testing.T) { func TestVerifyAAAARecords(t *testing.T) {
@ -598,7 +598,7 @@ func TestVerifyAAAARecords(t *testing.T) {
}, },
[]*plan.Changes{}, []*plan.Changes{},
) )
assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedAAAARecords)) assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedAAAARecords.Gauge))
testControllerFiltersDomains( testControllerFiltersDomains(
t, t,
@ -642,8 +642,8 @@ func TestVerifyAAAARecords(t *testing.T) {
}, },
}}, }},
) )
assert.Equal(t, math.Float64bits(0), valueFromMetric(verifiedARecords)) assert.Equal(t, math.Float64bits(0), valueFromMetric(verifiedARecords.Gauge))
assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedAAAARecords)) assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedAAAARecords.Gauge))
} }
func TestARecords(t *testing.T) { func TestARecords(t *testing.T) {
@ -689,8 +689,8 @@ func TestARecords(t *testing.T) {
}, },
}}, }},
) )
assert.Equal(t, math.Float64bits(2), valueFromMetric(sourceARecords)) assert.Equal(t, math.Float64bits(2), valueFromMetric(sourceARecords.Gauge))
assert.Equal(t, math.Float64bits(1), valueFromMetric(registryARecords)) assert.Equal(t, math.Float64bits(1), valueFromMetric(registryARecords.Gauge))
} }
func TestAAAARecords(t *testing.T) { func TestAAAARecords(t *testing.T) {
@ -736,6 +736,6 @@ func TestAAAARecords(t *testing.T) {
}, },
}}, }},
) )
assert.Equal(t, math.Float64bits(2), valueFromMetric(sourceAAAARecords)) assert.Equal(t, math.Float64bits(2), valueFromMetric(sourceAAAARecords.Gauge))
assert.Equal(t, math.Float64bits(1), valueFromMetric(registryAAAARecords)) assert.Equal(t, math.Float64bits(1), valueFromMetric(registryAAAARecords.Gauge))
} }

View File

@ -38,10 +38,11 @@ make test
make cover-html make cover-html
``` ```
If added any flags, re-generate flags documentation If added any flags or metrics, re-generate documentation
```shell ```shell
make generate-flags-documentation make generate-flags-documentation
make generate-metrics-documentation
``` ```
We require all changes to be covered by acceptance tests and/or unit tests, depending on the situation. We require all changes to be covered by acceptance tests and/or unit tests, depending on the situation.

View File

@ -182,48 +182,6 @@ or
You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group's scope. You need to add either https://www.googleapis.com/auth/ndev.clouddns.readwrite or https://www.googleapis.com/auth/cloud-platform on your instance group's scope.
## What metrics can I get from ExternalDNS and what do they mean?
ExternalDNS exposes 2 types of metrics: Sources and Registry errors.
`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions.
It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc.
`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records.
When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.
In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down).
You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors).
Here is the full list of available metrics provided by ExternalDNS:
| Name | Description | Type |
| -------------------------------------------------------- | ------------------------------------------------------------------ | ------- |
| external_dns_controller_last_sync_timestamp_seconds | Timestamp of last successful sync with the DNS provider | Gauge |
| external_dns_controller_last_reconcile_timestamp_seconds | Timestamp of last attempted sync with the DNS provider | Gauge |
| external_dns_registry_endpoints_total | Number of Endpoints in all sources | Gauge |
| external_dns_registry_errors_total | Number of Registry errors | Counter |
| external_dns_source_endpoints_total | Number of Endpoints in the registry | Gauge |
| external_dns_source_errors_total | Number of Source errors | Counter |
| external_dns_controller_verified_aaaa_records | Number of DNS AAAA-records that exists both in source and registry | Gauge |
| external_dns_controller_verified_a_records | Number of DNS A-records that exists both in source and registry | Gauge |
| external_dns_registry_aaaa_records | Number of AAAA records in registry | Gauge |
| external_dns_registry_a_records | Number of A records in registry | Gauge |
| external_dns_source_aaaa_records | Number of AAAA records in source | Gauge |
| external_dns_source_a_records | Number of A records in source | Gauge |
If you're using the webhook provider, the following additional metrics will be provided:
| Name | Description | Type |
| ------------------------------------------------------------ | ------------------------------------------------------ | ------- |
| external_dns_webhook_provider_records_errors_total | Number of errors with the /records method | Gauge |
| external_dns_webhook_provider_records_requests_total | Number of requests made to the /records method | Gauge |
| external_dns_webhook_provider_applychanges_errors_total | Number of errors with the /applychanges method | Gauge |
| external_dns_webhook_provider_applychanges_requests_total | Number of requests made to the /applychanges method | Gauge |
| external_dns_webhook_provider_adjustendpoints_errors_total | Number of errors with the /adjustendpoints method | Gauge |
| external_dns_webhook_provider_adjustendpoints_requests_total | Number of requests made to the /adjustendpoints method | Gauge |
## How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects? ## How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects?
Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions Have a look at https://github.com/linki/mate/blob/v0.6.2/examples/google/README.md#permissions

50
docs/monitoring/index.md Normal file
View File

@ -0,0 +1,50 @@
# Monitoring & Observability
Monitoring is a crucial aspect of maintaining the health and performance of your applications.
It involves collecting, analyzing, and using information to ensure that your system is running smoothly and efficiently. Effective monitoring helps in identifying issues early, understanding system behavior, and making informed decisions to improve performance and reliability.
For `external-dns`, all metrics available for scraping are exposed on the `/metrics` endpoint. The metrics are in the Prometheus exposition format, which is widely used for monitoring and alerting.
To access the metrics:
```sh
curl https://localhost:7979/metrics
```
In the metrics output, you'll see the help text, type information, and current value of the `external_dns_registry_endpoints_total` counter:
```yml
# HELP external_dns_registry_endpoints_total Number of Endpoints in the registry
# TYPE external_dns_registry_endpoints_total gauge
external_dns_registry_endpoints_total 11
```
You can configure a locally running [Prometheus instance](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) to scrape metrics from the application. Here's an example prometheus.yml configuration:
```yml
scrape_configs:
- job_name: external-dns
scrape_interval: 10s
static_configs:
- targets:
- localhost:7979
```
For more detailed information on how to instrument application with Prometheus, you can refer to the [Prometheus Go client library documentation](https://prometheus.io/docs/guides/go-application/).
## What metrics can I get from ExternalDNS and what do they mean?
- The project maintain a [metrics page](./metrics.md) with a list of supported custom metrics.
- [Go runtime](https://pkg.go.dev/runtime/metrics#hdr-Supported_metrics) metrics also available for scraping.
ExternalDNS exposes 3 types of metrics: Sources, Registry errors and Cache hits.
`Source`s are mostly Kubernetes API objects. Examples of `source` errors may be connection errors to the Kubernetes API server itself or missing RBAC permissions.
It can also stem from incompatible configuration in the objects itself like invalid characters, processing a broken fqdnTemplate, etc.
`Registry` errors are mostly Provider errors, unless there's some coding flaw in the registry package. Provider errors often arise due to accessing their APIs due to network or missing cloud-provider permissions when reading records.
When applying a changeset, errors will arise if the changeset applied is incompatible with the current state.
In case of an increased error count, you could correlate them with the `http_request_duration_seconds{handler="instrumented_http"}` metric which should show increased numbers for status codes 4xx (permissions, configuration, invalid changeset) or 5xx (apiserver down).
You can use the host label in the metric to figure out if the request was against the Kubernetes API server (Source errors) or the DNS provider API (Registry/Provider errors).

View File

@ -0,0 +1,89 @@
# Available Metrics
<!-- THIS FILE MUST NOT BE EDITED BY HAND -->
<!-- ON NEW METRIC ADDED PLEASE RUN 'make generate-metrics-documentation' -->
<!-- markdownlint-disable MD013 -->
All metrics available for scraping are exposed on the `/metrics` endpoint.
The metrics are in the Prometheus exposition format.
To access the metrics:
```sh
curl https://localhost:7979/metrics
```
## Supported Metrics
> Full metric name is constructed as follows:
> `external_dns_<subsystem>_<name>`
| Name | Metric Type | Subsystem | Help |
|:---------------------------------|:------------|:------------|:------------------------------------------------------|
| 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 |
| no_op_runs_total | Counter | controller | Number of reconcile loops ending up with no changes on the DNS provider side. |
| verified_a_records | Gauge | controller | Number of DNS A-records that exists both in source and registry. |
| verified_aaaa_records | Gauge | controller | Number of DNS AAAA-records that exists both in source and registry. |
| cache_apply_changes_calls | Counter | provider | Number of calls to the provider cache ApplyChanges. |
| cache_records_calls | Counter | provider | Number of calls to the provider cache Records list. |
| a_records | Gauge | registry | Number of Registry A records. |
| aaaa_records | Gauge | registry | Number of Registry AAAA records. |
| endpoints_total | Gauge | registry | Number of Endpoints in the registry |
| errors_total | Counter | registry | Number of Registry errors. |
| a_records | Gauge | source | Number of Source A records. |
| aaaa_records | Gauge | source | Number of Source AAAA records. |
| endpoints_total | Gauge | source | Number of Endpoints in all sources |
| errors_total | Counter | source | Number of Source errors. |
| adjustendpoints_errors_total | Gauge | webhook_provider | Errors with AdjustEndpoints method |
| adjustendpoints_requests_total | Gauge | webhook_provider | Requests with AdjustEndpoints method |
| applychanges_errors_total | Gauge | webhook_provider | Errors with ApplyChanges method |
| applychanges_requests_total | Gauge | webhook_provider | Requests with ApplyChanges method |
| records_errors_total | Gauge | webhook_provider | Errors with Records method |
| records_requests_total | Gauge | webhook_provider | Requests with Records method |
## Available Go Runtime Metrics
> The following Go runtime metrics are available for scraping. Please note that they may change over time and they are OS dependent.
| Name |
|:----------------------|
| go_gc_duration_seconds |
| go_gc_gogc_percent |
| go_gc_gomemlimit_bytes |
| go_goroutines |
| go_info |
| go_memstats_alloc_bytes |
| go_memstats_alloc_bytes_total |
| go_memstats_buck_hash_sys_bytes |
| go_memstats_frees_total |
| go_memstats_gc_sys_bytes |
| go_memstats_heap_alloc_bytes |
| go_memstats_heap_idle_bytes |
| go_memstats_heap_inuse_bytes |
| go_memstats_heap_objects |
| go_memstats_heap_released_bytes |
| go_memstats_heap_sys_bytes |
| go_memstats_last_gc_time_seconds |
| go_memstats_mallocs_total |
| go_memstats_mcache_inuse_bytes |
| go_memstats_mcache_sys_bytes |
| go_memstats_mspan_inuse_bytes |
| go_memstats_mspan_sys_bytes |
| go_memstats_next_gc_bytes |
| go_memstats_other_sys_bytes |
| go_memstats_stack_inuse_bytes |
| go_memstats_stack_sys_bytes |
| go_memstats_sys_bytes |
| go_sched_gomaxprocs_threads |
| go_threads |
| http_request_duration_seconds |
| process_cpu_seconds_total |
| process_max_fds |
| process_open_fds |
| process_resident_memory_bytes |
| process_start_time_seconds |
| process_virtual_memory_bytes |
| process_virtual_memory_max_bytes |
| process_network_receive_bytes_total |
| process_network_transmit_bytes_total |

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2017 The Kubernetes Authors. Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,14 +18,21 @@ package main
import ( import (
"bytes" "bytes"
"embed"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"text/template" "text/template"
"sigs.k8s.io/external-dns/internal/gen/docs/utils"
cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns" cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns"
) )
var (
//go:embed "templates/*"
templates embed.FS
)
type Flag struct { type Flag struct {
Name string Name string
Description string Description string
@ -37,19 +44,6 @@ func (f *Flags) addFlag(name, description string) {
*f = append(*f, Flag{Name: name, Description: description}) *f = append(*f, Flag{Name: name, Description: description})
} }
const markdownTemplate = `# Flags
<!-- THIS FILE MUST NOT BE EDITED BY HAND -->
<!-- ON NEW FLAG ADDED PLEASE RUN 'make generate-flags-documentation' -->
<!-- markdownlint-disable MD013 -->
| Flag | Description |
| :------ | :----------- |
{{- range . }}
| {{ .Name }} | {{ .Description }} |
{{- end -}}
`
// It generates a markdown file // It generates a markdown file
// with the supported flags and writes it to the 'docs/flags.md' file. // with the supported flags and writes it to the 'docs/flags.md' file.
// to re-generate `docs/flags.md` execute 'go run internal/gen/main.go' // to re-generate `docs/flags.md` execute 'go run internal/gen/main.go'
@ -64,7 +58,7 @@ func main() {
_ = fmt.Errorf("failed to generate markdown file '%s': %v\n", path, err.Error()) _ = fmt.Errorf("failed to generate markdown file '%s': %v\n", path, err.Error())
} }
content = content + "\n" content = content + "\n"
_ = writeToFile(path, content) _ = utils.WriteToFile(path, content)
} }
func computeFlags() Flags { func computeFlags() Flags {
@ -94,25 +88,13 @@ func computeFlags() Flags {
} }
func (f *Flags) generateMarkdownTable() (string, error) { func (f *Flags) generateMarkdownTable() (string, error) {
tmpl := template.Must(template.New("flags.md.tpl").Parse(markdownTemplate)) tmpl := template.New("").Funcs(utils.FuncMap())
template.Must(tmpl.ParseFS(templates, "templates/*.gotpl"))
var b bytes.Buffer var b bytes.Buffer
err := tmpl.Execute(&b, f) err := tmpl.ExecuteTemplate(&b, "flags.gotpl", f)
if err != nil { if err != nil {
return "", err return "", err
} }
return b.String(), nil return b.String(), nil
} }
func writeToFile(filename string, content string) error {
file, fileErr := os.Create(filename)
if fileErr != nil {
_ = fmt.Errorf("failed to create file: %v", fileErr)
}
defer file.Close()
_, writeErr := file.WriteString(content)
if writeErr != nil {
_ = fmt.Errorf("failed to write to file: %s", filename)
}
return nil
}

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2017 The Kubernetes Authors. Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -0,0 +1,11 @@
# Flags
<!-- THIS FILE MUST NOT BE EDITED BY HAND -->
<!-- ON NEW FLAG ADDED PLEASE RUN 'make generate-flags-documentation' -->
<!-- markdownlint-disable MD013 -->
| Flag | Description |
| :------ | :----------- |
{{- range . }}
| {{ .Name }} | {{ .Description }} |
{{- end -}}

View File

@ -0,0 +1,122 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"embed"
"fmt"
"os"
"reflect"
"sort"
"strings"
"text/template"
"unsafe"
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/external-dns/internal/gen/docs/utils"
"sigs.k8s.io/external-dns/pkg/metrics"
// the imports is necessary for the code generation process.
_ "sigs.k8s.io/external-dns/controller"
_ "sigs.k8s.io/external-dns/provider"
_ "sigs.k8s.io/external-dns/provider/webhook"
)
var (
//go:embed "templates/*"
templates embed.FS
)
func main() {
testPath, _ := os.Getwd()
path := fmt.Sprintf("%s/docs/monitoring/metrics.md", testPath)
fmt.Printf("generate file '%s' with configured metrics\n", path)
content, err := generateMarkdownTable(metrics.RegisterMetric, true)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to generate markdown file '%s': %v\n", path, err)
os.Exit(1)
}
content = content + "\n"
_ = utils.WriteToFile(path, content)
}
func generateMarkdownTable(m *metrics.MetricRegistry, withRuntime bool) (string, error) {
tmpl := template.New("").Funcs(utils.FuncMap())
template.Must(tmpl.ParseFS(templates, "templates/*.gotpl"))
sortMetrics(m.Metrics)
var runtimeMetrics []string
if withRuntime {
runtimeMetrics = getRuntimeMetrics(prometheus.DefaultRegisterer)
// available when promhttp.Handler() is activated
runtimeMetrics = append(runtimeMetrics, []string{
"process_network_receive_bytes_total",
"process_network_transmit_bytes_total",
}...)
} else {
runtimeMetrics = []string{}
}
var b bytes.Buffer
err := tmpl.ExecuteTemplate(&b, "metrics.gotpl", struct {
Metrics []*metrics.Metric
RuntimeMetrics []string
}{
Metrics: m.Metrics,
RuntimeMetrics: runtimeMetrics,
})
if err != nil {
return "", err
}
return b.String(), nil
}
// sortMetrics sorts the given slice of metrics by their subsystem and name.
// Metrics are first sorted by their subsystem, and then by their name within each subsystem.
func sortMetrics(metrics []*metrics.Metric) {
sort.Slice(metrics, func(i, j int) bool {
if metrics[i].Subsystem == metrics[j].Subsystem {
return metrics[i].Name < metrics[j].Name
}
return metrics[i].Subsystem < metrics[j].Subsystem
})
}
// getRuntimeMetrics retrieves the list of runtime metrics from the Prometheus library.
func getRuntimeMetrics(reg prometheus.Registerer) []string {
var runtimeMetrics []string
// hacks to get the runtime metrics from prometheus library
// safe to do because it's a just a documentation generator
values := reflect.ValueOf(reg).Elem().FieldByName("dimHashesByName")
values = reflect.NewAt(values.Type(), unsafe.Pointer(values.UnsafeAddr())).Elem()
switch v := values.Interface().(type) {
case map[string]uint64:
for k := range v {
if !strings.HasPrefix(k, "external_dns") {
runtimeMetrics = append(runtimeMetrics, k)
}
}
}
sort.Strings(runtimeMetrics)
return runtimeMetrics
}

View File

@ -0,0 +1,133 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"io/fs"
"math/rand/v2"
"os"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/pkg/metrics"
)
const pathToDocs = "%s/../../../../docs/monitoring"
func TestComputeMetrics(t *testing.T) {
reg := metrics.RegisterMetric
if len(reg.Metrics) == 0 {
t.Errorf("Expected not empty metrics registry, got %d", len(reg.Metrics))
}
assert.Len(t, reg.Metrics, 21)
}
func TestGenerateMarkdownTableRenderer(t *testing.T) {
reg := metrics.NewMetricsRegister()
got, err := generateMarkdownTable(reg, false)
assert.NoError(t, err)
assert.Contains(t, got, "# Available Metrics\n\n<!-- THIS FILE MUST NOT BE EDITED BY HAND -->\n")
assert.Contains(t, got, "| Metric Type | Subsystem | Help")
}
func TestGenerateMarkdownTableWithSingleMetric(t *testing.T) {
reg := metrics.NewMetricsRegister()
reg.MustRegister(metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: fmt.Sprintf("controller_%d", rand.IntN(100)),
Name: "verified_aaaa_records",
Help: "This is just a test.",
},
))
got, err := generateMarkdownTable(reg, false)
assert.NoError(t, err)
assert.Contains(t, got, "verified_aaaa_records")
assert.Contains(t, got, "This is just a test.")
}
func TestMetricsMdUpToDate(t *testing.T) {
testPath, _ := os.Getwd()
fsys := os.DirFS(fmt.Sprintf(pathToDocs, testPath))
fileName := "metrics.md"
expected, err := fs.ReadFile(fsys, fileName)
assert.NoError(t, err, "expected file %s to exist", fileName)
reg := metrics.RegisterMetric
actual, err := generateMarkdownTable(reg, false)
assert.NoError(t, err)
assert.Contains(t, string(expected), actual)
}
func TestMetricsMdExtraMetricAdded(t *testing.T) {
testPath, _ := os.Getwd()
fsys := os.DirFS(fmt.Sprintf(pathToDocs, testPath))
fileName := "metrics.md"
expected, err := fs.ReadFile(fsys, fileName)
assert.NoError(t, err, "expected file %s to exist", fileName)
reg := metrics.RegisterMetric
reg.MustRegister(metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: fmt.Sprintf("controller_%d", rand.IntN(100)),
Name: "verified_aaaa_records",
Help: "This is just a test.",
},
))
actual, err := generateMarkdownTable(reg, false)
assert.NoError(t, err)
assert.NotEqual(t, string(expected), actual)
}
func TestGetRuntimeMetricsForNewRegistry(t *testing.T) {
reg := prometheus.NewRegistry()
// Register some runtime metrics
reg.MustRegister(prometheus.NewGauge(prometheus.GaugeOpts{
Name: "go_goroutines",
Help: "Number of goroutines that currently exist.",
}))
reg.MustRegister(prometheus.NewGauge(prometheus.GaugeOpts{
Name: "go_memstats_alloc_bytes",
Help: "Number of bytes allocated and still in use.",
}))
runtimeMetrics := getRuntimeMetrics(reg)
// Check that the runtime metrics are correctly retrieved
expectedMetrics := []string{"go_goroutines", "go_memstats_alloc_bytes"}
assert.ElementsMatch(t, expectedMetrics, runtimeMetrics)
assert.Len(t, runtimeMetrics, 2)
}
func TestGetRuntimeMetricsForDefaultRegistry(t *testing.T) {
reg := prometheus.DefaultRegisterer
runtimeMetrics := getRuntimeMetrics(reg)
if len(runtimeMetrics) == 0 {
t.Errorf("Expected not empty runtime metrics, got %d", len(runtimeMetrics))
}
}

View File

@ -0,0 +1,37 @@
# Available Metrics
<!-- THIS FILE MUST NOT BE EDITED BY HAND -->
<!-- ON NEW METRIC ADDED PLEASE RUN 'make generate-metrics-documentation' -->
<!-- markdownlint-disable MD013 -->
All metrics available for scraping are exposed on the {{backtick 1}}/metrics{{backtick 1}} endpoint.
The metrics are in the Prometheus exposition format.
To access the metrics:
{{backtick 3}}sh
curl https://localhost:7979/metrics
{{backtick 3}}
## Supported Metrics
> Full metric name is constructed as follows:
> {{backtick 1}}external_dns_<subsystem>_<name>{{backtick 1}}
| Name | Metric Type | Subsystem | Help |
|:---------------------------------|:------------|:------------|:------------------------------------------------------|
{{- range .Metrics }}
| {{ .Name }} | {{ .Type | capitalize }} | {{ .Subsystem }} | {{ .Help }} |
{{- end }}
## Available Go Runtime Metrics
> The following Go runtime metrics are available for scraping. Please note that they may change over time and they are OS dependent.
{{ if .RuntimeMetrics -}}
| Name |
|:----------------------|
{{- range .RuntimeMetrics }}
| {{ . }} |
{{- end -}}
{{- end -}}

View File

@ -0,0 +1,50 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"fmt"
"os"
"strings"
"text/template"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func WriteToFile(filename string, content string) error {
file, fileErr := os.Create(filename)
if fileErr != nil {
_ = fmt.Errorf("failed to create file: %v", fileErr)
}
defer file.Close()
_, writeErr := file.WriteString(content)
if writeErr != nil {
_ = fmt.Errorf("failed to write to file: %s", filename)
}
return nil
}
// FuncMap returns a mapping of all of the functions that Engine has.
func FuncMap() template.FuncMap {
return template.FuncMap{
"backtick": func(times int) string {
return strings.Repeat("`", times)
},
"capitalize": cases.Title(language.English, cases.Compact).String,
}
}

View File

@ -0,0 +1,73 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"fmt"
"os"
"strings"
"testing"
"text/template"
"github.com/stretchr/testify/assert"
)
func TestWriteToFile(t *testing.T) {
filename := fmt.Sprintf("%s/testfile", t.TempDir())
content := "Hello, World!"
defer os.Remove(filename)
err := WriteToFile(filename, content)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
data, err := os.ReadFile(filename)
if err != nil {
t.Fatalf("expected no error reading file, got %v", err)
}
if string(data) != content {
t.Errorf("expected content %q, got %q", content, string(data))
}
}
func TestFuncs(t *testing.T) {
tests := []struct {
tpl, expect string
vars interface{}
}{
{
tpl: `{{ backtick 3 }}`,
expect: "```",
vars: map[string]interface{}{},
},
{
tpl: `{{ capitalize .name }}`,
expect: "Capital",
vars: map[string]interface{}{"name": "capital"},
},
}
for _, tt := range tests {
var b strings.Builder
err := template.Must(template.New("test").Funcs(FuncMap()).Parse(tt.tpl)).Execute(&b, tt.vars)
assert.NoError(t, err)
assert.Equal(t, tt.expect, b.String(), tt.tpl)
}
}

View File

@ -39,6 +39,7 @@ import (
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/pkg/apis/externaldns/validation" "sigs.k8s.io/external-dns/pkg/apis/externaldns/validation"
"sigs.k8s.io/external-dns/pkg/metrics"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/provider/akamai" "sigs.k8s.io/external-dns/provider/akamai"
@ -449,9 +450,13 @@ func handleSigterm(cancel func()) {
func serveMetrics(address string) { func serveMetrics(address string) {
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte("OK")) _, _ = w.Write([]byte("OK"))
}) })
log.Debugf("serving 'healthz' on 'localhost:%s/healthz'", address)
log.Debugf("serving 'metrics' on 'localhost:%s/metrics'", address)
log.Debugf("registered '%d' metrics", len(metrics.RegisterMetric.Metrics))
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(address, nil)) log.Fatal(http.ListenAndServe(address, nil))

View File

@ -27,6 +27,7 @@ nav:
- Advanced Topics: - Advanced Topics:
- Initial Design: docs/initial-design.md - Initial Design: docs/initial-design.md
- Leader Election: docs/proposal/001-leader-election.md - Leader Election: docs/proposal/001-leader-election.md
- Monitoring: docs/monitoring/*
- MultiTarget: docs/proposal/multi-target.md - MultiTarget: docs/proposal/multi-target.md
- NAT64: docs/nat64.md - NAT64: docs/nat64.md
- Rate Limits: docs/rate-limits.md - Rate Limits: docs/rate-limits.md

77
pkg/metrics/metrics.go Normal file
View File

@ -0,0 +1,77 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"runtime"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns"
)
var (
RegisterMetric = NewMetricsRegister()
)
func NewMetricsRegister() *MetricRegistry {
reg := prometheus.WrapRegistererWith(
prometheus.Labels{
"version": cfg.Version,
"arch": runtime.GOARCH,
"go_version": runtime.Version(),
},
prometheus.DefaultRegisterer)
return &MetricRegistry{
Registerer: reg,
Metrics: []*Metric{},
mName: make(map[string]bool),
}
}
// MustRegister registers a metric if it hasn't been registered yet.
//
// Usage: MustRegister(...)
// Example:
//
// func init() {
// metrics.RegisterMetric.MustRegister(errorsTotal)
// }
func (m *MetricRegistry) MustRegister(cs IMetric) {
switch v := cs.(type) {
case CounterMetric, GaugeMetric, CounterVecMetric:
if _, exists := m.mName[cs.Get().FQDN]; exists {
return
} else {
m.mName[cs.Get().FQDN] = true
}
m.Metrics = append(m.Metrics, cs.Get())
switch metric := v.(type) {
case CounterMetric:
m.Registerer.MustRegister(metric.Counter)
case GaugeMetric:
m.Registerer.MustRegister(metric.Gauge)
case CounterVecMetric:
m.Registerer.MustRegister(metric.CounterVec)
}
log.Debugf("Register metric: %s", cs.Get().FQDN)
default:
log.Warnf("Unsupported metric type: %T", v)
return
}
}

103
pkg/metrics/metrics_test.go Normal file
View File

@ -0,0 +1,103 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/internal/testutils"
)
type MockMetric struct {
FQDN string
}
func (m *MockMetric) Get() *Metric {
return &Metric{FQDN: m.FQDN}
}
func TestMustRegister(t *testing.T) {
tests := []struct {
name string
metrics []IMetric
expected int
}{
{
name: "single metric",
metrics: []IMetric{
NewCounterWithOpts(prometheus.CounterOpts{Name: "test_counter_1"}),
},
expected: 1,
},
{
name: "two metrics",
metrics: []IMetric{
NewGaugeWithOpts(prometheus.GaugeOpts{Name: "test_gauge_2", Subsystem: "test"}),
NewCounterWithOpts(prometheus.CounterOpts{Name: "test_counter_2", Subsystem: "app"}),
},
expected: 2,
},
{
name: "mix of metrics",
metrics: []IMetric{
NewGaugeWithOpts(prometheus.GaugeOpts{Name: "test_gauge_3"}),
NewCounterWithOpts(prometheus.CounterOpts{Name: "test_counter_3"}),
NewCounterVecWithOpts(prometheus.CounterOpts{Name: "test_counter_vec_3"}, []string{"label"}),
},
expected: 3,
},
{
name: "unsupported metric",
metrics: []IMetric{
&MockMetric{FQDN: "unsupported_metric"},
},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
registry := NewMetricsRegister()
for _, m := range tt.metrics {
registry.MustRegister(m)
}
assert.Len(t, registry.Metrics, tt.expected)
})
}
}
func TestUnsupportedMetricWarning(t *testing.T) {
buf := testutils.LogsToBuffer(log.WarnLevel, t)
registry := NewMetricsRegister()
mockUnsupported := &MockMetric{FQDN: "unsupported_metric"}
registry.MustRegister(mockUnsupported)
assert.NotContains(t, registry.mName, "unsupported_metric")
assert.Contains(t, buf.String(), "Unsupported metric type: *metrics.MockMetric")
}
func TestNewMetricsRegister(t *testing.T) {
registry := NewMetricsRegister()
assert.NotNil(t, registry)
assert.NotNil(t, registry.Registerer)
assert.Empty(t, registry.Metrics)
assert.Empty(t, registry.mName)
}

111
pkg/metrics/models.go Normal file
View File

@ -0,0 +1,111 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"fmt"
"github.com/prometheus/client_golang/prometheus"
)
type MetricRegistry struct {
Registerer prometheus.Registerer
Metrics []*Metric
mName map[string]bool
}
type Metric struct {
Type string
Namespace string
Subsystem string
Name string
Help string
FQDN string
}
type IMetric interface {
Get() *Metric
}
type GaugeMetric struct {
Metric
Gauge prometheus.Gauge
}
func (g GaugeMetric) Get() *Metric {
return &g.Metric
}
type CounterMetric struct {
Metric
Counter prometheus.Counter
}
func (g CounterMetric) Get() *Metric {
return &g.Metric
}
type CounterVecMetric struct {
Metric
CounterVec *prometheus.CounterVec
}
func (g CounterVecMetric) Get() *Metric {
return &g.Metric
}
func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric {
return GaugeMetric{
Metric: Metric{
Type: "gauge",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
},
Gauge: prometheus.NewGauge(opts),
}
}
func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric {
return CounterMetric{
Metric: Metric{
Type: "counter",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
},
Counter: prometheus.NewCounter(opts),
}
}
func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) CounterVecMetric {
return CounterVecMetric{
Metric: Metric{
Type: "counter",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
},
CounterVec: prometheus.NewCounterVec(opts, labelNames),
}
}

View File

@ -0,0 +1,83 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
)
func TestNewGaugeWithOpts(t *testing.T) {
opts := prometheus.GaugeOpts{
Name: "test_gauge",
Namespace: "test_namespace",
Subsystem: "test_subsystem",
Help: "This is a test gauge",
}
gaugeMetric := NewGaugeWithOpts(opts)
assert.Equal(t, "gauge", gaugeMetric.Type)
assert.Equal(t, "test_gauge", gaugeMetric.Name)
assert.Equal(t, "test_namespace", gaugeMetric.Namespace)
assert.Equal(t, "test_subsystem", gaugeMetric.Subsystem)
assert.Equal(t, "This is a test gauge", gaugeMetric.Help)
assert.Equal(t, "test_subsystem_test_gauge", gaugeMetric.FQDN)
assert.NotNil(t, gaugeMetric.Gauge)
}
func TestNewCounterWithOpts(t *testing.T) {
opts := prometheus.CounterOpts{
Name: "test_counter",
Namespace: "test_namespace",
Subsystem: "test_subsystem",
Help: "This is a test counter",
}
counterMetric := NewCounterWithOpts(opts)
assert.Equal(t, "counter", counterMetric.Type)
assert.Equal(t, "test_counter", counterMetric.Name)
assert.Equal(t, "test_namespace", counterMetric.Namespace)
assert.Equal(t, "test_subsystem", counterMetric.Subsystem)
assert.Equal(t, "This is a test counter", counterMetric.Help)
assert.Equal(t, "test_subsystem_test_counter", counterMetric.FQDN)
assert.NotNil(t, counterMetric.Counter)
}
func TestNewCounterVecWithOpts(t *testing.T) {
opts := prometheus.CounterOpts{
Name: "test_counter_vec",
Namespace: "test_namespace",
Subsystem: "test_subsystem",
Help: "This is a test counter vector",
}
labelNames := []string{"label1", "label2"}
counterVecMetric := NewCounterVecWithOpts(opts, labelNames)
assert.Equal(t, "counter", counterVecMetric.Type)
assert.Equal(t, "test_counter_vec", counterVecMetric.Name)
assert.Equal(t, "test_namespace", counterVecMetric.Namespace)
assert.Equal(t, "test_subsystem", counterVecMetric.Subsystem)
assert.Equal(t, "This is a test counter vector", counterVecMetric.Help)
assert.Equal(t, "test_subsystem_test_counter_vec", counterVecMetric.FQDN)
assert.NotNil(t, counterVecMetric.CounterVec)
}

View File

@ -13,22 +13,23 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package provider package provider
import ( import (
"context" "context"
"sync"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/metrics"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
) )
var ( var (
cachedRecordsCallsTotal = prometheus.NewCounterVec( cachedRecordsCallsTotal = metrics.NewCounterVecWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "provider", Subsystem: "provider",
@ -39,7 +40,7 @@ var (
"from_cache", "from_cache",
}, },
) )
cachedApplyChangesCallsTotal = prometheus.NewCounter( cachedApplyChangesCallsTotal = metrics.NewCounterWithOpts(
prometheus.CounterOpts{ prometheus.CounterOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "provider", Subsystem: "provider",
@ -47,10 +48,13 @@ var (
Help: "Number of calls to the provider cache ApplyChanges.", Help: "Number of calls to the provider cache ApplyChanges.",
}, },
) )
registerCacheProviderMetrics = sync.Once{}
) )
func init() {
metrics.RegisterMetric.MustRegister(cachedRecordsCallsTotal)
metrics.RegisterMetric.MustRegister(cachedApplyChangesCallsTotal)
}
type CachedProvider struct { type CachedProvider struct {
Provider Provider
RefreshDelay time.Duration RefreshDelay time.Duration
@ -59,9 +63,6 @@ type CachedProvider struct {
} }
func NewCachedProvider(provider Provider, refreshDelay time.Duration) *CachedProvider { func NewCachedProvider(provider Provider, refreshDelay time.Duration) *CachedProvider {
registerCacheProviderMetrics.Do(func() {
prometheus.MustRegister(cachedRecordsCallsTotal)
})
return &CachedProvider{ return &CachedProvider{
Provider: provider, Provider: provider,
RefreshDelay: refreshDelay, RefreshDelay: refreshDelay,
@ -78,10 +79,10 @@ func (c *CachedProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
} }
c.cache = records c.cache = records
c.lastRead = time.Now() c.lastRead = time.Now()
cachedRecordsCallsTotal.WithLabelValues("false").Inc() cachedRecordsCallsTotal.CounterVec.WithLabelValues("false").Inc()
} else { } else {
log.Debug("Records cache provider: using records list from cache") log.Debug("Records cache provider: using records list from cache")
cachedRecordsCallsTotal.WithLabelValues("true").Inc() cachedRecordsCallsTotal.CounterVec.WithLabelValues("true").Inc()
} }
return c.cache, nil return c.cache, nil
} }
@ -91,7 +92,7 @@ func (c *CachedProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
return nil return nil
} }
c.Reset() c.Reset()
cachedApplyChangesCallsTotal.Inc() cachedApplyChangesCallsTotal.Counter.Inc()
return c.Provider.ApplyChanges(ctx, changes) return c.Provider.ApplyChanges(ctx, changes)
} }

View File

@ -25,11 +25,12 @@ import (
"net/url" "net/url"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/metrics"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
webhookapi "sigs.k8s.io/external-dns/provider/webhook/api" webhookapi "sigs.k8s.io/external-dns/provider/webhook/api"
backoff "github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -40,7 +41,7 @@ const (
) )
var ( var (
recordsErrorsGauge = prometheus.NewGauge( recordsErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
@ -48,7 +49,7 @@ var (
Help: "Errors with Records method", Help: "Errors with Records method",
}, },
) )
recordsRequestsGauge = prometheus.NewGauge( recordsRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
@ -56,7 +57,7 @@ var (
Help: "Requests with Records method", Help: "Requests with Records method",
}, },
) )
applyChangesErrorsGauge = prometheus.NewGauge( applyChangesErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
@ -64,7 +65,7 @@ var (
Help: "Errors with ApplyChanges method", Help: "Errors with ApplyChanges method",
}, },
) )
applyChangesRequestsGauge = prometheus.NewGauge( applyChangesRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
@ -72,7 +73,7 @@ var (
Help: "Requests with ApplyChanges method", Help: "Requests with ApplyChanges method",
}, },
) )
adjustEndpointsErrorsGauge = prometheus.NewGauge( adjustEndpointsErrorsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
@ -80,7 +81,7 @@ var (
Help: "Errors with AdjustEndpoints method", Help: "Errors with AdjustEndpoints method",
}, },
) )
adjustEndpointsRequestsGauge = prometheus.NewGauge( adjustEndpointsRequestsGauge = metrics.NewGaugeWithOpts(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Namespace: "external_dns", Namespace: "external_dns",
Subsystem: "webhook_provider", Subsystem: "webhook_provider",
@ -97,12 +98,12 @@ type WebhookProvider struct {
} }
func init() { func init() {
prometheus.MustRegister(recordsErrorsGauge) metrics.RegisterMetric.MustRegister(recordsErrorsGauge)
prometheus.MustRegister(recordsRequestsGauge) metrics.RegisterMetric.MustRegister(recordsRequestsGauge)
prometheus.MustRegister(applyChangesErrorsGauge) metrics.RegisterMetric.MustRegister(applyChangesErrorsGauge)
prometheus.MustRegister(applyChangesRequestsGauge) metrics.RegisterMetric.MustRegister(applyChangesRequestsGauge)
prometheus.MustRegister(adjustEndpointsErrorsGauge) metrics.RegisterMetric.MustRegister(adjustEndpointsErrorsGauge)
prometheus.MustRegister(adjustEndpointsRequestsGauge) metrics.RegisterMetric.MustRegister(adjustEndpointsRequestsGauge)
} }
func NewWebhookProvider(u string) (*WebhookProvider, error) { func NewWebhookProvider(u string) (*WebhookProvider, error) {
@ -160,26 +161,26 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) {
// Records will make a GET call to remoteServerURL/records and return the results // Records will make a GET call to remoteServerURL/records and return the results
func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
recordsRequestsGauge.Inc() recordsRequestsGauge.Gauge.Inc()
u := p.remoteServerURL.JoinPath("records").String() u := p.remoteServerURL.JoinPath("records").String()
req, err := http.NewRequest("GET", u, nil) req, err := http.NewRequest("GET", u, nil)
if err != nil { if err != nil {
recordsErrorsGauge.Inc() recordsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to create request: %s", err.Error()) log.Debugf("Failed to create request: %s", err.Error())
return nil, err return nil, err
} }
req.Header.Set(acceptHeader, webhookapi.MediaTypeFormatAndVersion) req.Header.Set(acceptHeader, webhookapi.MediaTypeFormatAndVersion)
resp, err := p.client.Do(req) resp, err := p.client.Do(req)
if err != nil { if err != nil {
recordsErrorsGauge.Inc() recordsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to perform request: %s", err.Error()) log.Debugf("Failed to perform request: %s", err.Error())
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
recordsErrorsGauge.Inc() recordsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to get records with code %d", resp.StatusCode) log.Debugf("Failed to get records with code %d", resp.StatusCode)
err := fmt.Errorf("failed to get records with code %d", resp.StatusCode) err := fmt.Errorf("failed to get records with code %d", resp.StatusCode)
if isRetryableError(resp.StatusCode) { if isRetryableError(resp.StatusCode) {
@ -190,7 +191,7 @@ func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
endpoints := []*endpoint.Endpoint{} endpoints := []*endpoint.Endpoint{}
if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil {
recordsErrorsGauge.Inc() recordsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to decode response body: %s", err.Error()) log.Debugf("Failed to decode response body: %s", err.Error())
return nil, err return nil, err
} }
@ -199,19 +200,19 @@ func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err
// ApplyChanges will make a POST to remoteServerURL/records with the changes // ApplyChanges will make a POST to remoteServerURL/records with the changes
func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
applyChangesRequestsGauge.Inc() applyChangesRequestsGauge.Gauge.Inc()
u := p.remoteServerURL.JoinPath("records").String() u := p.remoteServerURL.JoinPath("records").String()
b := new(bytes.Buffer) b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(changes); err != nil { if err := json.NewEncoder(b).Encode(changes); err != nil {
applyChangesErrorsGauge.Inc() applyChangesErrorsGauge.Gauge.Inc()
log.Debugf("Failed to encode changes: %s", err.Error()) log.Debugf("Failed to encode changes: %s", err.Error())
return err return err
} }
req, err := http.NewRequest("POST", u, b) req, err := http.NewRequest("POST", u, b)
if err != nil { if err != nil {
applyChangesErrorsGauge.Inc() applyChangesErrorsGauge.Gauge.Inc()
log.Debugf("Failed to create request: %s", err.Error()) log.Debugf("Failed to create request: %s", err.Error())
return err return err
} }
@ -220,14 +221,14 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
resp, err := p.client.Do(req) resp, err := p.client.Do(req)
if err != nil { if err != nil {
applyChangesErrorsGauge.Inc() applyChangesErrorsGauge.Gauge.Inc()
log.Debugf("Failed to perform request: %s", err.Error()) log.Debugf("Failed to perform request: %s", err.Error())
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent { if resp.StatusCode != http.StatusNoContent {
applyChangesErrorsGauge.Inc() applyChangesErrorsGauge.Gauge.Inc()
log.Debugf("Failed to apply changes with code %d", resp.StatusCode) log.Debugf("Failed to apply changes with code %d", resp.StatusCode)
err := fmt.Errorf("failed to apply changes with code %d", resp.StatusCode) err := fmt.Errorf("failed to apply changes with code %d", resp.StatusCode)
if isRetryableError(resp.StatusCode) { if isRetryableError(resp.StatusCode) {
@ -242,25 +243,25 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes
// based on a provider specific requirement. // based on a provider specific requirement.
// This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered. // This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered.
func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) {
adjustEndpointsRequestsGauge.Inc() adjustEndpointsRequestsGauge.Gauge.Inc()
endpoints := []*endpoint.Endpoint{} endpoints := []*endpoint.Endpoint{}
u, err := url.JoinPath(p.remoteServerURL.String(), "adjustendpoints") u, err := url.JoinPath(p.remoteServerURL.String(), "adjustendpoints")
if err != nil { if err != nil {
adjustEndpointsErrorsGauge.Inc() adjustEndpointsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to join path, %s", err) log.Debugf("Failed to join path, %s", err)
return nil, err return nil, err
} }
b := new(bytes.Buffer) b := new(bytes.Buffer)
if err := json.NewEncoder(b).Encode(e); err != nil { if err := json.NewEncoder(b).Encode(e); err != nil {
adjustEndpointsErrorsGauge.Inc() adjustEndpointsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to encode endpoints, %s", err) log.Debugf("Failed to encode endpoints, %s", err)
return nil, err return nil, err
} }
req, err := http.NewRequest("POST", u, b) req, err := http.NewRequest("POST", u, b)
if err != nil { if err != nil {
adjustEndpointsErrorsGauge.Inc() adjustEndpointsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to create new HTTP request, %s", err) log.Debugf("Failed to create new HTTP request, %s", err)
return nil, err return nil, err
} }
@ -270,14 +271,14 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.En
resp, err := p.client.Do(req) resp, err := p.client.Do(req)
if err != nil { if err != nil {
adjustEndpointsErrorsGauge.Inc() adjustEndpointsErrorsGauge.Gauge.Inc()
log.Debugf("Failed executing http request, %s", err) log.Debugf("Failed executing http request, %s", err)
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
adjustEndpointsErrorsGauge.Inc() adjustEndpointsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to AdjustEndpoints with code %d", resp.StatusCode) log.Debugf("Failed to AdjustEndpoints with code %d", resp.StatusCode)
err := fmt.Errorf("failed to AdjustEndpoints with code %d", resp.StatusCode) err := fmt.Errorf("failed to AdjustEndpoints with code %d", resp.StatusCode)
if isRetryableError(resp.StatusCode) { if isRetryableError(resp.StatusCode) {
@ -287,7 +288,7 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.En
} }
if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil {
adjustEndpointsErrorsGauge.Inc() adjustEndpointsErrorsGauge.Gauge.Inc()
log.Debugf("Failed to decode response body: %s", err.Error()) log.Debugf("Failed to decode response body: %s", err.Error())
return nil, err return nil, err
} }