Files
otelsetup/subscription_metrics_test.go
T
argoyle c00564cdcb
Release / release (push) Successful in 1m31s
otelsetup / vulnerabilities (push) Successful in 2m8s
otelsetup / test (push) Successful in 2m47s
pre-commit / pre-commit (push) Successful in 6m47s
feat(metrics): add SubscriptionMetrics OTel observer for subscriptions (#150)
2026-06-16 13:14:14 +00:00

109 lines
3.3 KiB
Go

package otelsetup
import (
"context"
"testing"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/noop"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
// observerShape mirrors gitea.unbound.se/shiny/subscriptions.Observer. This
// compile-time assertion guards that SubscriptionMetrics still satisfies that
// interface structurally, without otelsetup importing the library.
//
// KEEP IN SYNC with subscriptions.Observer: this only proves SubscriptionMetrics
// matches this local copy. The authoritative check that the local copy still
// matches the real interface is the `subscriptions.WithObserver(...)` call site
// in each consuming service — keep a `var _ subscriptions.Observer` guard there.
type observerShape interface {
Pushed(string)
PushSkipped(string)
Dropped(string)
ChannelFull(string)
}
var _ observerShape = (*SubscriptionMetrics)(nil)
// TestSubscriptionMetrics_DisabledProviderIsSafe proves the "recording is free
// when metrics are disabled" claim: with the default global no-op provider, the
// methods neither panic nor emit instruments.
func TestSubscriptionMetrics_DisabledProviderIsSafe(t *testing.T) {
otel.SetMeterProvider(noop.NewMeterProvider())
m, err := NewSubscriptionMetrics("entryBasesChanged")
if err != nil {
t.Fatalf("NewSubscriptionMetrics returned error: %v", err)
}
// Must not panic on the no-op provider.
m.Pushed("c1")
m.PushSkipped("c1")
m.Dropped("c1")
m.ChannelFull("c1")
}
func TestNewSubscriptionMetrics_RecordsOutcomes(t *testing.T) {
reader := sdkmetric.NewManualReader()
otel.SetMeterProvider(sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)))
m, err := NewSubscriptionMetrics("availableCompanies")
if err != nil {
t.Fatalf("NewSubscriptionMetrics returned error: %v", err)
}
// The subscriber key is ignored for metrics; different keys must not create
// new series (cardinality guard is implicit — we only label by outcome).
m.Pushed("c1")
m.Pushed("c2")
m.PushSkipped("c1")
m.Dropped("c1")
m.ChannelFull("c1")
var rm metricdata.ResourceMetrics
if err := reader.Collect(context.Background(), &rm); err != nil {
t.Fatalf("collect: %v", err)
}
counts := map[string]int64{}
dataPoints := 0
found := false
for _, sm := range rm.ScopeMetrics {
for _, md := range sm.Metrics {
if md.Name != "subscription.notifications" {
continue
}
found = true
sum, ok := md.Data.(metricdata.Sum[int64])
if !ok {
t.Fatalf("expected Sum[int64], got %T", md.Data)
}
for _, dp := range sum.DataPoints {
dataPoints++
sub, _ := dp.Attributes.Value(attribute.Key("subscription"))
if sub.AsString() != "availableCompanies" {
t.Errorf("unexpected subscription attribute: %q", sub.AsString())
}
outcome, _ := dp.Attributes.Value(attribute.Key("outcome"))
counts[outcome.AsString()] += dp.Value
}
}
}
if !found {
t.Fatal("subscription.notifications counter not emitted")
}
// One series per outcome, keyed by outcome only (not by subscriber key).
if dataPoints != 4 {
t.Errorf("expected 4 data points (one per outcome), got %d", dataPoints)
}
want := map[string]int64{"pushed": 2, "skipped": 1, "dropped": 1, "channel_full": 1}
for k, v := range want {
if counts[k] != v {
t.Errorf("outcome %q: got %d, want %d", k, counts[k], v)
}
}
}