Repository navigation
Conversation
|
@obitech Thoughts on this one welcome 👍 |
There was a problem hiding this comment.
I'm not a big fan of this in its current state because the claim "cardinality remains small" is only true when using the default parser. Complex span name parsing logic would now lead to:
- The func being applied on every call instead of just when spans are recording, incurring that cost for everyone.
- Potentially unbounded label cardinality
- A potentially unbounded sync.Map cache
I'd prefer this to be default-off and then have a dedicated option that decouples the metric attribute from the span name hook entirely. Something like:
// OperationNameFunc returns the value for db.operation.name on the
// operation duration and error metrics. Return "" to omit the attribute.
type OperationNameFunc func(ctx context.Context, sql string) string
// WithMetricOperationName enables db.operation.name on
// db.client.operation.duration and db.client.operation.errors.
func WithMetricOperationName(fn OperationNameFunc) OptionWe should also export the default parser so users can write WithMetricOperationName(otelpgx.SQLOperationName) instead of some special nil argument.
The upsides here are that the span path stays as it is today, and nobody gets a new label or a hook running on unsampled calls just by bumping the lib. Also the separate function forces a conscious decision to increase cardinality.
For the cache: please benchmark whether it's even measurable next to the Postgres round trip. If not, you can drop it. If it is, cap the number of entries and build the attribute set uncached past the cap. I don't think we need any special eviction logic here.
The batch handling and the new test are good and should carry over.
Last note: the PR description is much longer than the diff warrants. A short summary and the design decisions are enough.
|
Thanks for the review @obitech... Will re-work... Also, just noticed that there is still a "legacy" |
Per @obitech's review: the previous default-on design assumed `db.operation.name` cardinality "stays small" — true for the default parser, but not for a caller-supplied `WithSpanNameCtxFunc`/ `WithSpanNameFunc`, which could return arbitrary, high-cardinality values. It also ran `spanNameCtxFunc` on every call regardless of trace sampling, and cached `attribute.Set`s in an unbounded `sync.Map`. Benchmarked the cache first, as requested: cached lookups were ~16.6ns/0 allocs vs. ~162.6ns/3 allocs (448B) uncached — a ~146ns difference that's immaterial next to any real Postgres round trip (tens to hundreds of µs even on loopback). Dropped it entirely rather than capping it, per the reviewer's own fallback. Replaces the default-on behaviour with a new, independent option: type OperationNameFunc func(ctx context.Context, sql string) string func WithMetricOperationName(fn OperationNameFunc) Option - Default is unset: `db.client.operation.duration`/`db.client.operation.errors` carry no `db.operation.name` unless this is explicitly configured, so nobody gets a new label or an SQL-parsing hook running on unsampled calls just by upgrading. - Fully decoupled from `spanNameCtxFunc`/`WithSpanNameFunc`/ `WithSpanNameCtxFunc` — the span-naming path is untouched and back to exactly its pre-exaring#87 behaviour (operation name computed only when the span is recording). - Exported the existing first-word SQL parser as `SQLOperationName` (was the unexported `defaultSpanNameCtxFunc`) so it can be passed explicitly: `WithMetricOperationName(otelpgx.SQLOperationName)`. It's still the default for `WithSpanNameFunc`/`WithSpanNameCtxFunc`. - `attributeSetFor` builds the attribute.Set fresh per call rather than caching it, per the benchmark above. Batch handling, the new test, and the general approach otherwise carry over from exaring#87 as-is, per review. Dropped the "Metrics" README section (reviewer: "I don't see value in this block") — the option's doc comment covers it, matching how every other `Option` here is documented. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for the detailed review — pushed a redesign addressing all of it:
Ready for another look whenever convenient. |
Split this into #88 👍 |
|
@obitech LMK if you think this needs any further tweaks 👍 |
|
@obitech Hope you had a good weekend :) Could I get another review on this one? 🙇♂️ |
|
@fatmcgav could you rebase please? |
`recordOperationDuration`/`incrementOperationErrorCount` only ever looked up a fixed `attribute.Set` from `t.metricAttrs[pgxOperation]`, precomputed once per pgx call kind in `createAttributeSets()` at `NewTracer()` time — so `db.client.operation.duration` could only ever be broken down by call kind (query/batch/copy/prepare/connect/acquire), never by SQL operation (`SELECT`/`INSERT`/…), even though `spanNameCtxFunc` already computes that value for the equivalent span attribute a few lines away. Per the OTel database metrics semconv (https://opentelemetry.io/docs/specs/semconv/database/database-metrics/), `db.operation.name` is Conditionally Required "if readily available and if there is a single operation name that describes the database call". This threads the already-computed operation name into the metric path for `query`, `prepare`, and per-statement `batch` calls: - `attributeSetFor` builds (and lazily caches, keyed by `(kind, operation name)`) an `attribute.Set` that also carries `db.operation.name`, falling back to the existing static per-kind set when no name applies. SQL verbs are a small, bounded vocabulary, so the cache stays small for the tracer's lifetime. - For `query`/`prepare`, the name is computed in `*Start` but recorded in `*End`, so it's threaded through `context.Context` via a new `operationNameCtxKey`, mirroring the existing `startTimeCtxKey` pattern. It's now computed unconditionally (before the `IsRecording()` check), since metric recording is already decoupled from trace sampling. - For `batch`, `TraceBatchQuery` attaches each statement's own name to its per-query error increment. The whole-batch aggregate recorded in `TraceBatchEnd` stays unlabelled: a batch can mix operation types, so there's no single name that describes it as a whole. - `connect`/`acquire`/`copy` are unaffected — no SQL statement to name. This ships default-on rather than behind an `Option`: `db.operation.name` is a stable semconv attribute with low, bounded cardinality, so it seemed like a strict improvement rather than something to gate. Adds `TestTracer_metricOperationName` in `tracer_test.go` (covering SELECT/INSERT/UPDATE/DELETE, prepare, per-statement batch errors, and the unlabelled batch aggregate, all against a noop `TracerProvider` to demonstrate the metric still gets the attribute when no span is recording), extends `meter_test.go`'s `dataPointAttributes` test helper with a `Histogram[float64]` case, and documents the new attribute in `README.md`. Addresses exaring#86. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per @obitech's review: the previous default-on design assumed `db.operation.name` cardinality "stays small" — true for the default parser, but not for a caller-supplied `WithSpanNameCtxFunc`/ `WithSpanNameFunc`, which could return arbitrary, high-cardinality values. It also ran `spanNameCtxFunc` on every call regardless of trace sampling, and cached `attribute.Set`s in an unbounded `sync.Map`. Benchmarked the cache first, as requested: cached lookups were ~16.6ns/0 allocs vs. ~162.6ns/3 allocs (448B) uncached — a ~146ns difference that's immaterial next to any real Postgres round trip (tens to hundreds of µs even on loopback). Dropped it entirely rather than capping it, per the reviewer's own fallback. Replaces the default-on behaviour with a new, independent option: type OperationNameFunc func(ctx context.Context, sql string) string func WithMetricOperationName(fn OperationNameFunc) Option - Default is unset: `db.client.operation.duration`/`db.client.operation.errors` carry no `db.operation.name` unless this is explicitly configured, so nobody gets a new label or an SQL-parsing hook running on unsampled calls just by upgrading. - Fully decoupled from `spanNameCtxFunc`/`WithSpanNameFunc`/ `WithSpanNameCtxFunc` — the span-naming path is untouched and back to exactly its pre-exaring#87 behaviour (operation name computed only when the span is recording). - Exported the existing first-word SQL parser as `SQLOperationName` (was the unexported `defaultSpanNameCtxFunc`) so it can be passed explicitly: `WithMetricOperationName(otelpgx.SQLOperationName)`. It's still the default for `WithSpanNameFunc`/`WithSpanNameCtxFunc`. - `attributeSetFor` builds the attribute.Set fresh per call rather than caching it, per the benchmark above. Batch handling, the new test, and the general approach otherwise carry over from exaring#87 as-is, per review. Dropped the "Metrics" README section (reviewer: "I don't see value in this block") — the option's doc comment covers it, matching how every other `Option` here is documented. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Dropped the doc comment on `recordOperationDuration` repeating `incrementOperationErrorCount`'s `operationName` explanation verbatim; points to it instead. - Tightened `attributeSetFor`'s doc: the full semconv quote is already in `WithMetricOperationName`'s doc, so just keep the cache-rationale part here. - Removed the doc comment on `TestTracer_metricOperationName` — no other test function in this file has one, and the sub-test names already say what's covered. - Shortened the two near-identical "only computed when ... runs regardless of sampling" comments in `TraceQueryStart`/`TracePrepareStart`, and the `TraceBatchQuery`/`metricOperationNameCtxKey` comments, to their essential point. - Trimmed `WithMetricOperationName`'s doc comment, cutting a sentence that just restated the cardinality point already made. No behavioural change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9341e83 to
923af0d
Compare
|
@obitech Done 👍 |
Closes #86.
Summary
WithMetricOperationName(fn OperationNameFunc)— a new, opt-in option — attachesdb.operation.nametodb.client.operation.duration/db.client.operation.errorsfor query, prepare, and per-statement batch-query calls, per the OTel database metrics semconv. Default (unset): unchanged behaviour, no new attribute.Design decisions
spanNameCtxFunc/WithSpanNameFunc/WithSpanNameCtxFunc. The span path is untouched.fnonly runs onceWithMetricOperationNameis set, and then runs on every call (sampled or not), since metric recording is decoupled from trace sampling.SQLOperationName(wasdefaultSpanNameCtxFunc) so it can be passed explicitly:WithMetricOperationName(otelpgx.SQLOperationName). Rebased onto the Default behvior of sqlOperationName does not ignore comments #63 fix, so it skips leading SQL comments — sqlc-style-- name: ...queries produce the real verb rather than a--label on the metric.attribute.Setlookups were ~16.6ns/0 allocs vs. ~162.6ns/3 allocs uncached. That ~146ns difference is immaterial next to a real Postgres round trip (tens–hundreds of µs even on loopback), so I dropped the cache entirely rather than capping it.batch:TraceBatchQuery's per-statement error increment gets its own operation name; the whole-batch aggregate inTraceBatchEndstays unlabelled (no single name for a possibly-mixed batch).connect/acquire/copyunaffected.Testing
TestTracer_metricOperationNamecovers: unset (no attribute), SELECT/INSERT/DELETE/UPDATE, prepare, per-statement batch error, batch aggregate (no name), and a customOperationNameFunc.go build,go vet,gofmt -l,go test -race ./...all pass locally.