diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 90771a7b06..e7d2f9a0c2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,10 @@ updates: directory: "/instrumentation/database/sql/splunksql/test" schedule: interval: "daily" + - package-ecosystem: "gomod" + directory: "/instrumentation/github.com/lib/pq/splunkpq" + schedule: + interval: "daily" - package-ecosystem: "gomod" directory: "/instrumentation/net/http/splunkhttp" schedule: diff --git a/README.md b/README.md index 4fd906db57..aff06bdb55 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Additional recommended Splunk specific instrumentations: - [`splunksql`](./instrumentation/database/sql/splunksql) - [`splunkhttp`](./instrumentation/net/http/splunkhttp) +- [`splunkpq`](./instrumentation/github.com/lib/pq/splunkpq) ## Manual Instrumentation diff --git a/instrumentation/github.com/lib/pq/splunkpq/go.mod b/instrumentation/github.com/lib/pq/splunkpq/go.mod new file mode 100644 index 0000000000..301f5eafdb --- /dev/null +++ b/instrumentation/github.com/lib/pq/splunkpq/go.mod @@ -0,0 +1,11 @@ +module github.com/signalfx/splunk-otel-go/instrumentation/github.com/lib/pq/splunkpq + +go 1.15 + +require ( + github.com/lib/pq v1.10.3 + github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.7.0 +) + +replace github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql => ../../../../database/sql/splunksql diff --git a/instrumentation/github.com/lib/pq/splunkpq/go.sum b/instrumentation/github.com/lib/pq/splunkpq/go.sum new file mode 100644 index 0000000000..f22ddf320b --- /dev/null +++ b/instrumentation/github.com/lib/pq/splunkpq/go.sum @@ -0,0 +1,29 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/signalfx/splunk-otel-go v0.5.0 h1:UFP1uBXSEWJ3T6UkTC73P8+z3613uMJe9ZJxnYTuHlI= +github.com/signalfx/splunk-otel-go v0.5.0/go.mod h1:B+SJ2LDSAHJQqeV4ff9HLO4TbZRPQYEOW0Hpu48qUGE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/contrib/propagators v0.22.0/go.mod h1:xGOuXr6lLIF9BXipA4pm6UuOSI0M98U6tsI3khbOiwU= +go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM= +go.opentelemetry.io/otel v1.0.0-RC3 h1:kvwiyEkiUT/JaadXzVLI/R1wDO934A7r3Bs2wEe6wqA= +go.opentelemetry.io/otel v1.0.0-RC3/go.mod h1:Ka5j3ua8tZs4Rkq4Ex3hwgBgOchyPVq5S6P2lz//nKQ= +go.opentelemetry.io/otel/exporters/jaeger v1.0.0-RC2/go.mod h1:sZZqN3Vb0iT+NE6mZ1S7sNyH3t4PFk6ElK5TLGFBZ7E= +go.opentelemetry.io/otel/sdk v1.0.0-RC2/go.mod h1:fgwHyiDn4e5k40TD9VX243rOxXR+jzsWBZYA2P5jpEw= +go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4= +go.opentelemetry.io/otel/trace v1.0.0-RC3 h1:9F0ayEvlxv8BmNmPbU005WK7hC+7KbOazCPZjNa1yME= +go.opentelemetry.io/otel/trace v1.0.0-RC3/go.mod h1:VUt2TUYd8S2/ZRX09ZDFZQwn2RqfMB5MzO17jBojGxo= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/github.com/lib/pq/splunkpq/internal/connector.go b/instrumentation/github.com/lib/pq/splunkpq/internal/connector.go new file mode 100644 index 0000000000..f5661bd88b --- /dev/null +++ b/instrumentation/github.com/lib/pq/splunkpq/internal/connector.go @@ -0,0 +1,371 @@ +// Copyright Splunk Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 +// Blake Mizerany +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// Copied from package github.com/lib/pq: +// https://github.com/lib/pq/tree/v1.10.3 + +// Package internal provides copied conversion functionality internal to the github.com/lib/pq package. +package internal + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "os/user" + "sort" + "strings" + "unicode" +) + +// Common error types +var ( + errCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly") +) + +// Values are configuration setting values. +type Values map[string]string + +// ParseDSN returns the values parsed from a dsn string. +func ParseDSN(dsn string) (Values, error) { + var err error + o := make(Values) + + // A number of defaults are applied here, in this order: + // + // * Very low precedence defaults applied in every situation + // * Environment variables + // * Explicitly passed connection information + o["host"] = "localhost" + o["port"] = "5432" + for k, v := range parseEnviron(os.Environ()) { + o[k] = v + } + + if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { + dsn, err = parseURL(dsn) + if err != nil { + return nil, err + } + } + + if err := parseOpts(dsn, o); err != nil { + return nil, err + } + + // If a user is not provided by any other means, the last + // resort is to use the current operating system provided user + // name. + if _, ok := o["user"]; !ok { + u, err := userCurrent() + if err != nil { + return nil, err + } + o["user"] = u + } + + return o, nil +} + +// parseURL no longer needs to be used by clients of this library since supplying a URL as a +// connection string to sql.Open() is now supported: +// +// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full") +// +// It remains exported here for backwards-compatibility. +// +// parseURL converts a url to a connection string for driver.Open. +// Example: +// +// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full" +// +// converts to: +// +// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full" +// +// A minimal example: +// +// "postgres://" +// +// This will be blank, causing driver.Open to use all of the defaults +func parseURL(dsn string) (string, error) { + u, err := url.Parse(dsn) + if err != nil { + return "", err + } + + if u.Scheme != "postgres" && u.Scheme != "postgresql" { + return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) + } + + var kvs []string + escaper := strings.NewReplacer(`'`, `\'`, `\`, `\\`) + accrue := func(k, v string) { + if v != "" { + kvs = append(kvs, k+"='"+escaper.Replace(v)+"'") + } + } + + if u.User != nil { + v := u.User.Username() + accrue("user", v) + + v, _ = u.User.Password() + accrue("password", v) + } + + if host, port, err := net.SplitHostPort(u.Host); err != nil { + accrue("host", u.Host) + } else { + accrue("host", host) + accrue("port", port) + } + + if u.Path != "" { + accrue("dbname", u.Path[1:]) + } + + q := u.Query() + for k := range q { + accrue(k, q.Get(k)) + } + + sort.Strings(kvs) // Makes testing easier (not a performance concern) + return strings.Join(kvs, " "), nil +} + +// parseEnviron tries to mimic some of libpq's environment handling +// +// To ease testing, it does not directly reference os.Environ, but is +// designed to accept its output. +// +// Environment-set connection information is intended to have a higher +// precedence than a library default but lower than any explicitly +// passed information (such as in the URL or connection string). +func parseEnviron(env []string) (out map[string]string) { // nolint: funlen, gocyclo + out = make(map[string]string) + + for _, v := range env { + parts := strings.SplitN(v, "=", 2) // nolint: gomnd + + accrue := func(keyname string) { + out[keyname] = parts[1] + } + unsupported := func() { + panic(fmt.Sprintf("setting %v not supported", parts[0])) + } + + // The order of these is the same as is seen in the + // PostgreSQL 9.1 manual. Unsupported but well-defined + // keys cause a panic; these should be unset prior to + // execution. Options which pq expects to be set to a + // certain value are allowed, but must be set to that + // value if present (they can, of course, be absent). + switch parts[0] { + case "PGHOST": + accrue("host") + case "PGHOSTADDR": + unsupported() + case "PGPORT": + accrue("port") + case "PGDATABASE": + accrue("dbname") + case "PGUSER": + accrue("user") + case "PGPASSWORD": + accrue("password") + case "PGSERVICE", "PGSERVICEFILE", "PGREALM": + unsupported() + case "PGOPTIONS": + accrue("options") + case "PGAPPNAME": + accrue("application_name") + case "PGSSLMODE": + accrue("sslmode") + case "PGSSLCERT": + accrue("sslcert") + case "PGSSLKEY": + accrue("sslkey") + case "PGSSLROOTCERT": + accrue("sslrootcert") + case "PGREQUIRESSL", "PGSSLCRL": + unsupported() + case "PGREQUIREPEER": + unsupported() + case "PGKRBSRVNAME", "PGGSSLIB": + unsupported() + case "PGCONNECT_TIMEOUT": + accrue("connect_timeout") + case "PGCLIENTENCODING": + accrue("client_encoding") + case "PGDATESTYLE": + accrue("datestyle") + case "PGTZ": + accrue("timezone") + case "PGGEQO": + accrue("geqo") + case "PGSYSCONFDIR", "PGLOCALEDIR": + unsupported() + } + } + + return out +} + +// scanner implements a tokenizer for libpq-style option strings. +type scanner struct { + s []rune + i int +} + +// newScanner returns a new scanner initialized with the option string s. +func newScanner(s string) *scanner { + return &scanner{[]rune(s), 0} +} + +// Next returns the next rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) Next() (rune, bool) { + if s.i >= len(s.s) { + return 0, false + } + r := s.s[s.i] + s.i++ + return r, true +} + +// SkipSpaces returns the next non-whitespace rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) SkipSpaces() (rune, bool) { + r, ok := s.Next() + for unicode.IsSpace(r) && ok { + r, ok = s.Next() + } + return r, ok +} + +// parseOpts parses the options from name and adds them to the values. +// +// The parsing code is based on conninfo_parse from libpq's fe-connect.c +func parseOpts(name string, o Values) error { // nolint: gocyclo + s := newScanner(name) + + for { + var ( + keyRunes, valRunes []rune + r rune + ok bool + ) + + if r, ok = s.SkipSpaces(); !ok { + break + } + + // Scan the key + for !unicode.IsSpace(r) && r != '=' { + keyRunes = append(keyRunes, r) + if r, ok = s.Next(); !ok { + break + } + } + + // Skip any whitespace if we're not at the = yet + if r != '=' { + r, ok = s.SkipSpaces() + } + + // The current character should be = + if r != '=' || !ok { + return fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) + } + + // Skip any whitespace after the = + if r, ok = s.SkipSpaces(); !ok { + // If we reach the end here, the last value is just an empty string as per libpq. + o[string(keyRunes)] = "" + break + } + + if r != '\'' { + for !unicode.IsSpace(r) { + if r == '\\' { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`missing character after backslash`) + } + } + valRunes = append(valRunes, r) + + if r, ok = s.Next(); !ok { + break + } + } + } else { + quote: + for { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`unterminated quoted string literal in connection string`) + } + switch r { + case '\'': + break quote + case '\\': + r, _ = s.Next() + fallthrough + default: + valRunes = append(valRunes, r) + } + } + } + + o[string(keyRunes)] = string(valRunes) + } + + return nil +} + +func userCurrent() (string, error) { + u, err := user.Current() + if err == nil { + return u.Username, nil + } + + name := os.Getenv("USER") + if name != "" { + return name, nil + } + + return "", errCouldNotDetectUsername +} diff --git a/instrumentation/github.com/lib/pq/splunkpq/sql.go b/instrumentation/github.com/lib/pq/splunkpq/sql.go new file mode 100644 index 0000000000..acd9b90a4e --- /dev/null +++ b/instrumentation/github.com/lib/pq/splunkpq/sql.go @@ -0,0 +1,99 @@ +// Copyright Splunk Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package splunkpq provides instrumentation for the github.com/lib/pq +// package when using database/sql. +// +// To use this package, replace any blank identified imports of the +// github.com/lib/pq package with an import of this package and +// use the splunksql.Open function as a replacement for any sql.Open function +// use. For example, if your code looks like this to start. +// +// import ( +// "database/sql" +// _ "github.com/lib/pq" +// ) +// // ... +// db, err := sql.Open("postgres", "postgres://localhost:5432/dbname") +// // ... +// +// Update to this. +// +// import ( +// _ "github.com/signalfx/splunk-otel-go/instrumentation/github.com/lib/pq/splunkpq" +// "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql" +// ) +// // ... +// db, err := splunksql.Open("postgres", "postgres://localhost:5432/dbname") +// // ... +package splunkpq + +import ( + "fmt" + "sort" + "strconv" + "strings" + + // Make sure to import this so the instrumented driver is registered. + _ "github.com/lib/pq" + "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql" + "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql/dbsystem" + "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql/transport" + "github.com/signalfx/splunk-otel-go/instrumentation/github.com/lib/pq/splunkpq/internal" +) + +func init() { // nolint: gochecknoinits + splunksql.Register("postgres", splunksql.InstrumentationConfig{ + DBSystem: dbsystem.PostgreSQL, + DSNParser: DSNParser, + }) +} + +// DSNParser parses the data source connection name for a connection to a +// Postgres database using the github.com/lib/pq client package. +func DSNParser(dataSourceName string) (splunksql.ConnectionConfig, error) { + var connCfg splunksql.ConnectionConfig + vals, err := internal.ParseDSN(dataSourceName) + if err != nil { + return connCfg, err + } + + connCfg.Name = vals["dbname"] + connCfg.User = vals["user"] + if h, ok := vals["host"]; ok { + connCfg.Host = h + } else { + connCfg.Host = "localhost" + } + if strings.HasPrefix(connCfg.Host, "/") { + connCfg.Transport = transport.Unix + } else { + connCfg.Transport = transport.TCP + } + if pInt, err := strconv.Atoi(vals["port"]); err == nil { + connCfg.Port = pInt + } + + // Redact password. + delete(vals, "password") + parts := make([]string, 0, len(vals)) + for k, v := range vals { + parts = append(parts, fmt.Sprintf("%s=%s", k, v)) + } + // Make this reproducible. + sort.Strings(parts) + connCfg.ConnectionString = strings.Join(parts, " ") + + return connCfg, nil +} diff --git a/instrumentation/github.com/lib/pq/splunkpq/sql_test.go b/instrumentation/github.com/lib/pq/splunkpq/sql_test.go new file mode 100644 index 0000000000..ef222a778c --- /dev/null +++ b/instrumentation/github.com/lib/pq/splunkpq/sql_test.go @@ -0,0 +1,75 @@ +// Copyright Splunk Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package splunkpq_test + +import ( + "testing" + + "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql" + "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql/transport" + "github.com/signalfx/splunk-otel-go/instrumentation/github.com/lib/pq/splunkpq" + "github.com/stretchr/testify/assert" +) + +func TestDSNParser(t *testing.T) { + testcases := []struct { + name string + dsn string + connCfg splunksql.ConnectionConfig + errStr string + }{ + { + name: "invalid dsn", + dsn: "invalid dsn", + errStr: "missing \"=\" after \"invalid\" in connection info string\"", + }, + { + name: "url: tcp address", + dsn: "postgres://user:password@localhost:8080/testdb", + connCfg: splunksql.ConnectionConfig{ + Name: "testdb", + ConnectionString: "dbname=testdb host=localhost port=8080 user=user", + User: "user", + Host: "localhost", + Port: 8080, + Transport: transport.TCP, + }, + }, + { + name: "params: unix socket", + dsn: "user=user password=password host=/tmp/pgdb dbname=testdb", + connCfg: splunksql.ConnectionConfig{ + Name: "testdb", + ConnectionString: "dbname=testdb host=/tmp/pgdb port=5432 user=user", + User: "user", + Host: "/tmp/pgdb", + Port: 5432, + Transport: transport.Unix, + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got, err := splunkpq.DSNParser(tc.dsn) + if tc.errStr != "" { + assert.EqualError(t, err, tc.errStr) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.connCfg, got) + }) + } +}