Skip to content

Commit

Permalink
Merge pull request #151 from appoptics/add-opentracing-multitracer
Browse files Browse the repository at this point in the history
Add OpenTracing multiplexing tracer
  • Loading branch information
cce authored Aug 27, 2020
2 parents 5cf07ca + ae70a0b commit ab656d2
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
go test -v -race -covermode=atomic -coverprofile=cov.out
popd
pushd opentracing
go test -v -race -covermode=atomic -coverprofile=cov.out -coverpkg github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter,github.com/appoptics/appoptics-apm-go/v1/ao/internal/log,github.com/appoptics/appoptics-apm-go/v1/ao/internal/bson,github.com/appoptics/appoptics-apm-go/v1/ao/internal/metrics,github.com/appoptics/appoptics-apm-go/v1/ao/opentracing,github.com/appoptics/appoptics-apm-go/v1/ao,github.com/appoptics/appoptics-apm-go/v1/ao/internal/config,github.com/appoptics/appoptics-apm-go/v1/ao/internal/host
go test -v -race -covermode=atomic -coverprofile=cov.out -coverpkg github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter,github.com/appoptics/appoptics-apm-go/v1/ao/internal/log,github.com/appoptics/appoptics-apm-go/v1/ao/internal/bson,github.com/appoptics/appoptics-apm-go/v1/ao/internal/metrics,github.com/appoptics/appoptics-apm-go/v1/ao/opentracing,github.com/appoptics/appoptics-apm-go/v1/ao,github.com/appoptics/appoptics-apm-go/v1/ao/internal/config,github.com/appoptics/appoptics-apm-go/v1/ao/internal/host,github.com/appoptics/appoptics-apm-go/v1/contrib/multitracer
popd
popd
pushd contrib/aogrpc
Expand Down
4 changes: 2 additions & 2 deletions v1/ao/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ SHELL=bash

prev_branch :=$(shell git rev-parse --abbrev-ref HEAD)

coverpkg="github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter,github.com/appoptics/appoptics-apm-go/v1/ao,github.com/appoptics/appoptics-apm-go/v1/ao/opentracing"
coverpkg="github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter,github.com/appoptics/appoptics-apm-go/v1/ao/internal/log,github.com/appoptics/appoptics-apm-go/v1/ao/internal/bson,github.com/appoptics/appoptics-apm-go/v1/ao/internal/metrics,github.com/appoptics/appoptics-apm-go/v1/ao,github.com/appoptics/appoptics-apm-go/v1/ao/internal/config,github.com/appoptics/appoptics-apm-go/v1/ao/internal/host,github.com/appoptics/appoptics-apm-go/v1/contrib/multitracer"
cov_args=-covermode=atomic -coverpkg=$(coverpkg)
cov_out=-coverprofile=cov.out
cov_files=cov.out internal/reporter/cov.out opentracing/cov.out
Expand All @@ -20,4 +20,4 @@ $(cov_merge): test $(cov_files)
gocovmerge $(cov_files) > $(cov_merge)

coverhtml: $(cov_merge)
go tool cover -html=$(cov_merge)
go tool cover -html=$(cov_merge)
26 changes: 26 additions & 0 deletions v1/ao/opentracing/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,31 @@ import (
"github.com/opentracing/opentracing-go/harness"
)

// apiCheckProbe exposes methods for testing data recorded by a Tracer.
type apiCheckProbe struct{}

// SameTrace helps tests assert that this tracer's spans are from the same trace.
func (apiCheckProbe) SameTrace(first, second opentracing.Span) bool {
sp1 := first.(*spanImpl)
sp2 := second.(*spanImpl)
return sp1.context.trace.LoggableTraceID() == sp2.context.trace.LoggableTraceID()
}

// SameSpanContext helps tests assert that a span and a context are from the same trace and span.
func (apiCheckProbe) SameSpanContext(span opentracing.Span, spanCtx opentracing.SpanContext) bool {
sp := span.(*spanImpl)
sc := spanCtx.(spanContext)
var md1, md2 string
md1 = sp.context.span.MetadataString()
if sc.span == nil {
md1 = sc.remoteMD
} else {
md1 = sc.span.MetadataString()
}
md2 = sp.context.span.MetadataString()
return md1 == md2
}

func TestAPICheck(t *testing.T) {
_ = reporter.SetTestReporter(reporter.TestReporterDisableDefaultSetting(true)) // set up test reporter
harness.RunAPIChecks(t, func() (tracer opentracing.Tracer, closer func()) {
Expand All @@ -18,5 +43,6 @@ func TestAPICheck(t *testing.T) {
harness.CheckBaggageValues(true),
harness.CheckInject(true),
harness.CheckExtract(true),
harness.UseProbe(apiCheckProbe{}),
)
}
63 changes: 63 additions & 0 deletions v1/ao/opentracing/multitracer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package opentracing

import (
"testing"

"github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter"
mt "github.com/appoptics/appoptics-apm-go/v1/contrib/multitracer"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/harness"
)

// This test sets up the AO Tracer wrapped in a MultiTracer
func TestMultiTracerAPICheck(t *testing.T) {
_ = reporter.SetTestReporter(reporter.TestReporterDisableDefaultSetting(true)) // set up test reporter
multiTracer := &mt.MultiTracer{Tracers: []opentracing.Tracer{NewTracer()}}

harness.RunAPIChecks(t, func() (tracer opentracing.Tracer, closer func()) {
return multiTracer, nil
},
harness.CheckBaggageValues(false),
harness.CheckInject(true),
harness.CheckExtract(true),
harness.UseProbe(&multiApiCheckProbe{
mt: multiTracer,
probes: []harness.APICheckProbe{apiCheckProbe{}},
}),
)
}

type multiApiCheckProbe struct {
mt *mt.MultiTracer
probes []harness.APICheckProbe
}

func (m *multiApiCheckProbe) SameTrace(first, second opentracing.Span) bool {
sp1 := first.(*mt.MultiSpan)
sp2 := second.(*mt.MultiSpan)

for i := range m.mt.Tracers {
if m.probes[i] == nil {
continue
}
if !m.probes[i].SameTrace(sp1.Spans[i], sp2.Spans[i]) {
return false
}
}
return true
}

func (m *multiApiCheckProbe) SameSpanContext(span opentracing.Span, spanCtx opentracing.SpanContext) bool {
sp := span.(*mt.MultiSpan)
sc := spanCtx.(*mt.MultiSpanContext)

for i := range m.mt.Tracers {
if m.probes[i] == nil {
continue
}
if !m.probes[i].SameSpanContext(sp.Spans[i], sc.SpanContexts[i]) {
return false
}
}
return true
}
58 changes: 58 additions & 0 deletions v1/ao/opentracing/multitracer_two_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// +build basictracer
//
// Behind a build tag avoid adding a dependency on basictracer-go

package opentracing

import (
"testing"

"github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter"
mt "github.com/appoptics/appoptics-apm-go/v1/contrib/multitracer"
bt "github.com/opentracing/basictracer-go"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/harness"
)

// This test sets up AO Tracer and the OT "BasicTracer" side by side
func TestMultiTracerAOBasicTracerAPICheck(t *testing.T) {
_ = reporter.SetTestReporter(reporter.TestReporterDisableDefaultSetting(true)) // set up test reporter
multiTracer := &mt.MultiTracer{
Tracers: []opentracing.Tracer{
NewTracer(),
bt.NewWithOptions(bt.Options{
Recorder: bt.NewInMemoryRecorder(),
ShouldSample: func(traceID uint64) bool { return true }, // always sample
}),
}}

harness.RunAPIChecks(t, func() (tracer opentracing.Tracer, closer func()) {
return multiTracer, nil
},
harness.CheckBaggageValues(false),
harness.CheckInject(true),
harness.CheckExtract(true),
harness.UseProbe(&multiApiCheckProbe{
mt: multiTracer,
probes: []harness.APICheckProbe{apiCheckProbe{}, nil},
}),
)
}

// This test sets up the OT "BasicTracer" wrapped in a MultiTracer
func TestMultiTracerBasicTracerAPICheck(t *testing.T) {
_ = reporter.SetTestReporter(reporter.TestReporterDisableDefaultSetting(true)) // set up test reporter
harness.RunAPIChecks(t, func() (tracer opentracing.Tracer, closer func()) {
return &mt.MultiTracer{
Tracers: []opentracing.Tracer{
bt.NewWithOptions(bt.Options{
Recorder: bt.NewInMemoryRecorder(),
ShouldSample: func(traceID uint64) bool { return true }, // always sample
}),
}}, nil
},
harness.CheckBaggageValues(false),
harness.CheckInject(true),
harness.CheckExtract(true),
)
}
186 changes: 186 additions & 0 deletions v1/contrib/multitracer/multitracer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Package multitracer provides a way to run more than one OpenTracing tracers, by multiplexing calls across
// multiple Tracer, Span, and SpanContext implementations. The goal is to support a user sending data to
// two tracing vendors simultaneously (e.g., AppOptics and another implementation).
//
// Currently, baggage propagation is not supported, and the two tracers must use distinct HTTP header
// names, so as not to clobber each other.
package multitracer

import (
ot "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log"
)

// MultiTracer multiplexes OpenTracing API calls across multiple Tracer implementations.
type MultiTracer struct {
Tracers []ot.Tracer
}

// MultiSpan represents a list of Spans returned from multiple Tracers, managed by a MultiTracer.
type MultiSpan struct {
multiTracer *MultiTracer
Spans []ot.Span
}

// MultiSpanContext represents a list of SpanContext returned from multiple Spans, managed by a MultiTracer.
type MultiSpanContext struct {
multiTracer *MultiTracer
SpanContexts []ot.SpanContext
}

func (m *MultiTracer) StartSpan(operationName string, opts ...ot.StartSpanOption) ot.Span {
ret := &MultiSpan{
multiTracer: m,
Spans: make([]ot.Span, len(m.Tracers)),
}
for i, t := range m.Tracers {
// look for ot.StartSpanOptions with a MultiSpanContext in an ot.SpanReference
tracerSpecificOpts := []ot.StartSpanOption{}
for _, opt := range opts {
switch o := opt.(type) {
case ot.SpanReference:
// pull out tracer-specific SpanContext from MultiSpanContext
refCtx := o.ReferencedContext.(*MultiSpanContext)
tracerSpecificReference := ot.SpanReference{
Type: o.Type,
ReferencedContext: refCtx.SpanContexts[i],
}
tracerSpecificOpts = append(tracerSpecificOpts, tracerSpecificReference)
case ot.StartTime, ot.Tags, ot.Tag:
tracerSpecificOpts = append(tracerSpecificOpts, opt)
}
}
ret.Spans[i] = t.StartSpan(operationName, tracerSpecificOpts...)
}
return ret
}

// Inject propagates context using multiple tracers. Errors from individual tracers are dropped.
func (m *MultiTracer) Inject(sm ot.SpanContext, format interface{}, carrier interface{}) error {
sc, ok := sm.(*MultiSpanContext)
if !ok {
return ot.ErrInvalidSpanContext
}
errs := make([]error, len(m.Tracers))
for i, t := range m.Tracers {
errs[i] = t.Inject(sc.SpanContexts[i], format, carrier)
}

// if every tracer returned an error, then also return the first error.
var allErrors = true
for _, err := range errs {
if err == nil {
allErrors = false
break
}
}
if allErrors {
return errs[0]
}
return nil
}

// Extract reads context propagated using multiple tracers. Errors from individual tracers are dropped.
func (m *MultiTracer) Extract(format interface{}, carrier interface{}) (ot.SpanContext, error) {
ret := &MultiSpanContext{
multiTracer: m,
SpanContexts: make([]ot.SpanContext, len(m.Tracers)),
}
errs := make([]error, len(m.Tracers))
for i, t := range m.Tracers {
ret.SpanContexts[i], errs[i] = t.Extract(format, carrier)
}

// if every tracer returned an error, then also return the first error.
var allErrors = true
for _, err := range errs {
if err == nil {
allErrors = false
break
}
}
if allErrors {
return nil, errs[0]
}
return ret, nil
}

func (m *MultiSpan) Finish() {
for _, s := range m.Spans {
s.Finish()
}
}

func (m *MultiSpan) FinishWithOptions(opts ot.FinishOptions) {
for _, s := range m.Spans {
s.FinishWithOptions(opts)
}
}

func (m *MultiSpan) Context() ot.SpanContext {
ret := &MultiSpanContext{
multiTracer: m.multiTracer,
SpanContexts: make([]ot.SpanContext, len(m.Spans)),
}
for i, s := range m.Spans {
ret.SpanContexts[i] = s.Context()
}
return ret
}

func (m *MultiSpan) SetOperationName(operationName string) ot.Span {
for i, s := range m.Spans {
m.Spans[i] = s.SetOperationName(operationName)
}
return m
}

func (m *MultiSpan) SetTag(key string, value interface{}) ot.Span {
for i, s := range m.Spans {
m.Spans[i] = s.SetTag(key, value)
}
return m
}

func (m *MultiSpan) LogFields(fields ...log.Field) {
for _, s := range m.Spans {
s.LogFields(fields...)
}
}

func (m *MultiSpan) LogKV(alternatingKeyValues ...interface{}) {
for _, s := range m.Spans {
s.LogKV(alternatingKeyValues...)
}
}

// SetBaggageItem does nothing: baggage propagation is not supported.
func (m *MultiSpan) SetBaggageItem(restrictedKey, value string) ot.Span { return m }

// BaggageItem does nothing: baggage propagation is not supported.
func (m *MultiSpan) BaggageItem(restrictedKey string) string { return "" }

func (m *MultiSpan) Tracer() ot.Tracer {
return m.multiTracer
}

func (m *MultiSpan) LogEvent(event string) {
for _, s := range m.Spans {
s.LogEvent(event) //nolint
}
}

func (m *MultiSpan) LogEventWithPayload(event string, payload interface{}) {
for _, s := range m.Spans {
s.LogEventWithPayload(event, payload) //nolint
}
}

func (m *MultiSpan) Log(data ot.LogData) {
for _, s := range m.Spans {
s.Log(data) //nolint
}
}

// ForeachBaggageItem does nothing: baggage propagation is not supported.
func (m *MultiSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {}

0 comments on commit ab656d2

Please sign in to comment.