Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add miekg/dns instrumentation #155

Merged
merged 17 commits into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ updates:
directory: "/instrumentation/github.com/lib/pq/splunkpq/test"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/instrumentation/github.com/miekg/dns/splunkdns"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/instrumentation/github.com/miekg/dns/splunkdns/test"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/instrumentation/net/http/splunkhttp"
schedule:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
`github.com/signalfx/splunk-otel-go/instrumentation/github.com/confluentinc/confluent-kafka-go/kafka/splunkkafka`
instrumentation for the `github.com/confluentinc/confluent-kafka-go/kafka`
package. (#100)
- Add the
`github.com/signalfx/splunk-otel-go/instrumentation/github.com/miekg/dns/splunkdns`
instrumentation for the `github.com/miekg/dns`
package. (#155)

### Changed

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:

- [`splunkdns`](./instrumentation/github.com/miekg/dns/splunkdns)
- [`splunkgorm`](./instrumentation/github.com/jinzhu/gorm/splunkgorm)
- [`splunkhttp`](./instrumentation/net/http/splunkhttp)
- [`splunkkafka`](./instrumentation/github.com/confluentinc/confluent-kafka-go/kafka/splunkkafka)
Expand Down
10 changes: 10 additions & 0 deletions instrumentation/github.com/miekg/dns/splunkdns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Splunk instrumentation for `github.com/miekg/dns`

This instrumentation is for the
[github.com/miekg/dns](https://github.com/miekg/dns) package.

## Getting Started

This package is designed to be used as a drop-in replacement for the use of the
`github.com/miekg/dns` package. Both a server and client example can be found
[here](./example_test.go).
54 changes: 54 additions & 0 deletions instrumentation/github.com/miekg/dns/splunkdns/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 splunkdns

import (
"context"
"time"

"github.com/miekg/dns"
"go.opentelemetry.io/otel/trace"
)

// A Client wraps a DNS Client so that requests are traced.
type Client struct {
*dns.Client

cfg *config
}

// WrapClient returns a wraped DNS client.
func WrapClient(client *dns.Client, opts ...Option) *Client {
return &Client{
Client: client,
cfg: newConfig(opts...),
}
}

// Exchange calls the underlying Client.Exchange and traces the request.
func (c *Client) Exchange(m *dns.Msg, addr string) (*dns.Msg, time.Duration, error) {
return c.ExchangeContext(context.Background(), m, addr)
}

// ExchangeContext calls the underlying Client.ExchangeContext and traces the
// request.
func (c *Client) ExchangeContext(ctx context.Context, m *dns.Msg, addr string) (resp *dns.Msg, rtt time.Duration, err error) {
err = c.cfg.withSpan(ctx, m, func(ctx context.Context) error {
var sErr error
resp, rtt, sErr = c.Client.ExchangeContext(ctx, m, addr)
return sErr
}, trace.WithSpanKind(trace.SpanKindClient))
return
}
134 changes: 134 additions & 0 deletions instrumentation/github.com/miekg/dns/splunkdns/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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 splunkdns

import (
"context"

"github.com/miekg/dns"
splunkotel "github.com/signalfx/splunk-otel-go"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)

// instrumentationName is the instrumentation library identifier for a Tracer.
const instrumentationName = "github.com/signalfx/splunk-otel-go/instrumentation/github.com/miekg/dns/splunkdns"

// config contains tracing configuration options.
type config struct {
tracer trace.Tracer
defaultStartOpts []trace.SpanStartOption
}

func newConfig(options ...Option) *config {
var c config
for _, o := range options {
if o != nil {
o.apply(&c)
}
}

if c.tracer == nil {
c.tracer = otel.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
}

return &c
}

// resolveTracer 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 *config) resolveTracer(ctx context.Context) trace.Tracer {
if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
return span.TracerProvider().Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
}
// There is a possibility that the config was not created with newConfig
// (i.e. new(Client)), try to handle this situation gracefully.
if c == nil || c.tracer == nil {
return otel.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
}
return c.tracer
}

// withSpan wraps the function f with a span.
func (c *config) withSpan(ctx context.Context, m *dns.Msg, f func(context.Context) error, opts ...trace.SpanStartOption) error {
var o []trace.SpanStartOption
if c == nil || len(c.defaultStartOpts) == 0 {
o = make([]trace.SpanStartOption, len(opts))
copy(o, opts)
} else {
o = make([]trace.SpanStartOption, len(c.defaultStartOpts)+len(opts))
copy(o, c.defaultStartOpts)
copy(o[len(c.defaultStartOpts):], opts)
}

name := "DNS " + dns.OpcodeToString[m.Opcode]
ctx, span := c.resolveTracer(ctx).Start(ctx, name, o...)

err := f(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
span.End()

return err
}

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

type optionFunc func(*config)

func (o optionFunc) apply(c *config) {
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 *config) {
c.tracer = tp.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
})
}

// 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 *config) {
c.defaultStartOpts = append(
c.defaultStartOpts,
trace.WithAttributes(attr...),
)
})
}
127 changes: 127 additions & 0 deletions instrumentation/github.com/miekg/dns/splunkdns/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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 splunkdns

import (
"context"
"testing"

splunkotel "github.com/signalfx/splunk-otel-go"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)

type fnTracerProvider struct {
tracer func(string, ...trace.TracerOption) trace.Tracer
}

func (fn *fnTracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.Tracer {
return fn.tracer(name, opts...)
}

type fnTracer struct {
start func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span)
}

func (fn *fnTracer) Start(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return fn.start(ctx, name, opts...)
}

func TestConfigDefaultTracer(t *testing.T) {
c := newConfig()
expect := otel.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
assert.Equal(t, expect, c.tracer)
}

func TestWithTracer(t *testing.T) {
tracer := &fnTracer{}
// Default is to use the global TracerProvider. This will override that.
tp := &fnTracerProvider{
tracer: func(string, ...trace.TracerOption) trace.Tracer {
return tracer
},
}
c := newConfig(WithTracerProvider(tp))
assert.Same(t, tracer, c.tracer)
}

func TestEmptyConfigTracer(t *testing.T) {
// If a config is directly created, fallback to the OTel global.
c := config{}
expected := otel.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
got := c.resolveTracer(context.Background())
assert.Equal(t, expected, got)
}

func TestConfigTracerFromGlobal(t *testing.T) {
c := newConfig()
expected := otel.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
got := c.resolveTracer(context.Background())
assert.Equal(t, expected, got)
}

func TestConfigTracerFromConfig(t *testing.T) {
tp := &fnTracerProvider{
tracer: func(string, ...trace.TracerOption) trace.Tracer {
return &fnTracer{}
},
}
c := newConfig(WithTracerProvider(tp))
expected := tp.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
got := c.resolveTracer(context.Background())
assert.Equal(t, expected, got)
}

func TestConfigTracerFromContext(t *testing.T) {
sc := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{0x01},
SpanID: trace.SpanID{0x01},
})
// This context will contain a non-recording span.
ctx := trace.ContextWithSpanContext(context.Background(), sc)
// Use the global TracerProvider in the config and override with the
// passed context to the tracer method.
c := newConfig()
got := c.resolveTracer(ctx)
expected := trace.NewNoopTracerProvider().Tracer(
instrumentationName,
trace.WithInstrumentationVersion(splunkotel.Version()),
)
assert.Equal(t, expected, got)
}

func TestWithAttributes(t *testing.T) {
attr := []attribute.KeyValue{
attribute.String("key", "value"),
}
c := newConfig(WithAttributes(attr))
assert.Len(t, c.defaultStartOpts, 1)
sc := trace.NewSpanStartConfig(c.defaultStartOpts...)
assert.Equal(t, attr, sc.Attributes())
}
Loading