chore(metrics): Updating so that request duration uses the common metrics registry. (#5677)

This commit is contained in:
Matt Mix 2025-08-03 03:21:38 -04:00 committed by GitHub
parent 7857f71d31
commit 0e7c3af221
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 104 additions and 42 deletions

View File

@ -26,6 +26,7 @@ curl https://localhost:7979/metrics
| 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 |
| no_op_runs_total | Counter | controller | Number of reconcile loops ending up with no changes on the DNS provider side. | | no_op_runs_total | Counter | controller | Number of reconcile loops ending up with no changes on the DNS provider side. |
| verified_records | Gauge | controller | Number of DNS records that exists both in source and registry (vector). | | verified_records | Gauge | controller | Number of DNS records that exists both in source and registry (vector). |
| request_duration_seconds | Summaryvec | http | The HTTP request latencies in seconds. |
| cache_apply_changes_calls | Counter | provider | Number of calls to the provider cache ApplyChanges. | | 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. | | cache_records_calls | Counter | provider | Number of calls to the provider cache Records list. |
| endpoints_total | Gauge | registry | Number of Endpoints in the registry | | endpoints_total | Gauge | registry | Number of Endpoints in the registry |
@ -76,7 +77,6 @@ curl https://localhost:7979/metrics
| go_memstats_sys_bytes | | go_memstats_sys_bytes |
| go_sched_gomaxprocs_threads | | go_sched_gomaxprocs_threads |
| go_threads | | go_threads |
| http_request_duration_seconds |
| process_cpu_seconds_total | | process_cpu_seconds_total |
| process_max_fds | | process_max_fds |
| process_network_receive_bytes_total | | process_network_receive_bytes_total |

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, 20) assert.Len(t, reg.Metrics, 21)
} }
func TestGenerateMarkdownTableRenderer(t *testing.T) { func TestGenerateMarkdownTableRenderer(t *testing.T) {

View File

@ -21,14 +21,15 @@ package http
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/external-dns/pkg/metrics"
) )
var ( var (
requestDuration = prometheus.NewSummaryVec( RequestDurationMetric = metrics.NewSummaryVecWithOpts(
prometheus.SummaryOpts{ prometheus.SummaryOpts{
Name: "request_duration_seconds", Name: "request_duration_seconds",
Help: "The HTTP request latencies in seconds.", Help: "The HTTP request latencies in seconds.",
@ -41,7 +42,7 @@ var (
) )
func init() { func init() {
prometheus.MustRegister(requestDuration) metrics.RegisterMetric.MustRegister(RequestDurationMetric)
} }
type CustomRoundTripper struct { type CustomRoundTripper struct {
@ -61,15 +62,8 @@ func (r *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
if resp != nil { if resp != nil {
status = fmt.Sprintf("%d", resp.StatusCode) status = fmt.Sprintf("%d", resp.StatusCode)
} }
RequestDurationMetric.SetWithLabels(time.Since(start).Seconds(), req.URL.Scheme, req.URL.Host, metrics.PathProcessor(req.URL.Path), req.Method, status)
labels := prometheus.Labels{
"scheme": req.URL.Scheme,
"host": req.URL.Host,
"path": pathProcessor(req.URL.Path),
"method": req.Method,
"status": status,
}
requestDuration.With(labels).Observe(time.Since(start).Seconds())
return resp, err return resp, err
} }
@ -90,8 +84,3 @@ func NewInstrumentedTransport(next http.RoundTripper) http.RoundTripper {
return &CustomRoundTripper{next: next} return &CustomRoundTripper{next: next}
} }
func pathProcessor(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
}

View File

@ -57,25 +57,3 @@ func TestNewInstrumentedClient(t *testing.T) {
_, ok = result2.Transport.(*CustomRoundTripper) _, ok = result2.Transport.(*CustomRoundTripper)
require.True(t, ok) require.True(t, ok)
} }
func TestPathProcessor(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"/foo/bar", "bar"},
{"/foo/", ""},
{"/", ""},
{"", ""},
{"/foo/bar/baz", "baz"},
{"foo/bar", "bar"},
{"foo", "foo"},
{"foo/", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
require.Equal(t, tt.expected, pathProcessor(tt.input))
})
}
}

View File

@ -73,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, GaugeFuncMetric: case CounterMetric, GaugeMetric, SummaryVecMetric, CounterVecMetric, GaugeVecMetric, GaugeFuncMetric:
if _, exists := m.mName[cs.Get().FQDN]; exists { if _, exists := m.mName[cs.Get().FQDN]; exists {
return return
} else { } else {
@ -85,6 +85,8 @@ func (m *MetricRegistry) MustRegister(cs IMetric) {
m.Registerer.MustRegister(metric.Counter) m.Registerer.MustRegister(metric.Counter)
case GaugeMetric: case GaugeMetric:
m.Registerer.MustRegister(metric.Gauge) m.Registerer.MustRegister(metric.Gauge)
case SummaryVecMetric:
m.Registerer.MustRegister(metric.SummaryVec)
case GaugeVecMetric: case GaugeVecMetric:
m.Registerer.MustRegister(metric.Gauge) m.Registerer.MustRegister(metric.Gauge)
case CounterVecMetric: case CounterVecMetric:

View File

@ -61,8 +61,9 @@ func TestMustRegister(t *testing.T) {
NewCounterWithOpts(prometheus.CounterOpts{Name: "test_counter_3"}), NewCounterWithOpts(prometheus.CounterOpts{Name: "test_counter_3"}),
NewCounterVecWithOpts(prometheus.CounterOpts{Name: "test_counter_vec_3"}, []string{"label"}), NewCounterVecWithOpts(prometheus.CounterOpts{Name: "test_counter_vec_3"}, []string{"label"}),
NewGaugedVectorOpts(prometheus.GaugeOpts{Name: "test_gauge_v_3"}, []string{"label"}), NewGaugedVectorOpts(prometheus.GaugeOpts{Name: "test_gauge_v_3"}, []string{"label"}),
NewSummaryVecWithOpts(prometheus.SummaryOpts{Name: "test_summary_v_3"}, []string{"label"}),
}, },
expected: 4, expected: 5,
}, },
{ {
name: "unsupported metric", name: "unsupported metric",

View File

@ -176,3 +176,42 @@ func NewGaugeFuncMetric(opts prometheus.GaugeOpts) GaugeFuncMetric {
GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }), GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }),
} }
} }
type SummaryVecMetric struct {
Metric
SummaryVec prometheus.SummaryVec
}
func (s SummaryVecMetric) Get() *Metric {
return &s.Metric
}
// SetWithLabels sets the value of the SummaryVec metric for the specified label values.
// All label values are converted to lowercase before being applied.
func (s SummaryVecMetric) SetWithLabels(value float64, lvs ...string) {
for i, v := range lvs {
lvs[i] = strings.ToLower(v)
}
s.SummaryVec.WithLabelValues(lvs...).Observe(value)
}
func NewSummaryVecWithOpts(opts prometheus.SummaryOpts, labelNames []string) SummaryVecMetric {
opts.Namespace = Namespace
return SummaryVecMetric{
Metric: Metric{
Type: "summaryVec",
Name: opts.Name,
FQDN: fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name),
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Help: opts.Help,
},
SummaryVec: *prometheus.NewSummaryVec(opts, labelNames),
}
}
func PathProcessor(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
}

View File

@ -23,6 +23,7 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestNewGaugeWithOpts(t *testing.T) { func TestNewGaugeWithOpts(t *testing.T) {
@ -128,3 +129,55 @@ func TestNewBuildInfoCollector(t *testing.T) {
assert.Equal(t, "external_dns_build_info", reflect.ValueOf(desc).Elem().FieldByName("fqName").String()) assert.Equal(t, "external_dns_build_info", reflect.ValueOf(desc).Elem().FieldByName("fqName").String())
assert.Contains(t, desc.String(), "version=\"0.0.1\"") assert.Contains(t, desc.String(), "version=\"0.0.1\"")
} }
func TestSummaryV_SetWithLabels(t *testing.T) {
opts := prometheus.SummaryOpts{
Name: "test_summaryVec",
Namespace: "test_ns",
Subsystem: "test_sub",
Help: "help text",
}
sv := NewSummaryVecWithOpts(opts, []string{"label1", "label2"})
sv.SetWithLabels(5.01, "Alpha", "BETA")
reg := prometheus.NewRegistry()
reg.MustRegister(sv.SummaryVec)
metricsFamilies, err := reg.Gather()
assert.NoError(t, err)
assert.Len(t, metricsFamilies, 1)
s, err := sv.SummaryVec.GetMetricWithLabelValues("alpha", "beta")
assert.NoError(t, err)
metricsFamilies, err = reg.Gather()
s.Observe(5.21)
metricsFamilies, err = reg.Gather()
assert.NoError(t, err)
assert.InDelta(t, 10.22, *metricsFamilies[0].Metric[0].Summary.SampleSum, 0.01)
assert.Len(t, metricsFamilies[0].Metric[0].Label, 2)
}
func TestPathProcessor(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"/foo/bar", "bar"},
{"/foo/", ""},
{"/", ""},
{"", ""},
{"/foo/bar/baz", "baz"},
{"foo/bar", "bar"},
{"foo", "foo"},
{"foo/", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
require.Equal(t, tt.expected, PathProcessor(tt.input))
})
}
}