Skip to content

Commit dee324a

Browse files
authored
feat: pgx & pgxpool support (#136)
* Add pgx integration with tracing support and example application * Add pgx and pgxpool integration with New Relic tracing support * Refactor pgx and pgxpool integration functions for improved consistency and clarity
1 parent 0e8d112 commit dee324a

14 files changed

Lines changed: 1078 additions & 0 deletions

File tree

cmd/instrument.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/newrelic/go-easy-instrumentation/integrations/nrgochi"
1919
"github.com/newrelic/go-easy-instrumentation/integrations/nrgrpc"
2020
"github.com/newrelic/go-easy-instrumentation/integrations/nrnethttp"
21+
"github.com/newrelic/go-easy-instrumentation/integrations/nrpgx5"
2122
"github.com/newrelic/go-easy-instrumentation/integrations/nrslog"
2223
"github.com/newrelic/go-easy-instrumentation/internal/comment"
2324
"github.com/newrelic/go-easy-instrumentation/parser"
@@ -60,6 +61,7 @@ func registerIntegrations(manager *parser.InstrumentationManager) {
6061
nrecho_v3.InstrumentEchoFunction,
6162
nrgrpc.InstrumentGrpcServerMethod,
6263
nrslog.InstrumentSlogHandler,
64+
nrpgx5.InstrumentPgxHandler,
6365
)
6466

6567
// Stateful tracing functions (ORDER PRESERVED)

integrations/nrpgx5/codegen.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package nrpgx5
2+
3+
import (
4+
"go/token"
5+
6+
"github.com/dave/dst"
7+
)
8+
9+
const (
10+
PgxImportPath = "github.com/jackc/pgx/v5"
11+
PgxPoolImportPath = "github.com/jackc/pgx/v5/pgxpool"
12+
Nrpgx5ImportPath = "github.com/newrelic/go-agent/v3/integrations/nrpgx5"
13+
14+
// configVar is the name used for the synthesized *Config local. It is consistent
15+
// across pgx and pgxpool so that the same name appears in every replacement.
16+
configVar = "config"
17+
)
18+
19+
// CreateParseConfig creates `config, err := <pkg>.ParseConfig(connStr)` where pkg is
20+
// the package identified by importPath (PgxImportPath or PgxPoolImportPath).
21+
func CreateParseConfig(connStrExpr dst.Expr, importPath string) *dst.AssignStmt {
22+
return &dst.AssignStmt{
23+
Lhs: []dst.Expr{dst.NewIdent(configVar), dst.NewIdent("err")},
24+
Tok: token.DEFINE,
25+
Rhs: []dst.Expr{
26+
&dst.CallExpr{
27+
Fun: &dst.Ident{Name: "ParseConfig", Path: importPath},
28+
Args: []dst.Expr{dst.Clone(connStrExpr).(dst.Expr)},
29+
},
30+
},
31+
}
32+
}
33+
34+
// CreateTracerAssignment creates `<receiver>.Tracer = nrpgx5.NewTracer()`.
35+
// receiver selects the field that holds the tracer hook; pass dst.NewIdent("config")
36+
// for pgx, or config.ConnConfig for pgxpool.
37+
func CreateTracerAssignment(receiver dst.Expr) *dst.AssignStmt {
38+
return &dst.AssignStmt{
39+
Lhs: []dst.Expr{
40+
&dst.SelectorExpr{
41+
X: receiver,
42+
Sel: dst.NewIdent("Tracer"),
43+
},
44+
},
45+
Tok: token.ASSIGN,
46+
Rhs: []dst.Expr{newTracerCall()},
47+
}
48+
}
49+
50+
// CreateConnectWithConfig creates `<varName>, err := <fun>(ctx, config)`. fun is the
51+
// connect-from-config call expression — pgx.ConnectConfig or pgxpool.NewWithConfig.
52+
func CreateConnectWithConfig(varName string, ctxExpr dst.Expr, fun dst.Expr) *dst.AssignStmt {
53+
return &dst.AssignStmt{
54+
Lhs: []dst.Expr{dst.NewIdent(varName), dst.NewIdent("err")},
55+
Tok: token.DEFINE,
56+
Rhs: []dst.Expr{
57+
&dst.CallExpr{
58+
Fun: fun,
59+
Args: []dst.Expr{
60+
dst.Clone(ctxExpr).(dst.Expr),
61+
dst.NewIdent(configVar),
62+
},
63+
},
64+
},
65+
}
66+
}
67+
68+
// newTracerCall returns a nrpgx5.NewTracer() call expression.
69+
func newTracerCall() *dst.CallExpr {
70+
return &dst.CallExpr{
71+
Fun: &dst.Ident{Name: "NewTracer", Path: Nrpgx5ImportPath},
72+
}
73+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package nrpgx5
2+
3+
import (
4+
"go/token"
5+
"testing"
6+
7+
"github.com/dave/dst"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestCreateParseConfig(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
importPath string
15+
}{
16+
{name: "pgx", importPath: PgxImportPath},
17+
{name: "pgxpool", importPath: PgxPoolImportPath},
18+
}
19+
20+
for _, tt := range tests {
21+
t.Run(tt.name, func(t *testing.T) {
22+
connStr := &dst.BasicLit{Kind: token.STRING, Value: `"postgres://localhost/mydb"`}
23+
got := CreateParseConfig(connStr, tt.importPath)
24+
25+
assert.NotNil(t, got)
26+
assert.Equal(t, token.DEFINE, got.Tok)
27+
assert.Len(t, got.Lhs, 2)
28+
29+
configIdent, ok := got.Lhs[0].(*dst.Ident)
30+
assert.True(t, ok)
31+
assert.Equal(t, configVar, configIdent.Name)
32+
33+
errIdent, ok := got.Lhs[1].(*dst.Ident)
34+
assert.True(t, ok)
35+
assert.Equal(t, "err", errIdent.Name)
36+
37+
call, ok := got.Rhs[0].(*dst.CallExpr)
38+
assert.True(t, ok)
39+
assert.Len(t, call.Args, 1)
40+
41+
funIdent, ok := call.Fun.(*dst.Ident)
42+
assert.True(t, ok)
43+
assert.Equal(t, "ParseConfig", funIdent.Name)
44+
assert.Equal(t, tt.importPath, funIdent.Path)
45+
46+
// Connection string is cloned, not aliased — original must be untouched.
47+
arg, ok := call.Args[0].(*dst.BasicLit)
48+
assert.True(t, ok)
49+
assert.Equal(t, connStr.Value, arg.Value)
50+
assert.NotSame(t, connStr, arg)
51+
})
52+
}
53+
}
54+
55+
func TestCreateTracerAssignment(t *testing.T) {
56+
tests := []struct {
57+
name string
58+
receiver dst.Expr
59+
assertOnLhs func(t *testing.T, lhs *dst.SelectorExpr)
60+
}{
61+
{
62+
name: "direct config receiver (pgx)",
63+
receiver: dst.NewIdent(configVar),
64+
assertOnLhs: func(t *testing.T, lhs *dst.SelectorExpr) {
65+
assert.Equal(t, "Tracer", lhs.Sel.Name)
66+
ident, ok := lhs.X.(*dst.Ident)
67+
assert.True(t, ok)
68+
assert.Equal(t, configVar, ident.Name)
69+
},
70+
},
71+
{
72+
name: "nested config.ConnConfig receiver (pgxpool)",
73+
receiver: &dst.SelectorExpr{
74+
X: dst.NewIdent(configVar),
75+
Sel: dst.NewIdent("ConnConfig"),
76+
},
77+
assertOnLhs: func(t *testing.T, lhs *dst.SelectorExpr) {
78+
assert.Equal(t, "Tracer", lhs.Sel.Name)
79+
inner, ok := lhs.X.(*dst.SelectorExpr)
80+
assert.True(t, ok)
81+
assert.Equal(t, "ConnConfig", inner.Sel.Name)
82+
ident, ok := inner.X.(*dst.Ident)
83+
assert.True(t, ok)
84+
assert.Equal(t, configVar, ident.Name)
85+
},
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
got := CreateTracerAssignment(tt.receiver)
92+
93+
assert.NotNil(t, got)
94+
assert.Equal(t, token.ASSIGN, got.Tok)
95+
assert.Len(t, got.Lhs, 1)
96+
97+
lhsSel, ok := got.Lhs[0].(*dst.SelectorExpr)
98+
assert.True(t, ok)
99+
tt.assertOnLhs(t, lhsSel)
100+
101+
call, ok := got.Rhs[0].(*dst.CallExpr)
102+
assert.True(t, ok)
103+
104+
funIdent, ok := call.Fun.(*dst.Ident)
105+
assert.True(t, ok)
106+
assert.Equal(t, "NewTracer", funIdent.Name)
107+
assert.Equal(t, Nrpgx5ImportPath, funIdent.Path)
108+
})
109+
}
110+
}
111+
112+
func TestCreateConnectWithConfig(t *testing.T) {
113+
tests := []struct {
114+
name string
115+
varName string
116+
fun dst.Expr
117+
wantMethod string
118+
wantPath string
119+
}{
120+
{
121+
name: "pgx.ConnectConfig",
122+
varName: "conn",
123+
fun: &dst.Ident{Name: "ConnectConfig", Path: PgxImportPath},
124+
wantMethod: "ConnectConfig",
125+
wantPath: PgxImportPath,
126+
},
127+
{
128+
name: "pgxpool.NewWithConfig",
129+
varName: "pool",
130+
fun: &dst.Ident{Name: "NewWithConfig", Path: PgxPoolImportPath},
131+
wantMethod: "NewWithConfig",
132+
wantPath: PgxPoolImportPath,
133+
},
134+
}
135+
136+
for _, tt := range tests {
137+
t.Run(tt.name, func(t *testing.T) {
138+
ctxExpr := &dst.Ident{Name: "ctx"}
139+
got := CreateConnectWithConfig(tt.varName, ctxExpr, tt.fun)
140+
141+
assert.NotNil(t, got)
142+
assert.Equal(t, token.DEFINE, got.Tok)
143+
assert.Len(t, got.Lhs, 2)
144+
145+
lhsIdent, ok := got.Lhs[0].(*dst.Ident)
146+
assert.True(t, ok)
147+
assert.Equal(t, tt.varName, lhsIdent.Name)
148+
149+
errIdent, ok := got.Lhs[1].(*dst.Ident)
150+
assert.True(t, ok)
151+
assert.Equal(t, "err", errIdent.Name)
152+
153+
call, ok := got.Rhs[0].(*dst.CallExpr)
154+
assert.True(t, ok)
155+
156+
funIdent, ok := call.Fun.(*dst.Ident)
157+
assert.True(t, ok)
158+
assert.Equal(t, tt.wantMethod, funIdent.Name)
159+
assert.Equal(t, tt.wantPath, funIdent.Path)
160+
161+
assert.Len(t, call.Args, 2)
162+
configIdent, ok := call.Args[1].(*dst.Ident)
163+
assert.True(t, ok)
164+
assert.Equal(t, configVar, configIdent.Name)
165+
})
166+
}
167+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--- a/main.go
2+
+++ b/main.go
3+
@@ -3,15 +3,24 @@
4+
import (
5+
"context"
6+
"fmt"
7+
+ "time"
8+
9+
"github.com/jackc/pgx/v5"
10+
+ "github.com/newrelic/go-agent/v3/integrations/nrpgx5"
11+
+ "github.com/newrelic/go-agent/v3/newrelic"
12+
)
13+
14+
func main() {
15+
// Set up a local postgres docker container with:
16+
// docker run -it -p 5432:5432 -e POSTGRES_PASSWORD=password postgres
17+
+ NewRelicAgent, agentInitError := newrelic.NewApplication(newrelic.ConfigFromEnvironment())
18+
+ if agentInitError != nil {
19+
+ panic(agentInitError)
20+
+ }
21+
22+
- conn, err := pgx.Connect(context.Background(), "postgres://postgres:password@localhost/postgres")
23+
+ config, err := pgx.ParseConfig("postgres://postgres:password@localhost/postgres")
24+
+ config.Tracer = nrpgx5.NewTracer()
25+
+ conn, err := pgx.ConnectConfig(context.Background(), config)
26+
if err != nil {
27+
panic(err)
28+
}
29+
@@ -23,4 +24,6 @@
30+
panic(err)
31+
}
32+
fmt.Println(version)
33+
+
34+
+ NewRelicAgent.Shutdown(5 * time.Second)
35+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module pgx
2+
3+
go 1.25.0
4+
5+
require github.com/jackc/pgx/v5 v5.9.2
6+
7+
require (
8+
github.com/jackc/pgpassfile v1.0.0 // indirect
9+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
10+
golang.org/x/text v0.33.0 // indirect
11+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
5+
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
6+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
7+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
8+
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
9+
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
10+
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
11+
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
12+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
15+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
16+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
17+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
18+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
19+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
20+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
21+
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
22+
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
23+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
26+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/jackc/pgx/v5"
8+
)
9+
10+
func main() {
11+
// Set up a local postgres docker container with:
12+
// docker run -it -p 5432:5432 -e POSTGRES_PASSWORD=password postgres
13+
14+
conn, err := pgx.Connect(context.Background(), "postgres://postgres:password@localhost/postgres")
15+
if err != nil {
16+
panic(err)
17+
}
18+
defer conn.Close(context.Background())
19+
20+
var version string
21+
err = conn.QueryRow(context.Background(), "SELECT version()").Scan(&version)
22+
if err != nil {
23+
panic(err)
24+
}
25+
fmt.Println(version)
26+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--- a/main.go
2+
+++ b/main.go
3+
@@ -3,15 +3,24 @@
4+
import (
5+
"context"
6+
"fmt"
7+
+ "time"
8+
9+
"github.com/jackc/pgx/v5/pgxpool"
10+
+ "github.com/newrelic/go-agent/v3/integrations/nrpgx5"
11+
+ "github.com/newrelic/go-agent/v3/newrelic"
12+
)
13+
14+
func main() {
15+
// Set up a local postgres docker container with:
16+
// docker run -it -p 5432:5432 -e POSTGRES_PASSWORD=password postgres
17+
+ NewRelicAgent, agentInitError := newrelic.NewApplication(newrelic.ConfigFromEnvironment())
18+
+ if agentInitError != nil {
19+
+ panic(agentInitError)
20+
+ }
21+
22+
- pool, err := pgxpool.New(context.Background(), "postgres://postgres:password@localhost/postgres")
23+
+ config, err := pgxpool.ParseConfig("postgres://postgres:password@localhost/postgres")
24+
+ config.ConnConfig.Tracer = nrpgx5.NewTracer()
25+
+ pool, err := pgxpool.NewWithConfig(context.Background(), config)
26+
if err != nil {
27+
panic(err)
28+
}
29+
@@ -23,4 +24,6 @@
30+
panic(err)
31+
}
32+
fmt.Println(version)
33+
+
34+
+ NewRelicAgent.Shutdown(5 * time.Second)
35+
}

0 commit comments

Comments
 (0)