From 8acec7eb0423a2341613c27f932d22d295103715 Mon Sep 17 00:00:00 2001 From: Egon Firman Date: Sat, 22 Oct 2022 22:53:22 +0700 Subject: [PATCH 01/11] adding nrpgx5 tracer for github.com/jackc/pgx/v5 --- v3/integrations/nrpgx5/README.md | 10 + v3/integrations/nrpgx5/go.mod | 12 + v3/integrations/nrpgx5/nrpgx5.go | 178 ++++++++++ v3/integrations/nrpgx5/nrpgx5_test.go | 318 ++++++++++++++++++ .../nrpgx5/pgsnap_tracer_batch.txt | 40 +++ .../nrpgx5/pgsnap_tracer_connect.txt | 16 + .../nrpgx5/pgsnap_tracer_inpool.txt | 94 ++++++ .../nrpgx5/pgsnap_tracer_trace_crud.txt | 96 ++++++ 8 files changed, 764 insertions(+) create mode 100644 v3/integrations/nrpgx5/README.md create mode 100644 v3/integrations/nrpgx5/go.mod create mode 100644 v3/integrations/nrpgx5/nrpgx5.go create mode 100644 v3/integrations/nrpgx5/nrpgx5_test.go create mode 100644 v3/integrations/nrpgx5/pgsnap_tracer_batch.txt create mode 100644 v3/integrations/nrpgx5/pgsnap_tracer_connect.txt create mode 100644 v3/integrations/nrpgx5/pgsnap_tracer_inpool.txt create mode 100644 v3/integrations/nrpgx5/pgsnap_tracer_trace_crud.txt diff --git a/v3/integrations/nrpgx5/README.md b/v3/integrations/nrpgx5/README.md new file mode 100644 index 000000000..3e5070f66 --- /dev/null +++ b/v3/integrations/nrpgx5/README.md @@ -0,0 +1,10 @@ +# v3/integrations/nrpgx5 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5) + +Package `nrpgx` instruments https://github.com/jackc/pgx/v5. + +```go +import "github.com/newrelic/go-agent/v3/integrations/nrpgx5" +``` + +For more information, see +[godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5). diff --git a/v3/integrations/nrpgx5/go.mod b/v3/integrations/nrpgx5/go.mod new file mode 100644 index 000000000..920f56b2f --- /dev/null +++ b/v3/integrations/nrpgx5/go.mod @@ -0,0 +1,12 @@ +module github.com/newrelic/go-agent/v3/integrations/nrpgx5 + +go 1.11 + +require ( + github.com/egon12/pgsnap v0.0.0-20221022154027-2847f0124ed8 + github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jackc/pgx/v5 v5.0.3 + github.com/newrelic/go-agent/v3 v3.3.0 + github.com/stretchr/testify v1.8.0 + golang.org/x/crypto v0.1.0 // indirect +) diff --git a/v3/integrations/nrpgx5/nrpgx5.go b/v3/integrations/nrpgx5/nrpgx5.go new file mode 100644 index 000000000..6e1c833da --- /dev/null +++ b/v3/integrations/nrpgx5/nrpgx5.go @@ -0,0 +1,178 @@ +// Package nrpgx5 instruments https://github.com/jackc/pgx/v5. +// +// Use this package to instrument your PostgreSQL calls using the pgx +// library. +// +// This are the steps to instrument your pgx calls without using `database/sql`: +// if you want to use `database/sql`, you can use `nrpgx` package instead +// +// to instrument your pgx calls: +// you can set the tracer in the pgx.Config like this +// ```go +// import ( +// "context" +// "github.com/jackc/pgx/v5" +// "github.com/newrelic/go-agent/v3/integrations/nrpgx5" +// ) +// +// func main() { +// config, err := pgx.ParseConfig("postgres://user:password@localhost:5432/database") +// if err != nil { +// panic(err) +// } +// +// cfg.Tracer = nrpgx5.NewTracer() +// conn, err := pgx.ConnectConfig(context.Background(), cfg) +// if err != nil { +// panic(err) +// } +// } +// ``` +// or you can set the tracer in the pgxpool.Config like this +// ```go +// import ( +// "context" +// "github.com/jackc/pgx/v5" +// "github.com/jackc/pgx/v5/pgxpool" +// "github.com/newrelic/go-agent/v3/integrations/nrpgx5" +// ) +// +// func main() { +// config, err := pgxpool.ParseConfig("postgres://user:password@localhost:5432/database") +// if err != nil { +// panic(err) +// } +// +// cfg.ConnConfig.Tracer = nrpgx5.NewTracer() +// conn, err := pgxpool.ConnectConfig(context.Background(), cfg) +// if err != nil { +// panic(err) +// } +// } +// ``` + +package nrpgx5 + +import ( + "context" + "strconv" + + "github.com/jackc/pgx/v5" + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/newrelic/go-agent/v3/newrelic/sqlparse" +) + +func init() { + internal.TrackUsage("integration", "driver", "nrpgx5") +} + +type ( + Tracer struct { + BaseSegment newrelic.DatastoreSegment + ParseQuery func(segment *newrelic.DatastoreSegment, query string) + } + + nrPgxSegmentType string +) + +const ( + querySegmentKey nrPgxSegmentType = "nrPgx5Segment" + prepareSegmentKey nrPgxSegmentType = "prepareNrPgx5Segment" + batchSegmentKey nrPgxSegmentType = "batchNrPgx5Segment" +) + +func NewTracer() *Tracer { + return &Tracer{ + ParseQuery: sqlparse.ParseQuery, + } +} + +// TraceConnectStart is called at the beginning of Connect and ConnectConfig calls. The returned context is used for +// the rest of the call and will be passed to TraceConnectEnd. // implement pgx.ConnectTracer +func (t *Tracer) TraceConnectStart(ctx context.Context, data pgx.TraceConnectStartData) context.Context { + t.BaseSegment = newrelic.DatastoreSegment{ + Product: newrelic.DatastorePostgres, + Host: data.ConnConfig.Host, + PortPathOrID: strconv.FormatUint(uint64(data.ConnConfig.Port), 10), + DatabaseName: data.ConnConfig.Database, + } + + return ctx +} + +// TraceConnectEnd method // implement pgx.ConnectTracer +func (Tracer) TraceConnectEnd(ctx context.Context, data pgx.TraceConnectEndData) {} + +// TraceQueryStart is called at the beginning of Query, QueryRow, and Exec calls. The returned context is used for the +// rest of the call and will be passed to TraceQueryEnd. //implement pgx.QueryTracer +func (t *Tracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context { + segment := t.BaseSegment + segment.StartTime = newrelic.FromContext(ctx).StartSegmentNow() + segment.ParameterizedQuery = data.SQL + segment.QueryParameters = t.getQueryParameters(data.Args) + + // fill Operation and Collection + t.ParseQuery(&segment, data.SQL) + + return context.WithValue(ctx, querySegmentKey, &segment) +} + +// TraceQueryEnd method implement pgx.QueryTracer. It will try to get segment from context and end it. +func (t *Tracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) { + segment, ok := ctx.Value(querySegmentKey).(*newrelic.DatastoreSegment) + if !ok { + return + } + segment.End() +} + +func (t *Tracer) getQueryParameters(args []interface{}) map[string]interface{} { + result := map[string]interface{}{} + for i, arg := range args { + result["$"+strconv.Itoa(i)] = arg + } + return result +} + +// TraceBatchStart is called at the beginning of SendBatch calls. The returned context is used for the +// rest of the call and will be passed to TraceBatchQuery and TraceBatchEnd. // implement pgx.BatchTracer +func (t *Tracer) TraceBatchStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceBatchStartData) context.Context { + segment := t.BaseSegment + segment.StartTime = newrelic.FromContext(ctx).StartSegmentNow() + segment.Operation = "batch" + segment.Collection = "" + + return context.WithValue(ctx, batchSegmentKey, &segment) +} + +// TraceBatchQuery implement pgx.BatchTracer. In this method we will get query and store it in segment. +func (t *Tracer) TraceBatchQuery(ctx context.Context, conn *pgx.Conn, data pgx.TraceBatchQueryData) { + segment, ok := ctx.Value(batchSegmentKey).(*newrelic.DatastoreSegment) + if !ok { + return + } + + segment.ParameterizedQuery += data.SQL + "\n" +} + +// TraceBatchEnd implement pgx.BatchTracer. In this method we will get segment from context and fill it with +func (t *Tracer) TraceBatchEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceBatchEndData) { + segment, ok := ctx.Value(batchSegmentKey).(*newrelic.DatastoreSegment) + if !ok { + return + } + segment.End() +} + +// TracePrepareStart is called at the beginning of Prepare calls. The returned context is used for the +// rest of the call and will be passed to TracePrepareEnd. // implement pgx.PrepareTracer +// The Query and QueryRow will call prepare. Fill this function will make the datastore segment called twice. +// So this function woudln't do anything and just return the context. +func (t *Tracer) TracePrepareStart(ctx context.Context, conn *pgx.Conn, data pgx.TracePrepareStartData) context.Context { + return ctx +} + +// TracePrepareEnd implement pgx.PrepareTracer. In this function nothing happens. +func (t *Tracer) TracePrepareEnd(ctx context.Context, conn *pgx.Conn, data pgx.TracePrepareEndData) { +} diff --git a/v3/integrations/nrpgx5/nrpgx5_test.go b/v3/integrations/nrpgx5/nrpgx5_test.go new file mode 100644 index 000000000..90e6be1b3 --- /dev/null +++ b/v3/integrations/nrpgx5/nrpgx5_test.go @@ -0,0 +1,318 @@ +package nrpgx5 + +import ( + "context" + "net/url" + "os" + "strconv" + "testing" + + "github.com/egon12/pgsnap" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/stretchr/testify/assert" +) + +// to create pgnsap__** snapshot file, we are using real database. +// delete all pgnap_*.txt file and fill PGSNAP_DB_URL to recreate the snapshot file +// for example run it with +// ```sh +// PGSNAP_DB_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" go test -v -run TestTracer_Trace_CRUD +// ``` + +func TestTracer_Trace_CRUD(t *testing.T) { + con, finish := getTestCon(t) + defer finish() + + tests := []struct { + name string + fn func(context.Context, *pgx.Conn) + metric []internal.WantMetric + }{ + { + name: "query should send the metric after the row close", + fn: func(ctx context.Context, con *pgx.Conn) { + rows, _ := con.Query(ctx, "SELECT id, name, timestamp FROM mytable LIMIT $1", 2) + rows.Close() + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + {Name: "Datastore/statement/Postgres/mytable/select"}, + }, + }, + { + name: "queryrow should send the metric after scan", + fn: func(ctx context.Context, con *pgx.Conn) { + row := con.QueryRow(ctx, "SELECT id, name, timestamp FROM mytable") + _ = row.Scan() + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + {Name: "Datastore/statement/Postgres/mytable/select"}, + }, + }, + { + name: "insert should send the metric", + fn: func(ctx context.Context, con *pgx.Conn) { + _, _ = con.Exec(ctx, "INSERT INTO mytable(name) VALUES ($1)", "myname is") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/insert"}, + {Name: "Datastore/statement/Postgres/mytable/insert"}, + }, + }, + { + name: "update should send the metric", + fn: func(ctx context.Context, con *pgx.Conn) { + _, _ = con.Exec(ctx, "UPDATE mytable set name = $2 WHERE id = $1", 1, "myname is") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/update"}, + {Name: "Datastore/statement/Postgres/mytable/update"}, + }, + }, + { + name: "delete should send the metric", + fn: func(ctx context.Context, con *pgx.Conn) { + _, _ = con.Exec(ctx, "DELETE FROM mytable WHERE id = $1", 4) + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/delete"}, + {Name: "Datastore/statement/Postgres/mytable/delete"}, + }, + }, + { + name: "select 1 should send the metric", + fn: func(ctx context.Context, con *pgx.Conn) { + _, _ = con.Exec(ctx, "SELECT 1") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + }, + }, + { + name: "query error should also send the metric", + fn: func(ctx context.Context, con *pgx.Conn) { + _, _ = con.Query(ctx, "SELECT * FROM non_existent_table") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + {Name: "Datastore/statement/Postgres/non_existent_table/select"}, + }, + }, + { + name: "exec error should also send the metric", + fn: func(ctx context.Context, con *pgx.Conn) { + _, _ = con.Exec(ctx, "INSERT INTO non_existent_table(name) VALUES ($1)", "wrong name") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/insert"}, + {Name: "Datastore/statement/Postgres/non_existent_table/insert"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := integrationsupport.NewBasicTestApp() + txn := app.StartTransaction(t.Name()) + ctx := newrelic.NewContext(context.Background(), txn) + + tt.fn(ctx, con) + + txn.End() + app.ExpectMetricsPresent(t, tt.metric) + }) + } +} + +func TestTracer_connect(t *testing.T) { + conn, finish := getTestCon(t) + defer finish() + + cfg := conn.Config() + tracer := cfg.Tracer.(*Tracer) + + // hostname will + t.Run("connect should set tracer host port and database", func(t *testing.T) { + assert.Equal(t, cfg.Host, tracer.BaseSegment.Host) + assert.Equal(t, cfg.Database, tracer.BaseSegment.DatabaseName) + assert.Equal(t, strconv.FormatUint(uint64(cfg.Port), 10), tracer.BaseSegment.PortPathOrID) + }) + + t.Run("exec should send metric with instance host and port ", func(t *testing.T) { + app := integrationsupport.NewBasicTestApp() + + txn := app.StartTransaction(t.Name()) + + ctx := newrelic.NewContext(context.Background(), txn) + _, _ = conn.Exec(ctx, "INSERT INTO mytable(name) VALUES ($1)", "myname is") + + txn.End() + + app.ExpectMetricsPresent(t, []internal.WantMetric{ + {Name: "Datastore/instance/Postgres/" + getDBHostname() + "/" + tracer.BaseSegment.PortPathOrID}, + }) + }) +} + +func TestTracer_batch(t *testing.T) { + conn, finish := getTestCon(t) + defer finish() + + cfg := conn.Config() + tracer := cfg.Tracer.(*Tracer) + + t.Run("exec should send metric with instance host and port ", func(t *testing.T) { + app := integrationsupport.NewBasicTestApp() + + txn := app.StartTransaction(t.Name()) + + ctx := newrelic.NewContext(context.Background(), txn) + batch := &pgx.Batch{} + _ = batch.Queue("INSERT INTO mytable(name) VALUES ($1)", "name a") + _ = batch.Queue("INSERT INTO mytable(name) VALUES ($1)", "name b") + _ = batch.Queue("INSERT INTO mytable(name) VALUES ($1)", "name c") + _ = batch.Queue("SELECT id FROM mytable ORDER by id DESC LIMIT 1") + result := conn.SendBatch(ctx, batch) + + _ = result.Close() + + txn.End() + + app.ExpectMetricsPresent(t, []internal.WantMetric{ + {Name: "Datastore/instance/Postgres/" + getDBHostname() + "/" + tracer.BaseSegment.PortPathOrID}, + {Name: "Datastore/operation/Postgres/batch"}, + }) + }) +} + +func TestTracer_inPool(t *testing.T) { + snap := pgsnap.NewSnap(t, os.Getenv("PGSNAP_DB_URL")) + defer snap.Finish() + + cfg, _ := pgxpool.ParseConfig(snap.Addr()) + cfg.ConnConfig.Tracer = NewTracer() + + u, _ := url.Parse(snap.Addr()) + + con, _ := pgxpool.NewWithConfig(context.Background(), cfg) + + tests := []struct { + name string + fn func(context.Context, *pgxpool.Pool) + metric []internal.WantMetric + }{ + { + name: "query should send the metric after the row close", + fn: func(ctx context.Context, con *pgxpool.Pool) { + rows, _ := con.Query(ctx, "SELECT id, name, timestamp FROM mytable LIMIT $1", 2) + rows.Close() + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + {Name: "Datastore/statement/Postgres/mytable/select"}, + }, + }, + { + name: "queryrow should send the metric after scan", + fn: func(ctx context.Context, con *pgxpool.Pool) { + row := con.QueryRow(ctx, "SELECT id, name, timestamp FROM mytable") + _ = row.Scan() + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + {Name: "Datastore/statement/Postgres/mytable/select"}, + }, + }, + { + name: "insert should send the metric", + fn: func(ctx context.Context, con *pgxpool.Pool) { + _, _ = con.Exec(ctx, "INSERT INTO mytable(name) VALUES ($1)", "myname is") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/insert"}, + {Name: "Datastore/statement/Postgres/mytable/insert"}, + }, + }, + { + name: "update should send the metric", + fn: func(ctx context.Context, con *pgxpool.Pool) { + _, _ = con.Exec(ctx, "UPDATE mytable set name = $2 WHERE id = $1", 1, "myname is") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/update"}, + {Name: "Datastore/statement/Postgres/mytable/update"}, + }, + }, + { + name: "delete should send the metric", + fn: func(ctx context.Context, con *pgxpool.Pool) { + _, _ = con.Exec(ctx, "DELETE FROM mytable WHERE id = $1", 4) + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/delete"}, + {Name: "Datastore/statement/Postgres/mytable/delete"}, + }, + }, + { + name: "select 1 should send the metric", + fn: func(ctx context.Context, con *pgxpool.Pool) { + _, _ = con.Exec(ctx, "SELECT 1") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/operation/Postgres/select"}, + }, + }, + { + name: "metric should send the metric database instance", + fn: func(ctx context.Context, con *pgxpool.Pool) { + _, _ = con.Exec(ctx, "SELECT 1") + }, + metric: []internal.WantMetric{ + {Name: "Datastore/instance/Postgres/" + getDBHostname() + "/" + u.Port()}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + app := integrationsupport.NewBasicTestApp() + txn := app.StartTransaction(t.Name()) + ctx := newrelic.NewContext(context.Background(), txn) + + tt.fn(ctx, con) + + txn.End() + app.ExpectMetricsPresent(t, tt.metric) + }) + } +} + +func getTestCon(t testing.TB) (*pgx.Conn, func()) { + snap := pgsnap.NewSnap(t, os.Getenv("PGSNAP_DB_URL")) + + cfg, _ := pgx.ParseConfig(snap.Addr()) + cfg.Tracer = NewTracer() + + con, _ := pgx.ConnectConfig(context.Background(), cfg) + + return con, func() { + _ = con.Close(context.Background()) + snap.Finish() + } +} + +// getDBHostname that should be localhost or local hostname +// becase the db is listen in local +func getDBHostname() string { + h, err := os.Hostname() + if err != nil { + return "127.0.0.1" + } + + return h +} diff --git a/v3/integrations/nrpgx5/pgsnap_tracer_batch.txt b/v3/integrations/nrpgx5/pgsnap_tracer_batch.txt new file mode 100644 index 000000000..24655bde6 --- /dev/null +++ b/v3/integrations/nrpgx5/pgsnap_tracer_batch.txt @@ -0,0 +1,40 @@ +F {"Type":"Parse","Name":"stmtcache_9","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_9"} +F {"Type":"Parse","Name":"stmtcache_10","Query":"SELECT id FROM mytable ORDER by id DESC LIMIT 1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_10"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[1043]} +B {"Type":"NoData"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[]} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_9","ParameterFormatCodes":[0],"Parameters":[{"text":"name a"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_9","ParameterFormatCodes":[0],"Parameters":[{"text":"name b"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_9","ParameterFormatCodes":[0],"Parameters":[{"text":"name c"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_10","ParameterFormatCodes":null,"Parameters":[],"ResultFormatCodes":[1]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} +B {"Type":"BindComplete"} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1}]} +B {"Type":"DataRow","Values":[{"binary":"00000008"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Terminate"} diff --git a/v3/integrations/nrpgx5/pgsnap_tracer_connect.txt b/v3/integrations/nrpgx5/pgsnap_tracer_connect.txt new file mode 100644 index 000000000..6b13359cd --- /dev/null +++ b/v3/integrations/nrpgx5/pgsnap_tracer_connect.txt @@ -0,0 +1,16 @@ +F {"Type":"Parse","Name":"stmtcache_8","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_8"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[1043]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_8","ParameterFormatCodes":[0],"Parameters":[{"text":"myname is"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Terminate"} diff --git a/v3/integrations/nrpgx5/pgsnap_tracer_inpool.txt b/v3/integrations/nrpgx5/pgsnap_tracer_inpool.txt new file mode 100644 index 000000000..e3bf80ec2 --- /dev/null +++ b/v3/integrations/nrpgx5/pgsnap_tracer_inpool.txt @@ -0,0 +1,94 @@ +F {"Type":"Parse","Name":"stmtcache_11","Query":"SELECT id, name, timestamp FROM mytable LIMIT $1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_11"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[20]} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_11","ParameterFormatCodes":[1],"Parameters":[{"binary":"0000000000000002"}],"ResultFormatCodes":[1,0,1]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} +B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000003"},{"text":"Someone"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 2"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_12","Query":"SELECT id, name, timestamp FROM mytable","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_12"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[]} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_12","ParameterFormatCodes":null,"Parameters":[],"ResultFormatCodes":[1,0,1]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} +B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000003"},{"text":"Someone"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000001"},{"text":"myname is"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000005"},{"text":"myname is"},{"binary":"00028ec50fdbabf2"}]} +B {"Type":"DataRow","Values":[{"binary":"00000006"},{"text":"name a"},{"binary":"00028ec50fdbc3b3"}]} +B {"Type":"DataRow","Values":[{"binary":"00000007"},{"text":"name b"},{"binary":"00028ec50fdbc3b3"}]} +B {"Type":"DataRow","Values":[{"binary":"00000008"},{"text":"name c"},{"binary":"00028ec50fdbc3b3"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 7"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_13","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_13"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[1043]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_13","ParameterFormatCodes":[0],"Parameters":[{"text":"myname is"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_14","Query":"UPDATE mytable set name = $2 WHERE id = $1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_14"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[23,1043]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_14","ParameterFormatCodes":[1,0],"Parameters":[{"binary":"00000001"},{"text":"myname is"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"UPDATE 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_15","Query":"DELETE FROM mytable WHERE id = $1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_15"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[23]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_15","ParameterFormatCodes":[1],"Parameters":[{"binary":"00000004"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"DELETE 0"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Query","String":"SELECT 1"} +B {"Type":"RowDescription","Fields":[{"Name":"?column?","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} +B {"Type":"DataRow","Values":[{"text":"1"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Query","String":"SELECT 1"} +B {"Type":"RowDescription","Fields":[{"Name":"?column?","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} +B {"Type":"DataRow","Values":[{"text":"1"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} diff --git a/v3/integrations/nrpgx5/pgsnap_tracer_trace_crud.txt b/v3/integrations/nrpgx5/pgsnap_tracer_trace_crud.txt new file mode 100644 index 000000000..41e8f50ba --- /dev/null +++ b/v3/integrations/nrpgx5/pgsnap_tracer_trace_crud.txt @@ -0,0 +1,96 @@ +F {"Type":"Parse","Name":"stmtcache_1","Query":"SELECT id, name, timestamp FROM mytable LIMIT $1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_1"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[20]} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_1","ParameterFormatCodes":[1],"Parameters":[{"binary":"0000000000000002"}],"ResultFormatCodes":[1,0,1]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} +B {"Type":"DataRow","Values":[{"binary":"00000001"},{"text":"Adrian"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 2"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_2","Query":"SELECT id, name, timestamp FROM mytable","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_2"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[]} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":0}]} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_2","ParameterFormatCodes":null,"Parameters":[],"ResultFormatCodes":[1,0,1]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"RowDescription","Fields":[{"Name":"id","TableOID":16551,"TableAttributeNumber":1,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":1},{"Name":"name","TableOID":16551,"TableAttributeNumber":2,"DataTypeOID":1043,"DataTypeSize":-1,"TypeModifier":-1,"Format":0},{"Name":"timestamp","TableOID":16551,"TableAttributeNumber":3,"DataTypeOID":1184,"DataTypeSize":8,"TypeModifier":-1,"Format":1}]} +B {"Type":"DataRow","Values":[{"binary":"00000001"},{"text":"Adrian"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000002"},{"text":"Magdalena"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"DataRow","Values":[{"binary":"00000003"},{"text":"Someone"},{"binary":"00028ec50f7a0c27"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 3"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_3","Query":"INSERT INTO mytable(name) VALUES ($1)","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_3"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[1043]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_3","ParameterFormatCodes":[0],"Parameters":[{"text":"myname is"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"INSERT 0 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_4","Query":"UPDATE mytable set name = $2 WHERE id = $1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_4"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[23,1043]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_4","ParameterFormatCodes":[1,0],"Parameters":[{"binary":"00000001"},{"text":"myname is"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"UPDATE 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_5","Query":"DELETE FROM mytable WHERE id = $1","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_5"} +F {"Type":"Sync"} +B {"Type":"ParseComplete"} +B {"Type":"ParameterDescription","ParameterOIDs":[23]} +B {"Type":"NoData"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Bind","DestinationPortal":"","PreparedStatement":"stmtcache_5","ParameterFormatCodes":[1],"Parameters":[{"binary":"00000004"}],"ResultFormatCodes":[]} +F {"Type":"Describe","ObjectType":"P","Name":""} +F {"Type":"Execute","Portal":"","MaxRows":0} +F {"Type":"Sync"} +B {"Type":"BindComplete"} +B {"Type":"NoData"} +B {"Type":"CommandComplete","CommandTag":"DELETE 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Query","String":"SELECT 1"} +B {"Type":"RowDescription","Fields":[{"Name":"?column?","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":23,"DataTypeSize":4,"TypeModifier":-1,"Format":0}]} +B {"Type":"DataRow","Values":[{"text":"1"}]} +B {"Type":"CommandComplete","CommandTag":"SELECT 1"} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_6","Query":"SELECT * FROM non_existent_table","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_6"} +F {"Type":"Sync"} +B {"Type":"ErrorResponse","Severity":"ERROR","SeverityUnlocalized":"ERROR","Code":"42P01","Message":"relation \"non_existent_table\" does not exist","Detail":"","Hint":"","Position":15,"InternalPosition":0,"InternalQuery":"","Where":"","SchemaName":"","TableName":"","ColumnName":"","DataTypeName":"","ConstraintName":"","File":"parse_relation.c","Line":1384,"Routine":"parserOpenTable","UnknownFields":null} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Parse","Name":"stmtcache_7","Query":"INSERT INTO non_existent_table(name) VALUES ($1)","ParameterOIDs":null} +F {"Type":"Describe","ObjectType":"S","Name":"stmtcache_7"} +F {"Type":"Sync"} +B {"Type":"ErrorResponse","Severity":"ERROR","SeverityUnlocalized":"ERROR","Code":"42P01","Message":"relation \"non_existent_table\" does not exist","Detail":"","Hint":"","Position":13,"InternalPosition":0,"InternalQuery":"","Where":"","SchemaName":"","TableName":"","ColumnName":"","DataTypeName":"","ConstraintName":"","File":"parse_relation.c","Line":1384,"Routine":"parserOpenTable","UnknownFields":null} +B {"Type":"ReadyForQuery","TxStatus":"I"} +F {"Type":"Terminate"} From ea8ed9127b930665cc1062d704930252b5418427 Mon Sep 17 00:00:00 2001 From: Egon Firman Date: Wed, 2 Nov 2022 22:26:11 +0700 Subject: [PATCH 02/11] adding examples --- v3/integrations/nrpgx5/example/pgx/main.go | 53 +++++++++++++++++++ .../nrpgx5/example/pgxpool/main.go | 53 +++++++++++++++++++ v3/integrations/nrpgx5/nrpgx5.go | 33 ++++++------ 3 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 v3/integrations/nrpgx5/example/pgx/main.go create mode 100644 v3/integrations/nrpgx5/example/pgxpool/main.go diff --git a/v3/integrations/nrpgx5/example/pgx/main.go b/v3/integrations/nrpgx5/example/pgx/main.go new file mode 100644 index 000000000..758af98bb --- /dev/null +++ b/v3/integrations/nrpgx5/example/pgx/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/jackc/pgx/v5" + "github.com/newrelic/go-agent/v3/integrations/nrpgx5" + "github.com/newrelic/go-agent/v3/newrelic" +) + +func main() { + cfg, err := pgx.ParseConfig("postgres://postgres:postgres@localhost:5432") + if err != nil { + panic(err) + } + + cfg.Tracer = nrpgx5.NewTracer() + conn, err := pgx.ConnectConfig(context.Background(), cfg) + if err != nil { + panic(err) + } + + app, err := newrelic.NewApplication( + newrelic.ConfigAppName("PostgreSQL App"), + newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), + newrelic.ConfigDebugLogger(os.Stdout), + ) + if err != nil { + panic(err) + } + // + // N.B.: We do not recommend using app.WaitForConnection in production code. + // + app.WaitForConnection(5 * time.Second) + txn := app.StartTransaction("postgresQuery") + + ctx := newrelic.NewContext(context.Background(), txn) + row := conn.QueryRow(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") + count := 0 + err = row.Scan(&count) + if err != nil { + log.Println(err) + } + + txn.End() + app.Shutdown(5 * time.Second) + + fmt.Println("number of entries in pg_catalog.pg_tables", count) +} diff --git a/v3/integrations/nrpgx5/example/pgxpool/main.go b/v3/integrations/nrpgx5/example/pgxpool/main.go new file mode 100644 index 000000000..8457a478d --- /dev/null +++ b/v3/integrations/nrpgx5/example/pgxpool/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/newrelic/go-agent/v3/integrations/nrpgx5" + "github.com/newrelic/go-agent/v3/newrelic" +) + +func main() { + cfg, err := pgxpool.ParseConfig("postgres://postgres:postgres@localhost:5432") + if err != nil { + panic(err) + } + + cfg.ConnConfig.Tracer = nrpgx5.NewTracer() + db, err := pgxpool.NewWithConfig(context.Background(), cfg) + if err != nil { + panic(err) + } + + app, err := newrelic.NewApplication( + newrelic.ConfigAppName("PostgreSQL App"), + newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), + newrelic.ConfigDebugLogger(os.Stdout), + ) + if err != nil { + panic(err) + } + // + // N.B.: We do not recommend using app.WaitForConnection in production code. + // + app.WaitForConnection(5 * time.Second) + txn := app.StartTransaction("postgresQuery") + + ctx := newrelic.NewContext(context.Background(), txn) + row := db.QueryRow(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") + count := 0 + err = row.Scan(&count) + if err != nil { + log.Println(err) + } + + txn.End() + app.Shutdown(5 * time.Second) + + fmt.Println("number of entries in pg_catalog.pg_tables", count) +} diff --git a/v3/integrations/nrpgx5/nrpgx5.go b/v3/integrations/nrpgx5/nrpgx5.go index 6e1c833da..da3f41265 100644 --- a/v3/integrations/nrpgx5/nrpgx5.go +++ b/v3/integrations/nrpgx5/nrpgx5.go @@ -10,45 +10,44 @@ // you can set the tracer in the pgx.Config like this // ```go // import ( -// "context" // "github.com/jackc/pgx/v5" // "github.com/newrelic/go-agent/v3/integrations/nrpgx5" +// "github.com/newrelic/go-agent/v3/newrelic" // ) // // func main() { -// config, err := pgx.ParseConfig("postgres://user:password@localhost:5432/database") +// cfg, err := pgx.ParseConfig("postgres://postgres:postgres@localhost:5432") // if err != nil { // panic(err) -// } +// } // -// cfg.Tracer = nrpgx5.NewTracer() -// conn, err := pgx.ConnectConfig(context.Background(), cfg) -// if err != nil { -// panic(err) +// cfg.Tracer = nrpgx5.NewTracer() +// conn, err := pgx.ConnectConfig(context.Background(), cfg) +// if err != nil { +// panic(err) // } -// } +// ... // ``` // or you can set the tracer in the pgxpool.Config like this // ```go // import ( -// "context" -// "github.com/jackc/pgx/v5" // "github.com/jackc/pgx/v5/pgxpool" // "github.com/newrelic/go-agent/v3/integrations/nrpgx5" +// "github.com/newrelic/go-agent/v3/newrelic" // ) // // func main() { -// config, err := pgxpool.ParseConfig("postgres://user:password@localhost:5432/database") +// cfg, err := pgxpool.ParseConfig("postgres://postgres:postgres@localhost:5432") // if err != nil { // panic(err) -// } +// } // -// cfg.ConnConfig.Tracer = nrpgx5.NewTracer() -// conn, err := pgxpool.ConnectConfig(context.Background(), cfg) -// if err != nil { -// panic(err) +// cfg.ConnConfig.Tracer = nrpgx5.NewTracer() +// db, err := pgxpool.NewWithConfig(context.Background(), cfg) +// if err != nil { +// panic(err) // } -// } +// ... // ``` package nrpgx5 From f9708d0dce023cf11ddb03fc9bb0aca9a05b4c86 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Thu, 3 Nov 2022 02:28:45 -0700 Subject: [PATCH 03/11] introduced plural forms of the *Prefix trace options --- v3/newrelic/code_level_metrics.go | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/v3/newrelic/code_level_metrics.go b/v3/newrelic/code_level_metrics.go index f50936024..4c21669f7 100644 --- a/v3/newrelic/code_level_metrics.go +++ b/v3/newrelic/code_level_metrics.go @@ -129,24 +129,63 @@ func WithCodeLocation(loc *CodeLocation) TraceOption { // // If no prefix strings are passed here, the configured defaults will be used. // +// Deprecated: New code should use WithIgnoredPrefixes instead. +// func WithIgnoredPrefix(prefix ...string) TraceOption { return func(o *traceOptSet) { o.IgnoredPrefixes = prefix } } +// +// WithIgnoredPrefixes indicates that the code location reported +// for Code Level Metrics should be the first function in the +// call stack that does not begin with the given string (or any of the given strings if more than one are given). This +// string is matched against the entire fully-qualified function +// name, which includes the name of the package the function +// comes from. By default, the Go Agent tries to take the first +// function on the call stack that doesn't seem to be internal to +// the agent itself, but you can control this behavior using +// this option. +// +// If all functions in the call stack begin with this prefix, +// the outermost one will be used anyway, since we didn't find +// anything better on the way to the bottom of the stack. +// +// If no prefix strings are passed here, the configured defaults will be used. +// +func WithIgnoredPrefixes(prefix ...string) TraceOption { + return func(o *traceOptSet) { + o.IgnoredPrefixes = prefix + } +} + // // WithPathPrefix overrides the list of source code path prefixes // used to trim source file pathnames, providing a new set of one // or more path prefixes to use for this trace only. // If no strings are given, the configured defaults will be used. // +// Deprecated: New code should use WithPathPrefixes instead. +// func WithPathPrefix(prefix ...string) TraceOption { return func(o *traceOptSet) { o.PathPrefixes = prefix } } +// +// WithPathPrefixes overrides the list of source code path prefixes +// used to trim source file pathnames, providing a new set of one +// or more path prefixes to use for this trace only. +// If no strings are given, the configured defaults will be used. +// +func WithPathPrefixes(prefix ...string) TraceOption { + return func(o *traceOptSet) { + o.PathPrefixes = prefix + } +} + // // WithoutCodeLevelMetrics suppresses the collection and reporting // of Code Level Metrics for this trace. This helps avoid the overhead From f21dc383fde522e6f26b060c300b7aa0906d2416 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Thu, 10 Nov 2022 14:46:39 -0800 Subject: [PATCH 04/11] implemented length limits and unit tests --- CHANGELOG.md | 12 +++ v3/go.mod | 8 ++ v3/newrelic/code_level_metrics.go | 18 ++++- v3/newrelic/code_level_metrics_test.go | 105 ++++++++++++++++++++++++- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0610662bc..b1fa9becf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## Unreleased (working notes) + +### Changed + +* Changed the following `TraceOption` function to be consistent with their usage and other related identifier names. The old names remain for backward compatibility, but new code should use the new names. + * `WithIgnoredPrefix` -> `WithIgnoredPrefixes` + * `WithPathPrefix` -> `WithPathPrefixes` +* Implemented better handling of Code Level Metrics reporting when the data (e.g., function names) are excessively long, so that those attributes are suppressed rather than being reported with truncated names. Specifically: + * Attributes with values longer than 255 characters are dropped. + * No CLM attributes at all will be attached to a trace if the `code.function` attribute is empty or is longer than 255 characters. + * No CLM attributes at all will be attached to a trace if both `code.namespace` and `code.filepath` are longer than 255 characters. + ## 3.20.0 **PLEASE READ** these changes, and verify your config settings to ensure your application behaves how you intend it to. This release changes some default behaviors in the go agent. diff --git a/v3/go.mod b/v3/go.mod index 4eaf02fb4..c8c214856 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -6,3 +6,11 @@ require ( github.com/golang/protobuf v1.5.2 google.golang.org/grpc v1.49.0 ) + +require ( + golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/v3/newrelic/code_level_metrics.go b/v3/newrelic/code_level_metrics.go index 4c21669f7..561435e54 100644 --- a/v3/newrelic/code_level_metrics.go +++ b/v3/newrelic/code_level_metrics.go @@ -612,8 +612,18 @@ func reportCodeLevelMetrics(tOpts traceOptSet, run *appRun, setAttr func(string, function = location.Function[ns+1:] } - setAttr(AttributeCodeLineno, "", location.LineNo) - setAttr(AttributeCodeNamespace, namespace, nil) - setAttr(AttributeCodeFilepath, location.FilePath, nil) - setAttr(AttributeCodeFunction, function, nil) + // Impose data value size limits. + // Report no field over 255 characters in length. + // Report no CLM data at all if the function name is empty or >255 chars. + // Report no CLM data at all if both namespace and file path are >255 chars. + if function != "" && len(function) <= 255 && (len(namespace) <= 255 || len(location.FilePath) <= 255) { + setAttr(AttributeCodeLineno, "", location.LineNo) + setAttr(AttributeCodeFunction, function, nil) + if len(namespace) <= 255 { + setAttr(AttributeCodeNamespace, namespace, nil) + } + if len(location.FilePath) <= 255 { + setAttr(AttributeCodeFilepath, location.FilePath, nil) + } + } } diff --git a/v3/newrelic/code_level_metrics_test.go b/v3/newrelic/code_level_metrics_test.go index 0b1f0e520..991cb8749 100644 --- a/v3/newrelic/code_level_metrics_test.go +++ b/v3/newrelic/code_level_metrics_test.go @@ -1,9 +1,9 @@ // Copyright 2020 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 package newrelic import ( + "fmt" "reflect" "strings" "testing" @@ -253,3 +253,106 @@ func skipCCached(t *testing.T) { func TestCLMSkipCached(t *testing.T) { skipACached(t) } + +func attributeMapMatchesCLM(expected, actual map[string]interface{}) error { + for k, v := range expected { + actualValue, present := actual[k] + if !present { + return fmt.Errorf("Expected field \"%s\" was not present in output", k) + } + + switch value := v.(type) { + case int: + act, ok := actualValue.(int) + if !ok { + return fmt.Errorf("Expected value %v for %s was actually %v of type %T, not int", + v, k, actualValue, actualValue) + } + + if act != value { + return fmt.Errorf("Expected %s value %v but got %v", k, value, act) + } + + case string: + act, ok := actualValue.(string) + if !ok { + return fmt.Errorf("Expected value %v for %s was actually %v of type %T, not string", + v, k, actualValue, actualValue) + } + + if act != value { + return fmt.Errorf("Expected %s value %v but got %v", k, value, act) + } + + default: + return fmt.Errorf("Test case does not consider expected value %v for type %T", k, v) + } + } + + if len(expected) != len(actual) { + return fmt.Errorf("expected %d fields, got %d", len(expected), len(actual)) + } + + return nil +} + +func TestLongCLMNames(t *testing.T) { + for i, testData := range []struct { + loc CodeLocation + expected map[string]interface{} + }{ + //0 + {CodeLocation{42, "main.aFunction", "/usr/local/foo.go"}, + map[string]interface{}{ + AttributeCodeLineno: 42, + AttributeCodeFunction: "aFunction", + AttributeCodeNamespace: "main", + AttributeCodeFilepath: "/usr/local/foo.go", + }}, + //1 + {CodeLocation{42, "main.aFunction", "/usr/local/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo.go"}, + map[string]interface{}{ + AttributeCodeLineno: 42, + AttributeCodeFunction: "aFunction", + AttributeCodeNamespace: "main", + }}, + //2 + {CodeLocation{42, "main.aFunctionLoremipsumdolorsitamet.consecteturadipiscingelit.seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.Utenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequat.Duisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariatur.Excepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum", "/usr/local/foo.go"}, + map[string]interface{}{ + AttributeCodeLineno: 42, + AttributeCodeFunction: "Excepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum", + AttributeCodeFilepath: "/usr/local/foo.go", + }}, + //3 + {CodeLocation{42, "mainaFunctionLoremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariaturExcepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum", "/usr/local/foo.go"}, + map[string]interface{}{}}, + //4 + {CodeLocation{42, "", "/usr/local/foo.go"}, + map[string]interface{}{}}, + //5 + {CodeLocation{42, "mainmainaFunctionLoremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariaturExcepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum.aFunction", "/usr/local/foo.go"}, + map[string]interface{}{ + AttributeCodeLineno: 42, + AttributeCodeFunction: "aFunction", + AttributeCodeFilepath: "/usr/local/foo.go", + }}, + //6 + {CodeLocation{42, "mainmainaFunctionLoremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliquaUtenimadminimveniamquisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequatDuisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariaturExcepteursintoccaecatcupidatatnonproidentsuntinculpaquiofficiadeseruntmollitanimidestlaborum.aFunction", "/usr/local/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafoo.go"}, + map[string]interface{}{}}, + } { + actual := make(map[string]interface{}) + reportCodeLevelMetrics(traceOptSet{ + LocationOverride: &testData.loc, + PathPrefixes: []string{"xyzzy"}, + }, nil, func(k, s string, v interface{}) { + if v == nil { + actual[k] = s + } else { + actual[k] = v + } + }) + if err := attributeMapMatchesCLM(testData.expected, actual); err != nil { + t.Errorf("testcase %d: %v", i, err) + } + } +} From b21c9737473b9a5986f3dfe33cd776805745ad3e Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Thu, 10 Nov 2022 14:51:39 -0800 Subject: [PATCH 05/11] cleaned go.mod --- v3/go.mod | 8 -------- 1 file changed, 8 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index c8c214856..4eaf02fb4 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -6,11 +6,3 @@ require ( github.com/golang/protobuf v1.5.2 google.golang.org/grpc v1.49.0 ) - -require ( - golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect - golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect - golang.org/x/text v0.3.3 // indirect - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect - google.golang.org/protobuf v1.27.1 // indirect -) From e1ee0c84dc943469f5073cb1d402a029e0eb0d97 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 14 Nov 2022 14:51:20 -0500 Subject: [PATCH 06/11] prep nrpgx5 pr for release --- .github/workflows/ci.yaml | 3 +++ v3/integrations/nrpgx5/go.mod | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e98575f41..3bd4e4863 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -188,6 +188,9 @@ jobs: - go-version: 1.17.x dirs: v3/integrations/nrpq extratesting: go get -u github.com/lib/pq@master + - go-version: 1.17.x + dirs: v3/integrations/nrpgx5 + extratesting: go get -u github.com/jackc/pgx/v5@master - go-version: 1.17.x dirs: v3/integrations/nrpq/example/sqlx - go-version: 1.17.x diff --git a/v3/integrations/nrpgx5/go.mod b/v3/integrations/nrpgx5/go.mod index 920f56b2f..5e8329cfc 100644 --- a/v3/integrations/nrpgx5/go.mod +++ b/v3/integrations/nrpgx5/go.mod @@ -6,7 +6,7 @@ require ( github.com/egon12/pgsnap v0.0.0-20221022154027-2847f0124ed8 github.com/jackc/pgx/v4 v4.17.2 // indirect github.com/jackc/pgx/v5 v5.0.3 - github.com/newrelic/go-agent/v3 v3.3.0 + github.com/newrelic/go-agent/v3 v3.20.0 github.com/stretchr/testify v1.8.0 golang.org/x/crypto v0.1.0 // indirect ) From 34f6cdf25f57a84fa9938b51a7822ba556a636d6 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 14 Nov 2022 14:54:51 -0500 Subject: [PATCH 07/11] nrpgx5 no extratesting due to deps issue --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3bd4e4863..fc06d3d15 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -190,7 +190,6 @@ jobs: extratesting: go get -u github.com/lib/pq@master - go-version: 1.17.x dirs: v3/integrations/nrpgx5 - extratesting: go get -u github.com/jackc/pgx/v5@master - go-version: 1.17.x dirs: v3/integrations/nrpq/example/sqlx - go-version: 1.17.x From 75fd2c351ad518334c4284614e4e638672d69dc6 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 14 Nov 2022 15:00:04 -0500 Subject: [PATCH 08/11] bump nrpgx5 to go version 1.17 to align with EOL policy --- .github/workflows/ci.yaml | 1 + v3/integrations/nrpgx5/go.mod | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fc06d3d15..3bd4e4863 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -190,6 +190,7 @@ jobs: extratesting: go get -u github.com/lib/pq@master - go-version: 1.17.x dirs: v3/integrations/nrpgx5 + extratesting: go get -u github.com/jackc/pgx/v5@master - go-version: 1.17.x dirs: v3/integrations/nrpq/example/sqlx - go-version: 1.17.x diff --git a/v3/integrations/nrpgx5/go.mod b/v3/integrations/nrpgx5/go.mod index 5e8329cfc..318391be1 100644 --- a/v3/integrations/nrpgx5/go.mod +++ b/v3/integrations/nrpgx5/go.mod @@ -1,12 +1,10 @@ module github.com/newrelic/go-agent/v3/integrations/nrpgx5 -go 1.11 +go 1.17 require ( github.com/egon12/pgsnap v0.0.0-20221022154027-2847f0124ed8 - github.com/jackc/pgx/v4 v4.17.2 // indirect github.com/jackc/pgx/v5 v5.0.3 github.com/newrelic/go-agent/v3 v3.20.0 github.com/stretchr/testify v1.8.0 - golang.org/x/crypto v0.1.0 // indirect ) From 164c417b17f80f5ce08b2b591938ea0f5081ff6b Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 14 Nov 2022 15:08:13 -0500 Subject: [PATCH 09/11] bump nrpgx5 test to go 1.18 --- .github/workflows/ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3bd4e4863..7e1ea8c19 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -188,9 +188,8 @@ jobs: - go-version: 1.17.x dirs: v3/integrations/nrpq extratesting: go get -u github.com/lib/pq@master - - go-version: 1.17.x + - go-version: 1.18.x dirs: v3/integrations/nrpgx5 - extratesting: go get -u github.com/jackc/pgx/v5@master - go-version: 1.17.x dirs: v3/integrations/nrpq/example/sqlx - go-version: 1.17.x From 9499c837625a2696b54655488cda38a29996ddae Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Tue, 15 Nov 2022 10:28:51 -0800 Subject: [PATCH 10/11] release 3.20.1 changelog and version increment --- CHANGELOG.md | 12 +++++++++++- v3/newrelic/version.go | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1fa9becf..9670337b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -## Unreleased (working notes) +## 3.20.1 + +### Added +* New integration `nrpgx5` v1.0.0 to instrument `github.com/jackc/pgx/v5`. ### Changed @@ -10,6 +13,13 @@ * No CLM attributes at all will be attached to a trace if the `code.function` attribute is empty or is longer than 255 characters. * No CLM attributes at all will be attached to a trace if both `code.namespace` and `code.filepath` are longer than 255 characters. +### Support Statement +New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. + +We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. + +See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. + ## 3.20.0 **PLEASE READ** these changes, and verify your config settings to ensure your application behaves how you intend it to. This release changes some default behaviors in the go agent. diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index 135afe0a8..d1cc2dfce 100644 --- a/v3/newrelic/version.go +++ b/v3/newrelic/version.go @@ -11,7 +11,7 @@ import ( const ( // Version is the full string version of this Go Agent. - Version = "3.20.0" + Version = "3.20.1" ) var ( From fd8c44aa4f5368a2ebd196ba6a8182ffaa378bd3 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Tue, 15 Nov 2022 10:37:35 -0800 Subject: [PATCH 11/11] added readme update for nrpgx5 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 732a7843b..d8880328f 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ package primitives can be found [here](GUIDE.md#datastore-segments). | ------------- | ------------- | - | | [lib/pq](https://github.com/lib/pq) | [v3/integrations/nrpq](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpq) | Instrument PostgreSQL driver (`pq` driver for `database/sql`) | | [jackc/pgx](https://github.com/jackc/pgx) | [v3/integrations/nrpgx](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx) | Instrument PostgreSQL driver (`pgx` driver for `database/sql`)| +| [jackc/pgx/v5](https://github.com/jackc/pgx/v5) | [v3/integrations/nrpgx5](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrpgx5) | Instrument PostgreSQL driver (`pgx/v5` driver for `database/sql`)| | [go-mssqldb](github.com/denisenkom/go-mssqldb) | [v3/integrations/nrmssql](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmssql) | Instrument MS SQL driver | | [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) | [v3/integrations/nrmysql](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrmysql) | Instrument MySQL driver | | [elastic/go-elasticsearch](https://github.com/elastic/go-elasticsearch) | [v3/integrations/nrelasticsearch-v7](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrelasticsearch-v7) | Instrument Elasticsearch datastore calls |