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  }