-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add instrumentation for the database/sql package (#88)
* 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
Showing
28 changed files
with
2,771 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.