Skip to content

Commit

Permalink
Add instrumentation for the database/sql package (#88)
Browse files Browse the repository at this point in the history
* Initial instrumentation for database/sql

* Add withClientSpan method to config

* Add trace.go

* Use withClientSpan and fallbacks to non-ctx methods

* Move span names to internal package

* Add initial integartion test

* Add config unit tests

* Target integration tests for spans only

* Add integration testing for stmt, rows, and tx

* Add comment for why all spans are clients

* Move config to internal package

* Revert "Move config to internal package"

This reverts commit a69a5e5.

* Rename withClientSpan to withSpan

* Add DSN parsing

* Update span name to comply with OTel

* Update test with new options pattern

* Add instrumentation registration function

Have instrumentation register setup configuration for the driver they
are instrumenting. This moves the internal dsn parsing to the
instrumentation itself.

* Update go.opentelemetry.io/otel* to v1.0.0-RC3

* Update int test with base attrs

* Document the moniker package

* Add package documentation for the test package

* Run go mod tidy for splunksql

* Add copyright notice to all source files

* Comment exported objects in the moniker pkg

* Fix exported comments in the dbsystem pkg

* Add comment for the Option type

* Skip staticcheck for backwards compatible iface support

* Quite lint errors in suite

* Fix lint issues with the test package

* Rename test files

* Nolint in test package

* No gocritic lint of value receivers

* Remove unused parameters and add nolint comments

* make gendependabot

* Add config tests

* Add unit tests for splunksql

* Add remaining unit tests

* Fix dependabot config

* Add splunksql to README

* Fix go.sum entries

* Consolidate mock types to single file for test pkg

* Use assertion to test span kind

* Send configuration errors to generic OTel error handler

* Move dbsystem package into splunksql

* Move transport pkg into splunksql

* Remove rows instrumentation

* Comment nolint comments
  • Loading branch information
MrAlias authored Sep 20, 2021
1 parent ebc201f commit 399ffb9
Show file tree
Hide file tree
Showing 28 changed files with 2,771 additions and 1 deletion.
6 changes: 5 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ updates:
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/build"
directory: "/instrumentation/database/sql/splunksql"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/instrumentation/database/sql/splunksql/test"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Supported libraries are listed
Additional recommended Splunk specific instrumentations:
- [`splunksql`](./instrumentation/database/sql/splunksql)
- [`splunkhttp`](./instrumentation/net/http/splunkhttp)
## Manual Instrumentation
Expand Down
267 changes: 267 additions & 0 deletions instrumentation/database/sql/splunksql/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// 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 splunksql

import (
"context"
"fmt"
"net"
"net/url"
"strconv"
"strings"

splunkotel "github.com/signalfx/splunk-otel-go"
"github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql/internal/moniker"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)

// instrumentationName is the instrumentation library identifier for a Tracer.
const instrumentationName = "github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql"

// traceConfig contains tracing configuration options.
type traceConfig struct {
TracerProvider trace.TracerProvider

DBName string
Attributes []attribute.KeyValue
}

func newTraceConfig(options ...Option) traceConfig {
var c traceConfig
for _, o := range options {
if o != nil {
o.apply(&c)
}
}
if c.TracerProvider == nil {
c.TracerProvider = otel.GetTracerProvider()
}
return c
}

// tracer returns an OTel tracer from the appropriate TracerProvider.
//
// If the passed context contains a span, the TracerProvider that created the
// tracer that created that span will be used. Otherwise, the TracerProvider
// from c is used.
func (c traceConfig) tracer(ctx context.Context) trace.Tracer {
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
return span.TracerProvider().Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
}
return c.TracerProvider.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
}

// withSpan wraps the function f with a span.
func (c traceConfig) withSpan(ctx context.Context, m moniker.Span, f func(context.Context) error, opts ...trace.SpanStartOption) error {
opts = append([]trace.SpanStartOption{trace.WithAttributes(c.Attributes...)}, opts...)
// From the specification: span kind MUST always be CLIENT.
opts = append(opts, trace.WithSpanKind(trace.SpanKindClient))

var (
err error
span trace.Span
)
ctx, span = c.tracer(ctx).Start(ctx, c.spanName(m), opts...)
defer func() {
handleErr(span, err)
span.End()
}()

err = f(ctx)
return err
}

// spanName returns the OpenTelemetry compliant span name.
func (c traceConfig) spanName(m moniker.Span) string {
// From the OpenTelemetry semantic conventions
// (https://github.com/open-telemetry/opentelemetry-specification/blob/v1.6.1/specification/trace/semantic_conventions/database.md):
//
// > The **span name** SHOULD be set to a low cardinality value representing the statement executed on the database.
// > It MAY be a stored procedure name (without arguments), DB statement without variable arguments, operation name, etc.
// > Since SQL statements may have very high cardinality even without arguments, SQL spans SHOULD be named the
// > following way, unless the statement is known to be of low cardinality:
// > `<db.operation> <db.name>.<db.sql.table>`, provided that `db.operation` and `db.sql.table` are available.
// > If `db.sql.table` is not available due to its semantics, the span SHOULD be named `<db.operation> <db.name>`.
// > It is not recommended to attempt any client-side parsing of `db.statement` just to get these properties,
// > they should only be used if the library being instrumented already provides them.
// > When it's otherwise impossible to get any meaningful span name, `db.name` or the tech-specific database name MAY be used.
//
// The database/sql package does not provide the database operation nor
// the SQL table the operation is being performed on during a call. It
// would require client-side parsing of the statement to determine these
// properties. Therefore, the database name is used if it is known.
if c.DBName != "" {
return c.DBName
}

// The database name is not known. Fallback to the known client-side
// operation being performed. This will comply with the low cardinality
// recommendation of the specification.
return m.String()
}

// Option applies options to a tracing configuration.
type Option interface {
apply(*traceConfig)
}

type optionFunc func(*traceConfig)

func (o optionFunc) apply(c *traceConfig) {
o(c)
}

// WithTracerProvider returns an Option that sets the TracerProvider used with
// this instrumentation library.
func WithTracerProvider(tp trace.TracerProvider) Option {
return optionFunc(func(c *traceConfig) {
c.TracerProvider = tp
})
}

// WithAttributes returns an Option that appends attr to the attributes set
// for every span created with this instrumentation library.
func WithAttributes(attr []attribute.KeyValue) Option {
return optionFunc(func(c *traceConfig) {
c.Attributes = append(c.Attributes, attr...)
})
}

// withRegistrationConfig returns an Option that sets database attributes
// required and recommended by the OpenTelemetry semantic conventions based on
// the information instrumentation registered.
func withRegistrationConfig(regCfg InstrumentationConfig, dsn string) Option {
var connCfg ConnectionConfig
if regCfg.DSNParser != nil {
var err error
connCfg, err = regCfg.DSNParser(dsn)
otel.Handle(err)
} else {
// Fallback. This is a best effort attempt if we do not know how to
// explicitly parse the DSN.
connCfg, _ = urlDSNParse(dsn)
}

attrs, err := connCfg.Attributes()
otel.Handle(err)
attrs = append(attrs, regCfg.DBSystem.Attribute())

return optionFunc(func(c *traceConfig) {
c.DBName = connCfg.Name
c.Attributes = append(c.Attributes, attrs...)
})
}

// ConnectionConfig are the relevant settings parsed from a database
// connection.
type ConnectionConfig struct {
// Name of the database being accessed.
Name string
// ConnectionString is the sanitized connection string (all credentials
// have been redacted) used to connect to the database.
ConnectionString string
// User is the username used to access the database.
User string
// Host is the IP or hostname of the database.
Host string
// Port is the port the database is lisening on.
Port int
// NetTransport is the transport protocol used to connect to the database.
NetTransport NetTransport
}

// Attributes returns the connection settings as attributes compliant with
// OpenTelemetry semantic coventions. If the settings do not conform to
// OpenTelemetry requirements an error is returned with a partial list of
// attributes that do conform.
func (c ConnectionConfig) Attributes() ([]attribute.KeyValue, error) { // nolint: gocritic // This is short lived, pass the type.
var attrs []attribute.KeyValue
var errs []string
if c.Name != "" {
attrs = append(attrs, semconv.DBNameKey.String(c.Name))
}
if c.ConnectionString != "" {
attrs = append(attrs, semconv.DBConnectionStringKey.String(c.ConnectionString))
}
if c.User != "" {
attrs = append(attrs, semconv.DBUserKey.String(c.User))
}
if c.Host != "" {
if ip := net.ParseIP(c.Host); ip != nil {
attrs = append(attrs, semconv.NetPeerIPKey.String(ip.String()))
} else {
attrs = append(attrs, semconv.NetPeerNameKey.String(c.Host))
}
} else {
errs = append(errs, "missing required peer IP or hostname")
}
if c.Port > 0 {
attrs = append(attrs, semconv.NetPeerPortKey.Int(c.Port))
}
attrs = append(attrs, c.NetTransport.Attribute())

var err error
if len(errs) > 0 {
err = fmt.Errorf("invalid connection config: %s", strings.Join(errs, ", "))
}
return attrs, err
}

func urlDSNParse(dataSourceName string) (ConnectionConfig, error) {
var connCfg ConnectionConfig
u, err := url.Parse(dataSourceName)
if err != nil {
return connCfg, err
}

connCfg.Host = u.Hostname()
if p, err := strconv.Atoi(u.Port()); err == nil {
connCfg.Port = p
}

if u.User != nil {
connCfg.User = u.User.Username()
if _, ok := u.User.Password(); ok {
// Redact password.
u.User = url.User(u.User.Username())
}
}

connCfg.ConnectionString = u.String()

return connCfg, nil
}

// DSNParser processes a driver-specific data source name into
// connection-level attributes conforming with the OpenTelemetry semantic
// conventions.
type DSNParser func(dataSourceName string) (ConnectionConfig, error)

// InstrumentationConfig is the setup configuration for the instrumentation of
// a database driver.
type InstrumentationConfig struct {
// DBSystem is the database system being registered.
DBSystem DBSystem
DSNParser DSNParser
}
Loading

0 comments on commit 399ffb9

Please sign in to comment.