Skip to content

Commit

Permalink
fix(instrumentation): multiple instrumented clients cause panic
Browse files Browse the repository at this point in the history
  • Loading branch information
apricote committed Jul 14, 2023
1 parent 6eea589 commit d3e8743
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 20 deletions.
66 changes: 47 additions & 19 deletions hcloud/internal/instrumentation/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,36 @@ func New(subsystemIdentifier string, instrumentationRegistry *prometheus.Registr

// InstrumentedRoundTripper returns an instrumented round tripper.
func (i *Instrumenter) InstrumentedRoundTripper() http.RoundTripper {
inFlightRequestsGauge := prometheus.NewGauge(prometheus.GaugeOpts{
Name: fmt.Sprintf("hcloud_%s_in_flight_requests", i.subsystemIdentifier),
Help: fmt.Sprintf("A gauge of in-flight requests to the hcloud %s.", i.subsystemIdentifier),
})

requestsPerEndpointCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: fmt.Sprintf("hcloud_%s_requests_total", i.subsystemIdentifier),
Help: fmt.Sprintf("A counter for requests to the hcloud %s per endpoint.", i.subsystemIdentifier),
},
[]string{"code", "method", "api_endpoint"},
inFlightRequestsGauge := registerOrReuse(
i.instrumentationRegistry,
prometheus.NewGauge(prometheus.GaugeOpts{
Name: fmt.Sprintf("hcloud_%s_in_flight_requests", i.subsystemIdentifier),
Help: fmt.Sprintf("A gauge of in-flight requests to the hcloud %s.", i.subsystemIdentifier),
}),
)

requestLatencyHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: fmt.Sprintf("hcloud_%s_request_duration_seconds", i.subsystemIdentifier),
Help: fmt.Sprintf("A histogram of request latencies to the hcloud %s .", i.subsystemIdentifier),
Buckets: prometheus.DefBuckets,
},
[]string{"method"},
requestsPerEndpointCounter := registerOrReuse(
i.instrumentationRegistry,
prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: fmt.Sprintf("hcloud_%s_requests_total", i.subsystemIdentifier),
Help: fmt.Sprintf("A counter for requests to the hcloud %s per endpoint.", i.subsystemIdentifier),
},
[]string{"code", "method", "api_endpoint"},
),
)

i.instrumentationRegistry.MustRegister(requestsPerEndpointCounter, requestLatencyHistogram, inFlightRequestsGauge)
requestLatencyHistogram := registerOrReuse(
i.instrumentationRegistry,
prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: fmt.Sprintf("hcloud_%s_request_duration_seconds", i.subsystemIdentifier),
Help: fmt.Sprintf("A histogram of request latencies to the hcloud %s .", i.subsystemIdentifier),
Buckets: prometheus.DefBuckets,
},
[]string{"method"},
),
)

return promhttp.InstrumentRoundTripperInFlight(inFlightRequestsGauge,
promhttp.InstrumentRoundTripperDuration(requestLatencyHistogram,
Expand Down Expand Up @@ -74,6 +81,27 @@ func (i *Instrumenter) instrumentRoundTripperEndpoint(counter *prometheus.Counte
}
}

// registerOrReuse will try to register the passed Collector, but in case a conflicting collector was already registered,
// it will instead return that collector. Make sure to always use the collector return by this method.
// Similar to [Registry.MustRegister] it will panic if any other error occurs.
func registerOrReuse[C prometheus.Collector](registry *prometheus.Registry, collector C) C {
err := registry.Register(collector)
if err != nil {
// If we get a AlreadyRegisteredError we can return the existing collector
if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
if existingCollector, ok := are.ExistingCollector.(C); ok {
collector = existingCollector
} else {
panic("received incompatible existing collector")
}
} else {
panic(err)
}
}

return collector
}

func preparePathForLabel(path string) string {
path = strings.ToLower(path)

Expand Down
16 changes: 15 additions & 1 deletion hcloud/internal/instrumentation/metrics_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package instrumentation

import "testing"
import (
"testing"

"github.com/prometheus/client_golang/prometheus"
)

func Test_preparePath(t *testing.T) {
tests := []struct {
Expand All @@ -27,3 +31,13 @@ func Test_preparePath(t *testing.T) {
})
}
}

func TestMultipleInstrumentedClients(t *testing.T) {
reg := prometheus.NewRegistry()

t.Run("should not panic", func(t *testing.T) {
// Following code should run without panicking
New("test", reg).InstrumentedRoundTripper()
New("test", reg).InstrumentedRoundTripper()
})
}

0 comments on commit d3e8743

Please sign in to comment.