github.com/newrelic/go-agent@v3.26.0+incompatible/_integrations/nrpq/nrpq.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 // +build go1.10 5 6 // Package nrpq instruments https://github.com/lib/pq. 7 // 8 // Use this package to instrument your PostgreSQL calls without having to manually 9 // create DatastoreSegments. This is done in a two step process: 10 // 11 // 1. Use this package's driver in place of the postgres driver. 12 // 13 // If your code is using sql.Open like this: 14 // 15 // import ( 16 // _ "github.com/lib/pq" 17 // ) 18 // 19 // func main() { 20 // db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") 21 // } 22 // 23 // Then change the side-effect import to this package, and open "nrpostgres" instead: 24 // 25 // import ( 26 // _ "github.com/newrelic/go-agent/_integrations/nrpq" 27 // ) 28 // 29 // func main() { 30 // db, err := sql.Open("nrpostgres", "user=pqgotest dbname=pqgotest sslmode=verify-full") 31 // } 32 // 33 // If your code is using pq.NewConnector, simply use nrpq.NewConnector 34 // instead. 35 // 36 // 2. Provide a context containing a newrelic.Transaction to all exec and query 37 // methods on sql.DB, sql.Conn, and sql.Tx. This requires using the 38 // context methods ExecContext, QueryContext, and QueryRowContext in place of 39 // Exec, Query, and QueryRow respectively. For example, instead of the 40 // following: 41 // 42 // row := db.QueryRow("SELECT count(*) FROM pg_catalog.pg_tables") 43 // 44 // Do this: 45 // 46 // ctx := newrelic.NewContext(context.Background(), txn) 47 // row := db.QueryRowContext(ctx, "SELECT count(*) FROM pg_catalog.pg_tables") 48 // 49 // Unfortunately, sql.Stmt exec and query calls are not supported since pq.stmt 50 // does not have ExecContext and QueryContext methods (as of June 2019, see 51 // https://github.com/lib/pq/pull/768). 52 // 53 // A working example is shown here: 54 // https://github.com/newrelic/go-agent/tree/master/_integrations/nrpq/example/main.go 55 package nrpq 56 57 import ( 58 "database/sql" 59 "database/sql/driver" 60 "os" 61 "path" 62 "regexp" 63 "strings" 64 65 "github.com/lib/pq" 66 newrelic "github.com/newrelic/go-agent" 67 "github.com/newrelic/go-agent/internal" 68 "github.com/newrelic/go-agent/internal/sqlparse" 69 ) 70 71 var ( 72 baseBuilder = newrelic.SQLDriverSegmentBuilder{ 73 BaseSegment: newrelic.DatastoreSegment{ 74 Product: newrelic.DatastorePostgres, 75 }, 76 ParseQuery: sqlparse.ParseQuery, 77 ParseDSN: parseDSN(os.Getenv), 78 } 79 ) 80 81 // NewConnector can be used in place of pq.NewConnector to get an instrumented 82 // PostgreSQL connector. 83 func NewConnector(dsn string) (driver.Connector, error) { 84 connector, err := pq.NewConnector(dsn) 85 if nil != err || nil == connector { 86 // Return nil rather than 'connector' since a nil pointer would 87 // be returned as a non-nil driver.Connector. 88 return nil, err 89 } 90 bld := baseBuilder 91 bld.ParseDSN(&bld.BaseSegment, dsn) 92 return newrelic.InstrumentSQLConnector(connector, bld), nil 93 } 94 95 func init() { 96 sql.Register("nrpostgres", newrelic.InstrumentSQLDriver(&pq.Driver{}, baseBuilder)) 97 internal.TrackUsage("integration", "driver", "postgres") 98 } 99 100 var dsnSplit = regexp.MustCompile(`(\w+)\s*=\s*('[^=]*'|[^'\s]+)`) 101 102 func getFirstHost(value string) string { 103 host := strings.SplitN(value, ",", 2)[0] 104 host = strings.Trim(host, "[]") 105 return host 106 } 107 108 func parseDSN(getenv func(string) string) func(*newrelic.DatastoreSegment, string) { 109 return func(s *newrelic.DatastoreSegment, dsn string) { 110 if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { 111 var err error 112 dsn, err = pq.ParseURL(dsn) 113 if nil != err { 114 return 115 } 116 } 117 118 host := getenv("PGHOST") 119 hostaddr := "" 120 ppoid := getenv("PGPORT") 121 dbname := getenv("PGDATABASE") 122 123 for _, split := range dsnSplit.FindAllStringSubmatch(dsn, -1) { 124 if len(split) != 3 { 125 continue 126 } 127 key := split[1] 128 value := strings.Trim(split[2], `'`) 129 130 switch key { 131 case "dbname": 132 dbname = value 133 case "host": 134 host = getFirstHost(value) 135 case "hostaddr": 136 hostaddr = getFirstHost(value) 137 case "port": 138 ppoid = strings.SplitN(value, ",", 2)[0] 139 } 140 } 141 142 if "" != hostaddr { 143 host = hostaddr 144 } else if "" == host { 145 host = "localhost" 146 } 147 if "" == ppoid { 148 ppoid = "5432" 149 } 150 if strings.HasPrefix(host, "/") { 151 // this is a unix socket 152 ppoid = path.Join(host, ".s.PGSQL."+ppoid) 153 host = "localhost" 154 } 155 156 s.Host = host 157 s.PortPathOrID = ppoid 158 s.DatabaseName = dbname 159 } 160 }