Files
otelsetup/metrics_test.go
argoyle 88dde56687
Unbound Release / Check Preconditions (push) Successful in 22s
Unbound Release / Create Tag (push) Skipped
Unbound Release / Create Release (push) Successful in 23s
otelsetup / test (push) Successful in 58s
otelsetup / vulnerabilities (push) Successful in 53s
Unbound Release / Generate Changelog and Handle PR (push) Successful in 37s
Release / release (push) Successful in 1m2s
pre-commit / pre-commit (push) Successful in 2m45s
feat(metrics): record eventsourced pg outbox publishes and retries (#182)
## Why
authz-service now publishes through pg/v2's transactional outbox, and nothing measured it: `NewEventsourcedMetrics` ignored the `pg.Outbox*` metrics. Without them a failing or abandoned publish (a lost privilege revocation for running authz_clients) can't be alerted on.

## What
- `pg.OutboxPublish` → histogram `eventsourced.outbox.publish.duration` (`event.type`, `success`)
- `pg.OutboxRetry` → counter `eventsourced.outbox.retries` (`event.type`, `permanent`)
- `pg.OutboxBatch` / `pg.OutboxCleanup` stay ignored.

otelsetup now imports `codeberg.org/eventsourced/pg/v2` v2.1.1 (`go` directive becomes 1.26.0; grpc patch bump via MVS). All current consumers already depend on pg/v2; a release raises their minimum to v2.1.1.

## Tests
`go test -race ./...` green; the contract test records both new metrics.

## Review
Go Backend expert reviewed: no Critical/High. Noted: the test checks instrument names only, not attributes; the pg/v2 dependency is worth a line in the release notes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_013DJouG8ZZZxKtodF9kDvzj
Reviewed-on: https://gitea.unbound.se/shiny/otelsetup/pulls/182
2026-09-17 11:54:05 +00:00

81 lines
2.4 KiB
Go

package otelsetup
import (
"context"
"sort"
"testing"
"time"
"codeberg.org/eventsourced/eventsourced"
"codeberg.org/eventsourced/pg/v2"
"go.opentelemetry.io/otel"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)
func TestNewEventsourcedMetrics_RecordsContract(t *testing.T) {
reader := sdkmetric.NewManualReader()
otel.SetMeterProvider(sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)))
r, err := NewEventsourcedMetrics()
if err != nil {
t.Fatalf("NewEventsourcedMetrics returned error: %v", err)
}
if r == nil {
t.Fatal("NewEventsourcedMetrics returned nil recorder")
}
// Recording every known metric type (and an unknown one) must not panic
// and must emit the expected instruments.
for _, m := range []eventsourced.Metric{
eventsourced.CommandDuration{CommandType: "AddEntry", Duration: time.Millisecond, Success: true},
eventsourced.EventStored{AggregateType: "Entry", EventType: "EntryAdded", Duration: time.Millisecond},
eventsourced.EventsLoaded{AggregateType: "Entry", EventCount: 3, Duration: time.Millisecond},
eventsourced.SnapshotStored{AggregateType: "Entry", Duration: time.Millisecond, Success: true},
eventsourced.SnapshotLoaded{AggregateType: "Entry", Found: false, Duration: time.Millisecond},
eventsourced.IdempotencyCheck{AggregateType: "Entry", Hit: true},
pg.OutboxPublish{EventType: "EntryAdded", Success: false, Duration: time.Millisecond},
pg.OutboxRetry{EventType: "EntryAdded", RetryCount: 1},
unknownMetric{},
} {
r.Record(context.Background(), m)
}
var rm metricdata.ResourceMetrics
if err := reader.Collect(context.Background(), &rm); err != nil {
t.Fatalf("collect: %v", err)
}
got := map[string]bool{}
for _, sm := range rm.ScopeMetrics {
for _, md := range sm.Metrics {
got[md.Name] = true
}
}
want := []string{
"eventsourced.command.duration",
"eventsourced.event.store.duration",
"eventsourced.events.loaded",
"eventsourced.event.load.duration",
"eventsourced.snapshot.store.duration",
"eventsourced.snapshot.load.duration",
"eventsourced.idempotency.checks",
"eventsourced.outbox.publish.duration",
"eventsourced.outbox.retries",
}
var missing []string
for _, w := range want {
if !got[w] {
missing = append(missing, w)
}
}
if len(missing) > 0 {
sort.Strings(missing)
t.Errorf("missing expected metrics: %v", missing)
}
}
type unknownMetric struct{}
func (unknownMetric) IsMetric() {}