Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib/jackc/pgx.v5: publish pgxpool stats to statsd client #2692

Merged
merged 10 commits into from
Jul 10, 2024
67 changes: 67 additions & 0 deletions contrib/jackc/pgx.v5/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022 Datadog, Inc.

package pgx

import (
"time"

"github.com/jackc/pgx/v5/pgxpool"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

const tracerPrefix = "datadog.tracer."

const (
AcquireCount = tracerPrefix + "pgx.pool.connections.acquire"
AcquireDuration = tracerPrefix + "pgx.pool.connections.acquire_duration"
AcquiredConns = tracerPrefix + "pgx.pool.connections.acquired_conns"
CanceledAcquireCount = tracerPrefix + "pgx.pool.connections.canceled_acquire"
ConstructingConns = tracerPrefix + "pgx.pool.connections.constructing_conns"
EmptyAcquireCount = tracerPrefix + "pgx.pool.connections.empty_acquire"
IdleConns = tracerPrefix + "pgx.pool.connections.idle_conns"
MaxConns = tracerPrefix + "pgx.pool.connections.max_conns"
TotalConns = tracerPrefix + "pgx.pool.connections.total_conns"
NewConnsCount = tracerPrefix + "pgx.pool.connections.new_conns"
MaxLifetimeDestroyCount = tracerPrefix + "pgx.pool.connections.max_lifetime_destroy"
MaxIdleDestroyCount = tracerPrefix + "pgx.pool.connections.max_idle_destroy"
)

var interval = 10 * time.Second

// pollPoolStats calls (*pgxpool).Stats on the pool at a predetermined interval. It pushes the pool Stats off to the statsd client.
func pollPoolStats(statsd internal.StatsdClient, pool *pgxpool.Pool) {
if pool == nil {
log.Debug("No traced pool connection found; cannot pull pool stats.")
return
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove lines 38 - 41 as they essentially duplicate the err != nil check done by the caller, NewPoolWithConfig (Note: We also removed a similar check in the database/sql DB Stats feature in more recent versions , PR)

log.Debug("Traced pool connection found: Pool stats will be gathered and sent every %v.", interval)
bengentree marked this conversation as resolved.
Show resolved Hide resolved
for range time.NewTicker(interval).C {
log.Debug("Reporting pgxpool.Stat metrics...")
bengentree marked this conversation as resolved.
Show resolved Hide resolved
stat := pool.Stat()
statsd.Gauge(AcquireCount, float64(stat.AcquireCount()), []string{}, 1)
statsd.Timing(AcquireDuration, stat.AcquireDuration(), []string{}, 1)
statsd.Gauge(AcquiredConns, float64(stat.AcquiredConns()), []string{}, 1)
statsd.Gauge(CanceledAcquireCount, float64(stat.CanceledAcquireCount()), []string{}, 1)
statsd.Gauge(ConstructingConns, float64(stat.ConstructingConns()), []string{}, 1)
statsd.Gauge(EmptyAcquireCount, float64(stat.EmptyAcquireCount()), []string{}, 1)
statsd.Gauge(IdleConns, float64(stat.IdleConns()), []string{}, 1)
statsd.Gauge(MaxConns, float64(stat.MaxConns()), []string{}, 1)
statsd.Gauge(TotalConns, float64(stat.TotalConns()), []string{}, 1)
statsd.Gauge(NewConnsCount, float64(stat.NewConnsCount()), []string{}, 1)
statsd.Gauge(MaxLifetimeDestroyCount, float64(stat.MaxLifetimeDestroyCount()), []string{}, 1)
statsd.Gauge(MaxIdleDestroyCount, float64(stat.MaxIdleDestroyCount()), []string{}, 1)
}
}

func statsTags(c *config) []string {
tags := globalconfig.StatsTags()
if c.serviceName != "" {
tags = append(tags, "service:"+c.serviceName)
}
return tags
}
33 changes: 32 additions & 1 deletion contrib/jackc/pgx.v5/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

package pgx

import "gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
import (
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema"
)

type config struct {
serviceName string
Expand All @@ -14,6 +19,8 @@ type config struct {
traceCopyFrom bool
tracePrepare bool
traceConnect bool
poolStats bool
statsdClient internal.StatsdClient
}

func defaultConfig() *config {
Expand All @@ -27,6 +34,21 @@ func defaultConfig() *config {
}
}

// checkStatsdRequired adds a statsdClient onto the config if poolStats is enabled
// NOTE: For now, the only use-case for a statsdclient is the poolStats feature. If a statsdclient becomes necessary for other items in future work, then this logic should change
func (c *config) checkStatsdRequired() {
if c.poolStats && c.statsdClient == nil {
// contrib/jackc/pgx's statsdclient should always inherit its address from the tracer's statsdclient via the globalconfig
// destination is not user-configurable
sc, err := internal.NewStatsdClient(globalconfig.DogstatsdAddr(), statsTags(c))
if err == nil {
c.statsdClient = sc
} else {
log.Warn("Error creating statsd client for jackc/pgx contrib package; Pool stats will be dropped: %v", err)
bengentree marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

type Option func(*config)

// WithServiceName sets the service name to use for all spans.
Expand Down Expand Up @@ -81,3 +103,12 @@ func WithTraceConnect(enabled bool) Option {
c.traceConnect = enabled
}
}

// WithPoolStats enables polling of pgxpool.Stat metrics
// ref: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Stat
// These metrics are submitted to Datadog and are not billed as custom metrics
func WithPoolStats() Option {
return func(cfg *config) {
cfg.poolStats = true
}
}
24 changes: 24 additions & 0 deletions contrib/jackc/pgx.v5/option_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2022 Datadog, Inc.

package pgx

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestWithPoolStats(t *testing.T) {
t.Run("default off", func(t *testing.T) {
cfg := defaultConfig()
assert.False(t, cfg.poolStats)
})
t.Run("on", func(t *testing.T) {
cfg := new(config)
WithPoolStats()(cfg)
assert.True(t, cfg.poolStats)
})
}
2 changes: 2 additions & 0 deletions contrib/jackc/pgx.v5/pgx_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package pgx

import (
"context"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
Expand Down Expand Up @@ -59,6 +60,7 @@ func newPgxTracer(opts ...Option) *pgxTracer {
for _, opt := range opts {
opt(cfg)
}
cfg.checkStatsdRequired()
return &pgxTracer{cfg: cfg}
}

Expand Down
12 changes: 10 additions & 2 deletions contrib/jackc/pgx.v5/pgxpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ func NewPool(ctx context.Context, connString string, opts ...Option) (*pgxpool.P
}

func NewPoolWithConfig(ctx context.Context, config *pgxpool.Config, opts ...Option) (*pgxpool.Pool, error) {
config.ConnConfig.Tracer = newPgxTracer(opts...)
return pgxpool.NewWithConfig(ctx, config)
tracer := newPgxTracer(opts...)
config.ConnConfig.Tracer = tracer
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, err
}
if tracer.cfg.poolStats {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if tracer.cfg.poolStats {
if tracer.cfg.poolStats && tracer.cfg.statsdClient != nil {

If we failed to generate a statsd client, we don't want to attempt to submit metrics on it.
See this PR for more context/motivation: #2682

go pollPoolStats(tracer.cfg.statsdClient, pool)
}
return pool, nil
}
33 changes: 33 additions & 0 deletions contrib/jackc/pgx.v5/pgxpool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ package pgx
import (
"context"
"testing"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/statsdtest"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -38,3 +41,33 @@ func TestPool(t *testing.T) {
assert.Len(t, mt.OpenSpans(), 0)
assert.Len(t, mt.FinishedSpans(), 5)
}

func TestPoolWithPoolStats(t *testing.T) {
originalInterval := interval
interval = 1 * time.Millisecond
t.Cleanup(func() {
interval = originalInterval
})

ctx := context.Background()
statsd := new(statsdtest.TestStatsdClient)
conn, err := NewPool(ctx, postgresDSN, withStatsdClient(statsd), WithPoolStats())
require.NoError(t, err)
defer conn.Close()

wantStats := []string{AcquireCount, AcquireDuration, AcquiredConns, CanceledAcquireCount, ConstructingConns, EmptyAcquireCount, IdleConns, MaxConns, TotalConns, NewConnsCount, MaxLifetimeDestroyCount, MaxIdleDestroyCount}

assert := assert.New(t)
if err := statsd.Wait(assert, len(wantStats), time.Second); err != nil {
t.Fatalf("statsd.Wait(): %v", err)
}
for _, name := range wantStats {
assert.Contains(statsd.CallNames(), name)
}
}

func withStatsdClient(s internal.StatsdClient) Option {
return func(c *config) {
c.statsdClient = s
}
}