Skip to content

Commit 545238e

Browse files
committed
feat: remove pool.acquire and prepare spans, preserve metrics
Remove span creation for pool.acquire and prepare operations to reduce trace noise. Metrics (db.client.operation.duration, error counts) are preserved for both operations. For prepare, the wall-clock duration is recorded as a pgx.prepare.duration attribute (Int64, milliseconds) on the parent query span instead of creating a separate child span. Pool acquire duration is better tracked through aggregate metrics (pgxpool.* from RecordStats) since contention is an environmental concern, not useful as individual trace spans. Made-with: Cursor
1 parent b4aeedc commit 545238e

File tree

2 files changed

+116
-92
lines changed

2 files changed

+116
-92
lines changed

tracer.go

Lines changed: 20 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ const (
5252
PGXOperationTypeKey = attribute.Key("pgx.operation.type")
5353
// DBClientOperationErrorsKey represents the count of operation errors
5454
DBClientOperationErrorsKey = attribute.Key("db.client.operation.errors")
55+
// PrepareDurationKey records the wall-clock time of a prepared-statement
56+
// round-trip, in milliseconds, as an attribute on the parent query span.
57+
PrepareDurationKey = attribute.Key("pgx.prepare.duration")
5558
)
5659

5760
type startTimeCtxKey struct{}
@@ -530,116 +533,41 @@ func (t *Tracer) TraceConnectEnd(ctx context.Context, data pgx.TraceConnectEndDa
530533
// TracePrepareStart is called at the beginning of Prepare calls. The returned
531534
// context is used for the rest of the call and will be passed to
532535
// TracePrepareEnd.
533-
func (t *Tracer) TracePrepareStart(ctx context.Context, conn *pgx.Conn, data pgx.TracePrepareStartData) context.Context {
534-
ctx = context.WithValue(ctx, startTimeCtxKey{}, time.Now())
535-
536-
if !trace.SpanFromContext(ctx).IsRecording() {
537-
return ctx
538-
}
539-
540-
optsP := t.spanStartOptionsPool.Get().(*[]trace.SpanStartOption)
541-
defer t.spanStartOptionsPool.Put(optsP)
542-
attrsP := t.attributeSlicePool.Get().(*[]attribute.KeyValue)
543-
defer t.attributeSlicePool.Put(attrsP)
544-
545-
// reslice to empty
546-
opts := (*optsP)[:0]
547-
attrs := (*attrsP)[:0]
548-
549-
attrs = append(attrs, t.tracerAttrs...)
550-
551-
if data.Name != "" {
552-
attrs = append(attrs, PrepareStmtNameKey.String(data.Name))
553-
}
554-
555-
if t.logConnectionDetails && conn != nil {
556-
attrs = append(attrs, connectionAttributesFromConfig(conn.Config())...)
557-
}
558-
559-
attrs = append(attrs, semconv.DBOperationName(t.spanNameCtxFunc(ctx, data.SQL)))
560-
561-
if t.logSQLStatement {
562-
attrs = append(attrs, semconv.DBQueryText(data.SQL))
563-
}
564-
565-
opts = append(opts,
566-
trace.WithSpanKind(trace.SpanKindClient),
567-
trace.WithAttributes(attrs...),
568-
)
569-
570-
spanName := data.SQL
571-
if t.trimQuerySpanName {
572-
spanName = t.spanNameCtxFunc(ctx, data.SQL)
573-
}
574-
if t.prefixQuerySpanName {
575-
spanName = "prepare " + spanName
576-
}
577-
578-
ctx, _ = t.tracer.Start(ctx, spanName, opts...)
579-
580-
return ctx
536+
//
537+
// No span is created for prepare. Instead, the prepare duration is recorded as
538+
// the pgx.prepare.duration attribute (milliseconds) on the parent query span.
539+
func (t *Tracer) TracePrepareStart(ctx context.Context, _ *pgx.Conn, _ pgx.TracePrepareStartData) context.Context {
540+
return context.WithValue(ctx, startTimeCtxKey{}, time.Now())
581541
}
582542

583543
// TracePrepareEnd is called at the end of Prepare calls.
584544
func (t *Tracer) TracePrepareEnd(ctx context.Context, _ *pgx.Conn, data pgx.TracePrepareEndData) {
585-
span := trace.SpanFromContext(ctx)
586545
t.incrementOperationErrorCount(ctx, data.Err, pgxOperationPrepare)
587546
t.recordOperationDuration(ctx, pgxOperationPrepare)
588547

589-
if !span.IsRecording() {
590-
return
548+
if startTime, ok := ctx.Value(startTimeCtxKey{}).(time.Time); ok {
549+
span := trace.SpanFromContext(ctx)
550+
if span.IsRecording() {
551+
span.SetAttributes(PrepareDurationKey.Int64(time.Since(startTime).Milliseconds()))
552+
}
591553
}
592-
593-
recordSpanError(span, data.Err)
594-
span.End()
595554
}
596555

597556
// TraceAcquireStart is called at the beginning of Acquire.
598557
// The returned context is used for the rest of the call and will be passed to the TraceAcquireEnd.
599-
func (t *Tracer) TraceAcquireStart(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireStartData) context.Context {
600-
ctx = context.WithValue(ctx, startTimeCtxKey{}, time.Now())
601-
602-
if !trace.SpanFromContext(ctx).IsRecording() {
603-
return ctx
604-
}
605-
606-
optsP := t.spanStartOptionsPool.Get().(*[]trace.SpanStartOption)
607-
defer t.spanStartOptionsPool.Put(optsP)
608-
attrsP := t.attributeSlicePool.Get().(*[]attribute.KeyValue)
609-
defer t.attributeSlicePool.Put(attrsP)
610-
611-
// reslice to empty
612-
opts := (*optsP)[:0]
613-
attrs := (*attrsP)[:0]
614-
615-
attrs = append(attrs, t.tracerAttrs...)
616-
617-
if t.logConnectionDetails && pool != nil && pool.Config() != nil && pool.Config().ConnConfig != nil {
618-
attrs = append(attrs, connectionAttributesFromConfig(pool.Config().ConnConfig)...)
619-
}
620-
621-
opts = append(opts,
622-
trace.WithSpanKind(trace.SpanKindClient),
623-
trace.WithAttributes(attrs...),
624-
)
625-
626-
ctx, _ = t.tracer.Start(ctx, "pool.acquire", opts...)
627-
628-
return ctx
558+
//
559+
// No span is created for pool.acquire. Pool acquire duration is tracked via the
560+
// db.client.operation.duration metric (pgx.operation.type=acquire) and the pgxpool.*
561+
// metrics from RecordStats. As a span it adds noise without actionable signal; pool
562+
// contention is an environmental concern better diagnosed from aggregate metrics.
563+
func (t *Tracer) TraceAcquireStart(ctx context.Context, _ *pgxpool.Pool, _ pgxpool.TraceAcquireStartData) context.Context {
564+
return context.WithValue(ctx, startTimeCtxKey{}, time.Now())
629565
}
630566

631567
// TraceAcquireEnd is called when a connection has been acquired.
632568
func (t *Tracer) TraceAcquireEnd(ctx context.Context, _ *pgxpool.Pool, data pgxpool.TraceAcquireEndData) {
633-
span := trace.SpanFromContext(ctx)
634569
t.incrementOperationErrorCount(ctx, data.Err, pgxOperationAcquire)
635570
t.recordOperationDuration(ctx, pgxOperationAcquire)
636-
637-
if !span.IsRecording() {
638-
return
639-
}
640-
641-
recordSpanError(span, data.Err)
642-
span.End()
643571
}
644572

645573
func makeParamsAttribute(args []any) attribute.KeyValue {

tracer_span_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package otelpgx
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/jackc/pgx/v5"
8+
"github.com/jackc/pgx/v5/pgxpool"
9+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
10+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
11+
)
12+
13+
func setupTestTracer(t *testing.T) (*Tracer, *tracetest.InMemoryExporter, sdktrace.TracerProvider) {
14+
t.Helper()
15+
16+
exporter := tracetest.NewInMemoryExporter()
17+
tp := *sdktrace.NewTracerProvider(
18+
sdktrace.WithSyncer(exporter),
19+
sdktrace.WithSampler(sdktrace.AlwaysSample()),
20+
)
21+
22+
tracer := NewTracer(WithTracerProvider(&tp))
23+
return tracer, exporter, tp
24+
}
25+
26+
func TestTraceAcquire_NoSpan(t *testing.T) {
27+
tracer, exporter, tp := setupTestTracer(t)
28+
29+
parentTracer := tp.Tracer("test")
30+
ctx, parentSpan := parentTracer.Start(context.Background(), "parent")
31+
32+
ctx = tracer.TraceAcquireStart(ctx, nil, pgxpool.TraceAcquireStartData{})
33+
tracer.TraceAcquireEnd(ctx, nil, pgxpool.TraceAcquireEndData{})
34+
35+
parentSpan.End()
36+
37+
spans := exporter.GetSpans()
38+
39+
for _, s := range spans {
40+
if s.Name == "pool.acquire" {
41+
t.Error("pool.acquire span should not be created")
42+
}
43+
}
44+
45+
if len(spans) != 1 {
46+
t.Errorf("expected exactly 1 span (the parent), got %d", len(spans))
47+
}
48+
if spans[0].Name != "parent" {
49+
t.Errorf("expected span name 'parent', got %q", spans[0].Name)
50+
}
51+
}
52+
53+
func TestTracePrepare_NoSpan_SetsAttribute(t *testing.T) {
54+
tracer, exporter, tp := setupTestTracer(t)
55+
56+
parentTracer := tp.Tracer("test")
57+
ctx, parentSpan := parentTracer.Start(context.Background(), "query SELECT 1")
58+
59+
ctx = tracer.TracePrepareStart(ctx, nil, pgx.TracePrepareStartData{
60+
Name: "stmt1",
61+
SQL: "SELECT 1",
62+
})
63+
tracer.TracePrepareEnd(ctx, nil, pgx.TracePrepareEndData{})
64+
65+
parentSpan.End()
66+
67+
spans := exporter.GetSpans()
68+
69+
for _, s := range spans {
70+
if s.Name == "prepare SELECT 1" || s.Name == "SELECT 1" {
71+
t.Errorf("prepare span should not be created, found span %q", s.Name)
72+
}
73+
}
74+
75+
if len(spans) != 1 {
76+
t.Errorf("expected exactly 1 span (the parent), got %d", len(spans))
77+
}
78+
79+
parentAttrs := spans[0].Attributes
80+
var found bool
81+
for _, attr := range parentAttrs {
82+
if attr.Key == PrepareDurationKey {
83+
found = true
84+
if attr.Value.Type().String() != "INT64" {
85+
t.Errorf("expected pgx.prepare.duration to be INT64, got %s", attr.Value.Type())
86+
}
87+
if attr.Value.AsInt64() < 0 {
88+
t.Errorf("expected pgx.prepare.duration >= 0, got %d", attr.Value.AsInt64())
89+
}
90+
}
91+
}
92+
93+
if !found {
94+
t.Error("expected pgx.prepare.duration attribute on parent span, but not found")
95+
}
96+
}

0 commit comments

Comments
 (0)