feat(metrics): record eventsourced pg outbox publishes and retries (#182)
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

## 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
This commit was merged in pull request #182.
This commit is contained in:
argoyle committed 2026-09-17 11:54:05 +00:00
1 parent e9a2dcf720
commit 88dde56687
4 files changed
+79 -9

No files matched your search

+17 -2
View File
@@ -5,6 +5,7 @@ import (
"errors"
"codeberg.org/eventsourced/eventsourced"
"codeberg.org/eventsourced/pg/v2"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
@@ -40,6 +41,8 @@ type eventsourcedMetrics struct {
snapshotStoreDur metric.Float64Histogram
snapshotLoadDur metric.Float64Histogram
idempotencyCheck metric.Int64Counter
outboxPublishDur metric.Float64Histogram
outboxRetries metric.Int64Counter
}
// NewEventsourcedMetrics builds an eventsourced.MetricsRecorder that records to
@@ -78,6 +81,8 @@ func NewEventsourcedMetrics() (eventsourced.MetricsRecorder, error) {
snapshotStoreDur: hist("eventsourced.snapshot.store.duration", "Time taken to persist a snapshot."),
snapshotLoadDur: hist("eventsourced.snapshot.load.duration", "Time taken to load a snapshot."),
idempotencyCheck: counter("eventsourced.idempotency.checks", "Number of command idempotency lookups."),
outboxPublishDur: hist("eventsourced.outbox.publish.duration", "Time taken to publish an event from the outbox."),
outboxRetries: counter("eventsourced.outbox.retries", "Failed outbox publishes; permanent ones are never retried."),
}
if err := errors.Join(errs...); err != nil {
return nil, err
@@ -86,8 +91,8 @@ func NewEventsourcedMetrics() (eventsourced.MetricsRecorder, error) {
}
// Record implements eventsourced.MetricsRecorder. Metric types the adapter does
// not recognise (for example pg outbox metrics when the outbox is not enabled)
// are ignored.
// not recognise are ignored, and so are pg.OutboxBatch and pg.OutboxCleanup: a failed
// publish already shows in the publish and retry instruments.
func (e *eventsourcedMetrics) Record(ctx context.Context, raw eventsourced.Metric) {
switch m := raw.(type) {
case eventsourced.CommandDuration:
@@ -119,5 +124,15 @@ func (e *eventsourcedMetrics) Record(ctx context.Context, raw eventsourced.Metri
attribute.String("aggregate.type", m.AggregateType),
attribute.Bool("hit", m.Hit),
))
case pg.OutboxPublish:
e.outboxPublishDur.Record(ctx, m.Duration.Seconds(), metric.WithAttributes(
attribute.String("event.type", m.EventType),
attribute.Bool("success", m.Success),
))
case pg.OutboxRetry:
e.outboxRetries.Add(ctx, 1, metric.WithAttributes(
attribute.String("event.type", m.EventType),
attribute.Bool("permanent", m.Permanent),
))
}
}