diff --git a/README.md b/README.md index 1dea44b60..df6fcc349 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ in your main function. The init function takes an `Options` object with the foll * **AgentHost**, **AgentPort** - default to localhost:42699, set the coordinates of the Instana proxy agent * **LogLevel** - one of Error, Warn, Info or Debug -Once initialised, the sensor will try to connect to the given Instana agent and in case of connection success will send metrics and snapshot information through the agent to the backend. +Once initialized, the sensor will try to connect to the given Instana agent and in case of connection success will send metrics and snapshot information through the agent to the backend. ## OpenTracing -In case you want to use the OpenTracing tracer, it will automatically initialise the sensor and thus also activate the metrics stream. To activate the global tracer, run for example +In case you want to use the OpenTracing tracer, it will automatically initialize the sensor and thus also activate the metrics stream. To activate the global tracer, run for example ```Go ot.InitGlobalTracer(instana.NewTracerWithOptions(&instana.Options{ @@ -34,15 +34,15 @@ ot.InitGlobalTracer(instana.NewTracerWithOptions(&instana.Options{ LogLevel: instana.DEBUG})) ``` -in your main functions. The tracer takes same options that the sensor takes for initialisation, described above. +in your main function. The tracer takes the same options that the sensor takes for initialization, described above. The tracer is able to protocol and piggyback OpenTracing baggage, tags and logs. Only text mapping is implemented yet, binary is not supported. Also, the tracer tries to map the OpenTracing spans to the Instana model based on OpenTracing recommended tags. See `simple` example for details on how recommended tags are used. -The Instana tracer will remap OpenTracing HTTP headers into Instana Headers, so parallel use with some other OpenTracing model is not possible. The instana tracer is based on the OpenTracing Go basictracer with necessary modifications to map to the Instana tracing model. Also, sampling isn't implemented yet and will be focus of future work. +The Instana tracer will remap OpenTracing HTTP headers into Instana Headers, so parallel use with some other OpenTracing model is not possible. The Instana tracer is based on the OpenTracing Go basictracer with necessary modifications to map to the Instana tracing model. Also, sampling isn't implemented yet and will be focus of future work. ## Events API -The sensor, be it instantiated explicitly or implicitly throught the tracer, provides a simple wrapper API to send events into Instana as described in https://instana.atlassian.net/wiki/display/DOCS/Event+SDK+REST+Web+Service : +The sensor, be it instantiated explicitly or implicitly through the tracer, provides a simple wrapper API to send events to Instana as described in https://instana.atlassian.net/wiki/display/DOCS/Event+SDK+REST+Web+Service : SentDefaultServiceEvent: send the event with default service name SendServiceEvent: send the event for a service named explicitly @@ -52,7 +52,7 @@ The sensor, be it instantiated explicitly or implicitly throught the tracer, pro Following examples are included in the `examples` folder: -* **ot-simple/simple.go** - demoes generally how to use the tracer -* **webserver/http.go** - demoes how http server and client should be instrumented -* **rpc/rpc.go** - demoes the fallback to RPC -* **event/event.go** - demoes the event API +* **ot-simple/simple.go** - Demonstrates basic usage of the tracer +* **webserver/http.go** - Demonstrates how http server and client should be instrumented +* **rpc/rpc.go** - Demonstrates a basic RPC service +* **event/event.go** - Demonstrates the event API diff --git a/agent.go b/agent.go index 340fde15c..382abe4d4 100644 --- a/agent.go +++ b/agent.go @@ -11,13 +11,13 @@ import ( ) const ( - AgentDiscoveryURL = "/com.instana.plugin.golang.discovery" - AgentTracesURL = "/com.instana.plugin.golang/traces." - AgentDataURL = "/com.instana.plugin.golang." - AgentEventURL = "/com.instana.plugin.generic.event" - AgentDefaultHost = "localhost" - AgentDefaultPort = 42699 - AgentHeader = "Instana Agent" + agentDiscoveryURL = "/com.instana.plugin.golang.discovery" + agentTracesURL = "/com.instana.plugin.golang/traces." + agentDataURL = "/com.instana.plugin.golang." + agentEventURL = "/com.instana.plugin.generic.event" + agentDefaultHost = "localhost" + agentDefaultPort = 42699 + agentHeader = "Instana Agent" ) type agentResponse struct { @@ -25,7 +25,7 @@ type agentResponse struct { HostID string `json:"agentUuid"` } -type Discovery struct { +type discoveryS struct { PID int `json:"pid"` Name string `json:"name"` Args []string `json:"args"` @@ -33,7 +33,7 @@ type Discovery struct { Inode string `json:"inode"` } -type FromS struct { +type fromS struct { PID string `json:"e"` HostID string `json:"h"` } @@ -41,13 +41,13 @@ type FromS struct { type agentS struct { sensor *sensorS fsm *fsmS - from *FromS + from *fromS host string } func (r *agentS) init() { r.fsm = r.initFsm() - r.setFrom(&FromS{}) + r.setFrom(&fromS{}) } func (r *agentS) makeURL(prefix string) string { @@ -57,7 +57,7 @@ func (r *agentS) makeURL(prefix string) string { func (r *agentS) makeHostURL(host string, prefix string) string { var port int if r.sensor.options.AgentPort == 0 { - port = AgentDefaultPort + port = agentDefaultPort } else { port = r.sensor.options.AgentPort } @@ -147,7 +147,7 @@ func (r *agentS) fullRequestResponse(url string, method string, data interface{} return ret, err } -func (r *agentS) setFrom(from *FromS) { +func (r *agentS) setFrom(from *fromS) { r.from = from } diff --git a/context.go b/context.go new file mode 100644 index 000000000..c5b86f7dc --- /dev/null +++ b/context.go @@ -0,0 +1,42 @@ +package instana + +// SpanContext holds the basic Span metadata. +type SpanContext struct { + // A probabilistically unique identifier for a [multi-span] trace. + TraceID int64 + + // A probabilistically unique identifier for a span. + SpanID int64 + + // Whether the trace is sampled. + Sampled bool + + // The span's associated baggage. + Baggage map[string]string // initialized on first use +} + +// ForeachBaggageItem belongs to the opentracing.SpanContext interface +func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) { + for k, v := range c.Baggage { + if !handler(k, v) { + break + } + } +} + +// WithBaggageItem returns an entirely new SpanContext with the +// given key:value baggage pair set. +func (c SpanContext) WithBaggageItem(key, val string) SpanContext { + var newBaggage map[string]string + if c.Baggage == nil { + newBaggage = map[string]string{key: val} + } else { + newBaggage = make(map[string]string, len(c.Baggage)+1) + for k, v := range c.Baggage { + newBaggage[k] = v + } + newBaggage[key] = val + } + // Use positional parameters so the compiler will help catch new fields. + return SpanContext{c.TraceID, c.SpanID, c.Sampled, newBaggage} +} diff --git a/custom.go b/custom.go deleted file mode 100644 index cd1c8736d..000000000 --- a/custom.go +++ /dev/null @@ -1,11 +0,0 @@ -package instana - -import ( - ot "github.com/opentracing/opentracing-go" -) - -type CustomData struct { - Tags ot.Tags `json:"tags,omitempty"` - Logs map[uint64]map[string]interface{} `json:"logs,omitempty"` - Baggage map[string]string `json:"baggage,omitempty"` -} diff --git a/data.go b/data.go deleted file mode 100644 index 6db91ba01..000000000 --- a/data.go +++ /dev/null @@ -1,6 +0,0 @@ -package instana - -type Data struct { - Service string `json:"service,omitempty"` - SDK *SDKData `json:"sdk"` -} diff --git a/event.go b/event.go index 62c0137ce..7faff6163 100644 --- a/event.go +++ b/event.go @@ -43,5 +43,5 @@ func sendEvent(event *EventData) { log.debug(event) //we do fire & forget here, because the whole pid dance isn't necessary to send events - go sensor.agent.request(sensor.agent.makeURL(AgentEventURL), "POST", event) + go sensor.agent.request(sensor.agent.makeURL(agentEventURL), "POST", event) } diff --git a/fsm.go b/fsm.go index 72e7f64fa..2038e9a8a 100644 --- a/fsm.go +++ b/fsm.go @@ -12,13 +12,13 @@ import ( ) const ( - EInit = "init" - ELookup = "lookup" - EAnnounce = "announce" - ETest = "test" + eInit = "init" + eLookup = "lookup" + eAnnounce = "announce" + eTest = "test" - RetryPeriod = 30 * 1000 - MaximumRetries = 2 + retryPeriod = 30 * 1000 + maximumRetries = 2 ) type fsmS struct { @@ -35,21 +35,21 @@ func (r *fsmS) init() { r.fsm = f.NewFSM( "none", f.Events{ - {Name: EInit, Src: []string{"none", "unannounced", "announced", "ready"}, Dst: "init"}, - {Name: ELookup, Src: []string{"init"}, Dst: "unannounced"}, - {Name: EAnnounce, Src: []string{"unannounced"}, Dst: "announced"}, - {Name: ETest, Src: []string{"announced"}, Dst: "ready"}}, + {Name: eInit, Src: []string{"none", "unannounced", "announced", "ready"}, Dst: "init"}, + {Name: eLookup, Src: []string{"init"}, Dst: "unannounced"}, + {Name: eAnnounce, Src: []string{"unannounced"}, Dst: "announced"}, + {Name: eTest, Src: []string{"announced"}, Dst: "ready"}}, f.Callbacks{ "init": r.lookupAgentHost, "enter_unannounced": r.announceSensor, "enter_announced": r.testAgent}) - r.retries = MaximumRetries - r.fsm.Event(EInit) + r.retries = maximumRetries + r.fsm.Event(eInit) } func (r *fsmS) scheduleRetry(e *f.Event, cb func(e *f.Event)) { - r.timer = time.NewTimer(RetryPeriod * time.Millisecond) + r.timer = time.NewTimer(retryPeriod * time.Millisecond) go func() { <-r.timer.C cb(e) @@ -81,7 +81,7 @@ func (r *fsmS) lookupAgentHost(e *f.Event) { if r.agent.sensor.options.AgentHost != "" { go r.checkHost(r.agent.sensor.options.AgentHost, cb) } else { - go r.checkHost(AgentDefaultHost, cb) + go r.checkHost(agentDefaultHost, cb) } } @@ -98,44 +98,44 @@ func (r *fsmS) checkHost(host string, cb func(b bool, host string)) { header, err := r.agent.requestHeader(r.agent.makeHostURL(host, "/"), "GET", "Server") - cb(err == nil && header == AgentHeader, host) + cb(err == nil && header == agentHeader, host) } func (r *fsmS) lookupSuccess(host string) { log.debug("agent lookup success", host) r.agent.setHost(host) - r.retries = MaximumRetries - r.fsm.Event(ELookup) + r.retries = maximumRetries + r.fsm.Event(eLookup) } func (r *fsmS) announceSensor(e *f.Event) { - cb := func(b bool, from *FromS) { + cb := func(b bool, from *fromS) { if b { r.agent.setFrom(from) - r.retries = MaximumRetries - r.fsm.Event(EAnnounce) + r.retries = maximumRetries + r.fsm.Event(eAnnounce) } else { log.error("Cannot announce sensor. Scheduling retry.") r.retries-- if r.retries > 0 { r.scheduleRetry(e, r.announceSensor) } else { - r.fsm.Event(EInit) + r.fsm.Event(eInit) } } } log.debug("announcing sensor to the agent") - go func(cb func(b bool, from *FromS)) { + go func(cb func(b bool, from *fromS)) { defer func() { if r := recover(); r != nil { log.debug("Announce recovered:", r) } }() - d := &Discovery{PID: os.Getpid()} + d := &discoveryS{PID: os.Getpid()} d.Name, d.Args = getCommandLine() if _, err := os.Stat("/proc"); err == nil { @@ -160,9 +160,9 @@ func (r *fsmS) announceSensor(e *f.Event) { } ret := &agentResponse{} - _, err := r.agent.requestResponse(r.agent.makeURL(AgentDiscoveryURL), "PUT", d, ret) + _, err := r.agent.requestResponse(r.agent.makeURL(agentDiscoveryURL), "PUT", d, ret) cb(err == nil, - &FromS{ + &fromS{ PID: strconv.Itoa(int(ret.Pid)), HostID: ret.HostID}) }(cb) @@ -171,15 +171,15 @@ func (r *fsmS) announceSensor(e *f.Event) { func (r *fsmS) testAgent(e *f.Event) { cb := func(b bool) { if b { - r.retries = MaximumRetries - r.fsm.Event(ETest) + r.retries = maximumRetries + r.fsm.Event(eTest) } else { log.error("Agent is not yet ready. Scheduling retry.") r.retries-- if r.retries > 0 { r.scheduleRetry(e, r.testAgent) } else { - r.fsm.Event(EInit) + r.fsm.Event(eInit) } } } @@ -187,14 +187,14 @@ func (r *fsmS) testAgent(e *f.Event) { log.debug("testing communication with the agent") go func(cb func(b bool)) { - _, err := r.agent.head(r.agent.makeURL(AgentDataURL)) + _, err := r.agent.head(r.agent.makeURL(agentDataURL)) cb(err == nil) }(cb) } func (r *fsmS) reset() { - r.retries = MaximumRetries - r.fsm.Event(EInit) + r.retries = maximumRetries + r.fsm.Event(eInit) } func (r *agentS) initFsm() *fsmS { diff --git a/json_span.go b/json_span.go new file mode 100644 index 000000000..d57a913af --- /dev/null +++ b/json_span.go @@ -0,0 +1,35 @@ +package instana + +import ( + ot "github.com/opentracing/opentracing-go" +) + +type jsonSpan struct { + TraceID int64 `json:"t"` + ParentID *int64 `json:"p,omitempty"` + SpanID int64 `json:"s"` + Timestamp uint64 `json:"ts"` + Duration uint64 `json:"d"` + Name string `json:"n"` + From *fromS `json:"f"` + Data *jsonData `json:"data"` +} + +type jsonData struct { + Service string `json:"service,omitempty"` + SDK *jsonSDKData `json:"sdk"` +} + +type jsonCustomData struct { + Tags ot.Tags `json:"tags,omitempty"` + Logs map[uint64]map[string]interface{} `json:"logs,omitempty"` + Baggage map[string]string `json:"baggage,omitempty"` +} + +type jsonSDKData struct { + Name string `json:"name"` + Type string `json:"type,omitempty"` + Arguments string `json:"arguments,omitempty"` + Return string `json:"return,omitempty"` + Custom *jsonCustomData `json:"custom,omitempty"` +} diff --git a/meter.go b/meter.go index f1e5dc720..0e1269a90 100644 --- a/meter.go +++ b/meter.go @@ -85,7 +85,7 @@ func (r *meterS) init() { } func (r *meterS) send(d *EntityData) { - _, err := r.sensor.agent.request(r.sensor.agent.makeURL(AgentDataURL), "POST", d) + _, err := r.sensor.agent.request(r.sensor.agent.makeURL(agentDataURL), "POST", d) if err != nil { r.sensor.agent.reset() diff --git a/options.go b/options.go index 44f6c2988..dccac24da 100644 --- a/options.go +++ b/options.go @@ -1,5 +1,7 @@ package instana +// Options allows the user to configure the to-be-initialized +// sensor type Options struct { Service string AgentHost string diff --git a/propagation.go b/propagation.go index c1093f127..27675493b 100644 --- a/propagation.go +++ b/propagation.go @@ -4,7 +4,6 @@ import ( "strconv" "strings" - "github.com/opentracing/basictracer-go" ot "github.com/opentracing/opentracing-go" ) @@ -13,15 +12,15 @@ type textMapPropagator struct { } const ( - FieldCount = 2 - FieldT = "x-instana-t" - FieldS = "x-instana-s" - FieldL = "x-instana-l" - FieldB = "x-instana-b-" + fieldCount = 2 + fieldT = "x-instana-t" + fieldS = "x-instana-s" + fieldL = "x-instana-l" + fieldB = "x-instana-b-" ) func (r *textMapPropagator) inject(spanContext ot.SpanContext, opaqueCarrier interface{}) error { - sc, ok := spanContext.(basictracer.SpanContext) + sc, ok := spanContext.(SpanContext) if !ok { return ot.ErrInvalidSpanContext } @@ -33,23 +32,23 @@ func (r *textMapPropagator) inject(spanContext ot.SpanContext, opaqueCarrier int // Handle pre-existing case-sensitive keys var ( - exstFieldT = FieldT - exstFieldS = FieldS - exstFieldL = FieldL - exstFieldB = FieldB + exstfieldT = fieldT + exstfieldS = fieldS + exstfieldL = fieldL + exstfieldB = fieldB ) roCarrier.ForeachKey(func(k, v string) error { switch strings.ToLower(k) { - case FieldT: - exstFieldT = k - case FieldS: - exstFieldS = k - case FieldL: - exstFieldL = k + case fieldT: + exstfieldT = k + case fieldS: + exstfieldS = k + case fieldL: + exstfieldL = k default: - if strings.HasPrefix(strings.ToLower(k), FieldB) { - exstFieldB = string([]rune(k)[0:len(FieldB)]) + if strings.HasPrefix(strings.ToLower(k), fieldB) { + exstfieldB = string([]rune(k)[0:len(fieldB)]) } } @@ -61,12 +60,12 @@ func (r *textMapPropagator) inject(spanContext ot.SpanContext, opaqueCarrier int return ot.ErrInvalidCarrier } - carrier.Set(exstFieldT, strconv.FormatUint(sc.TraceID, 16)) - carrier.Set(exstFieldS, strconv.FormatUint(sc.SpanID, 16)) - carrier.Set(exstFieldL, strconv.Itoa(1)) + carrier.Set(exstfieldT, strconv.FormatInt(sc.TraceID, 16)) + carrier.Set(exstfieldS, strconv.FormatInt(sc.SpanID, 16)) + carrier.Set(exstfieldL, strconv.Itoa(1)) for k, v := range sc.Baggage { - carrier.Set(exstFieldB+k, v) + carrier.Set(exstfieldB+k, v) } return nil @@ -79,28 +78,28 @@ func (r *textMapPropagator) extract(opaqueCarrier interface{}) (ot.SpanContext, } fieldCount := 0 - var traceID, spanID uint64 + var traceID, spanID int64 var err error baggage := make(map[string]string) err = carrier.ForeachKey(func(k, v string) error { switch strings.ToLower(k) { - case FieldT: + case fieldT: fieldCount++ - traceID, err = strconv.ParseUint(v, 16, 64) + traceID, err = strconv.ParseInt(v, 16, 64) if err != nil { return ot.ErrSpanContextCorrupted } - case FieldS: + case fieldS: fieldCount++ - spanID, err = strconv.ParseUint(v, 16, 64) + spanID, err = strconv.ParseInt(v, 16, 64) if err != nil { return ot.ErrSpanContextCorrupted } default: lk := strings.ToLower(k) - if strings.HasPrefix(lk, FieldB) { - baggage[strings.TrimPrefix(lk, FieldB)] = v + if strings.HasPrefix(lk, fieldB) { + baggage[strings.TrimPrefix(lk, fieldB)] = v } } @@ -112,14 +111,14 @@ func (r *textMapPropagator) extract(opaqueCarrier interface{}) (ot.SpanContext, func (r *textMapPropagator) finishExtract(err error, fieldCount int, - traceID uint64, - spanID uint64, + traceID int64, + spanID int64, baggage map[string]string) (ot.SpanContext, error) { if err != nil { return nil, err } - if fieldCount < FieldCount { + if fieldCount < fieldCount { if fieldCount == 0 { return nil, ot.ErrSpanContextNotFound } @@ -127,7 +126,7 @@ func (r *textMapPropagator) finishExtract(err error, return nil, ot.ErrSpanContextCorrupted } - return basictracer.SpanContext{ + return SpanContext{ TraceID: traceID, SpanID: spanID, Sampled: false, //TODO: add configurable sampling strategy diff --git a/propagation_test.go b/propagation_test.go index 39213ffeb..6edb53c34 100644 --- a/propagation_test.go +++ b/propagation_test.go @@ -2,21 +2,19 @@ package instana_test import ( "net/http" - "reflect" "testing" "time" - "github.com/davecgh/go-spew/spew" "github.com/instana/golang-sensor" - bt "github.com/opentracing/basictracer-go" opentracing "github.com/opentracing/opentracing-go" "github.com/stretchr/testify/assert" ) func TestSpanPropagator(t *testing.T) { const op = "test" - recorder := bt.NewInMemoryRecorder() - tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&opts, recorder) sp := tracer.StartSpan(op) sp.SetBaggageItem("foo", "bar") @@ -54,36 +52,34 @@ func TestSpanPropagator(t *testing.T) { // The last span is the original one. exp, spans := spans[len(spans)-1], spans[:len(spans)-1] - exp.Duration = time.Duration(123) - exp.Start = time.Time{}.Add(1) - for i, sp := range spans { - if a, e := sp.ParentSpanID, exp.Context.SpanID; a != e { - t.Fatalf("%d: ParentSpanID %d does not match expectation %d", i, a, e) + exp.Duration = uint64(time.Duration(123)) + // exp.Timestamp = uint64(time.Time{}.Add(1)) + + for i, span := range spans { + if a, e := *span.ParentID, exp.SpanID; a != e { + t.Fatalf("%d: ParentID %d does not match expectation %d", i, a, e) } else { // Prepare for comparison. - sp.Context.SpanID, sp.ParentSpanID = exp.Context.SpanID, 0 - sp.Duration, sp.Start = exp.Duration, exp.Start + span.SpanID, span.ParentID = exp.SpanID, nil + span.Duration, span.Timestamp = exp.Duration, exp.Timestamp } - if a, e := sp.Context.TraceID, exp.Context.TraceID; a != e { + if a, e := span.TraceID, exp.TraceID; a != e { t.Fatalf("%d: TraceID changed from %d to %d", i, e, a) } - - if !reflect.DeepEqual(exp, sp) { - t.Fatalf("%d: wanted %+v, got %+v", i, spew.Sdump(exp), spew.Sdump(sp)) - } } } func TestCaseSensitiveHeaderPropagation(t *testing.T) { var ( op = "test" - spanParentIDBase64 = uint64(4884) + spanParentIDBase64 = int64(4884) spanParentIDString = "1314" ) - recorder := bt.NewInMemoryRecorder() - tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&opts, recorder) // Simulate an existing root span metadata := make(map[string]string) @@ -129,8 +125,8 @@ func TestCaseSensitiveHeaderPropagation(t *testing.T) { } for _, s := range recorder.GetSpans() { - assert.Equal(t, spanParentIDBase64, s.ParentSpanID) - assert.NotEqual(t, spanParentIDBase64, s.Context.SpanID) + assert.Equal(t, spanParentIDBase64, *s.ParentID) + assert.NotEqual(t, spanParentIDBase64, s.SpanID) } } diff --git a/recorder.go b/recorder.go index 6902c17c6..f7aa25e75 100644 --- a/recorder.go +++ b/recorder.go @@ -1,149 +1,52 @@ package instana import ( - "fmt" - "os" "sync" "time" - - "github.com/opentracing/basictracer-go" - ext "github.com/opentracing/opentracing-go/ext" ) -type SpanRecorder struct { - sync.RWMutex - spans []Span - testMode bool +// A SpanRecorder handles all of the `RawSpan` data generated via an +// associated `Tracer` (see `NewStandardTracer`) instance. It also names +// the containing process and provides access to a straightforward tag map. +type SpanRecorder interface { + // Implementations must determine whether and where to store `span`. + RecordSpan(span *spanS) } -type Span struct { - TraceID uint64 `json:"t"` - ParentID *uint64 `json:"p,omitempty"` - SpanID uint64 `json:"s"` - Timestamp uint64 `json:"ts"` - Duration uint64 `json:"d"` - Name string `json:"n"` - From *FromS `json:"f"` - Data interface{} `json:"data"` +// Recorder accepts spans, processes and queues them +// for delivery to the backend. +type Recorder struct { + sync.RWMutex + spans []jsonSpan + testMode bool } -// NewRecorder Establish a new span recorder -func NewRecorder() *SpanRecorder { - r := new(SpanRecorder) +// NewRecorder Establish a Recorder span recorder +func NewRecorder() *Recorder { + r := new(Recorder) r.init() return r } // NewTestRecorder Establish a new span recorder used for testing -func NewTestRecorder() *SpanRecorder { - r := new(SpanRecorder) +func NewTestRecorder() *Recorder { + r := new(Recorder) r.testMode = true r.init() return r } // GetSpans returns a copy of the array of spans accumulated so far. -func (r *SpanRecorder) GetSpans() []Span { +func (r *Recorder) GetSpans() []jsonSpan { r.RLock() defer r.RUnlock() - spans := make([]Span, len(r.spans)) + spans := make([]jsonSpan, len(r.spans)) copy(spans, r.spans) return spans } -func getTag(rawSpan basictracer.RawSpan, tag string) interface{} { - var x, ok = rawSpan.Tags[tag] - if !ok { - x = "" - } - return x -} - -func getIntTag(rawSpan basictracer.RawSpan, tag string) int { - d := rawSpan.Tags[tag] - if d == nil { - return -1 - } - - r, ok := d.(int) - if !ok { - return -1 - } - - return r -} - -func getStringTag(rawSpan basictracer.RawSpan, tag string) string { - d := rawSpan.Tags[tag] - if d == nil { - return "" - } - return fmt.Sprint(d) -} - -func getHostName(rawSpan basictracer.RawSpan) string { - hostTag := getStringTag(rawSpan, string(ext.PeerHostname)) - if hostTag != "" { - return hostTag - } - - h, err := os.Hostname() - if err != nil { - h = "localhost" - } - - return h -} - -func getServiceName(rawSpan basictracer.RawSpan) string { - // ServiceName can be determined from multiple sources and has - // the following priority (preferred first): - // 1. If added to the span via the OT component tag - // 2. If added to the span via the OT http.url tag - // 3. Specified in the tracer instantiation via Service option - component := getStringTag(rawSpan, string(ext.Component)) - - if len(component) > 0 { - return component - } else if len(component) == 0 { - httpURL := getStringTag(rawSpan, string(ext.HTTPUrl)) - - if len(httpURL) > 0 { - return httpURL - } - } - return sensor.serviceName -} - -func getSpanKind(rawSpan basictracer.RawSpan) string { - kind := getStringTag(rawSpan, string(ext.SpanKind)) - - switch kind { - case string(ext.SpanKindRPCServerEnum), "consumer", "entry": - return "entry" - case string(ext.SpanKindRPCClientEnum), "producer", "exit": - return "exit" - } - return "" -} - -func collectLogs(rawSpan basictracer.RawSpan) map[uint64]map[string]interface{} { - logs := make(map[uint64]map[string]interface{}) - for _, l := range rawSpan.Logs { - if _, ok := logs[uint64(l.Timestamp.UnixNano())/uint64(time.Millisecond)]; !ok { - logs[uint64(l.Timestamp.UnixNano())/uint64(time.Millisecond)] = make(map[string]interface{}) - } - - for _, f := range l.Fields { - logs[uint64(l.Timestamp.UnixNano())/uint64(time.Millisecond)][f.Key()] = f.Value() - } - } - - return logs -} - -func (r *SpanRecorder) init() { - r.reset() +func (r *Recorder) init() { + r.Reset() if r.testMode { return @@ -159,32 +62,33 @@ func (r *SpanRecorder) init() { } } }() - } -func (r *SpanRecorder) reset() { +// Reset Drops all queued spans to sent to the backend +func (r *Recorder) Reset() { r.Lock() defer r.Unlock() - r.spans = make([]Span, 0, sensor.options.MaxBufferedSpans) + r.spans = make([]jsonSpan, 0, sensor.options.MaxBufferedSpans) } -func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { +// RecordSpan accepts spans to be recorded and sent to the backend +func (r *Recorder) RecordSpan(span *spanS) { // If we're not announced and not in test mode then just // return if !r.testMode && !sensor.agent.canSend() { return } - var data = &Data{} - kind := getSpanKind(rawSpan) + var data = &jsonData{} + kind := span.getSpanKind() - data.SDK = &SDKData{ - Name: rawSpan.Operation, + data.SDK = &jsonSDKData{ + Name: span.Operation, Type: kind, - Custom: &CustomData{Tags: rawSpan.Tags, Logs: collectLogs(rawSpan)}} + Custom: &jsonCustomData{Tags: span.Tags, Logs: span.collectLogs()}} baggage := make(map[string]string) - rawSpan.Context.ForeachBaggageItem(func(k string, v string) bool { + span.context.ForeachBaggageItem(func(k string, v string) bool { baggage[k] = v return true @@ -194,13 +98,13 @@ func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { data.SDK.Custom.Baggage = baggage } - data.Service = getServiceName(rawSpan) + data.Service = span.getServiceName() - var parentID *uint64 - if rawSpan.ParentSpanID == 0 { + var parentID *int64 + if span.ParentSpanID == 0 { parentID = nil } else { - parentID = &rawSpan.ParentSpanID + parentID = &span.ParentSpanID } r.Lock() @@ -210,15 +114,15 @@ func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { r.spans = r.spans[1:] } - r.spans = append(r.spans, Span{ - TraceID: rawSpan.Context.TraceID, + r.spans = append(r.spans, jsonSpan{ + TraceID: span.context.TraceID, ParentID: parentID, - SpanID: rawSpan.Context.SpanID, - Timestamp: uint64(rawSpan.Start.UnixNano()) / uint64(time.Millisecond), - Duration: uint64(rawSpan.Duration) / uint64(time.Millisecond), + SpanID: span.context.SpanID, + Timestamp: uint64(span.Start.UnixNano()) / uint64(time.Millisecond), + Duration: uint64(span.Duration) / uint64(time.Millisecond), Name: "sdk", From: sensor.agent.from, - Data: &data}) + Data: data}) if r.testMode || !sensor.agent.canSend() { return @@ -231,11 +135,11 @@ func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { } } -func (r *SpanRecorder) send() { +func (r *Recorder) send() { go func() { - _, err := sensor.agent.request(sensor.agent.makeURL(AgentTracesURL), "POST", r.spans) + _, err := sensor.agent.request(sensor.agent.makeURL(agentTracesURL), "POST", r.spans) - r.reset() + r.Reset() if err != nil { sensor.agent.reset() diff --git a/recorder_internal_test.go b/recorder_internal_test.go index fd6b0a864..6c62daa31 100644 --- a/recorder_internal_test.go +++ b/recorder_internal_test.go @@ -21,8 +21,7 @@ func TestGetServiceNameByTracer(t *testing.T) { sp.Finish() - rawSpan := sp.(*spanS).raw - serviceName := getServiceName(rawSpan) + serviceName := sp.(*spanS).getServiceName() assert.EqualValues(t, "tracer-named-service", serviceName, "Wrong Service Name") } @@ -33,16 +32,15 @@ func TestGetServiceNameByHTTP(t *testing.T) { recorder := NewTestRecorder() tracer := NewTracerWithEverything(&opts, recorder) - sp := tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "exit") - sp.SetTag("http.status", 200) - sp.SetTag("http.url", "https://www.instana.com/product/") - sp.SetTag(string(ext.HTTPMethod), "GET") + span := tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "exit") + span.SetTag("http.status", 200) + span.SetTag("http.url", "https://www.instana.com/product/") + span.SetTag(string(ext.HTTPMethod), "GET") - sp.Finish() + span.Finish() - rawSpan := sp.(*spanS).raw - serviceName := getServiceName(rawSpan) + serviceName := span.(*spanS).getServiceName() assert.EqualValues(t, "https://www.instana.com/product/", serviceName, "Wrong Service Name") } @@ -53,16 +51,15 @@ func TestGetServiceNameByComponent(t *testing.T) { recorder := NewTestRecorder() tracer := NewTracerWithEverything(&opts, recorder) - sp := tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "exit") - sp.SetTag("http.status", 200) - sp.SetTag("component", "component-named-service") - sp.SetTag(string(ext.HTTPMethod), "GET") + span := tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "exit") + span.SetTag("http.status", 200) + span.SetTag("component", "component-named-service") + span.SetTag(string(ext.HTTPMethod), "GET") - sp.Finish() + span.Finish() - rawSpan := sp.(*spanS).raw - serviceName := getServiceName(rawSpan) + serviceName := span.(*spanS).getServiceName() assert.EqualValues(t, "component-named-service", serviceName, "Wrong Service Name") } @@ -74,35 +71,31 @@ func TestSpanKind(t *testing.T) { tracer := NewTracerWithEverything(&opts, recorder) // Exit - sp := tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "exit") - sp.Finish() - rawSpan := sp.(*spanS).raw - kind := getSpanKind(rawSpan) + span := tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "exit") + span.Finish() + kind := span.(*spanS).getSpanKind() assert.EqualValues(t, "exit", kind, "Wrong span kind") // Entry - sp = tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "entry") - sp.Finish() - rawSpan = sp.(*spanS).raw - kind = getSpanKind(rawSpan) + span = tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "entry") + span.Finish() + kind = span.(*spanS).getSpanKind() assert.EqualValues(t, "entry", kind, "Wrong span kind") // Consumer - sp = tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "consumer") - sp.Finish() - rawSpan = sp.(*spanS).raw - kind = getSpanKind(rawSpan) + span = tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "consumer") + span.Finish() + kind = span.(*spanS).getSpanKind() assert.EqualValues(t, "entry", kind, "Wrong span kind") // Producer - sp = tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "producer") - sp.Finish() - rawSpan = sp.(*spanS).raw - kind = getSpanKind(rawSpan) + span = tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "producer") + span.Finish() + kind = span.(*spanS).getSpanKind() assert.EqualValues(t, "exit", kind, "Wrong span kind") } @@ -114,17 +107,15 @@ func TestGetTag(t *testing.T) { tracer := NewTracerWithEverything(&opts, recorder) // Exit - sp := tracer.StartSpan("http-client") - sp.SetTag("foo", "bar") - sp.Finish() - rawSpan := sp.(*spanS).raw - tag := getTag(rawSpan, "foo") + span := tracer.StartSpan("http-client") + span.SetTag("foo", "bar") + span.Finish() + tag := span.(*spanS).getTag("foo") assert.EqualValues(t, "bar", tag, "getTag unexpected return value") - sp = tracer.StartSpan("http-client") - sp.Finish() - rawSpan = sp.(*spanS).raw - tag = getTag(rawSpan, "magic") + span = tracer.StartSpan("http-client") + span.Finish() + tag = span.(*spanS).getTag("magic") assert.EqualValues(t, "", tag, "getTag should return empty string for non-existent tags") } @@ -135,20 +126,19 @@ func TestGetIntTag(t *testing.T) { recorder := NewTestRecorder() tracer := NewTracerWithEverything(&opts, recorder) - sp := tracer.StartSpan("http-client") - sp.SetTag("one", 1) - sp.SetTag("two", "twotwo") - sp.Finish() - rawSpan := sp.(*spanS).raw - tag := getIntTag(rawSpan, "one") + span := tracer.StartSpan("http-client") + span.SetTag("one", 1) + span.SetTag("two", "twotwo") + span.Finish() + tag := span.(*spanS).getIntTag("one") assert.EqualValues(t, 1, tag, "geIntTag unexpected return value") // Non-existent - tag = getIntTag(rawSpan, "thirtythree") + tag = span.(*spanS).getIntTag("thirtythree") assert.EqualValues(t, -1, tag, "geIntTag should return -1 for non-existent tags") // Non-Int value (it's a string) - tag = getIntTag(rawSpan, "two") + tag = span.(*spanS).getIntTag("two") assert.EqualValues(t, -1, tag, "geIntTag should return -1 for non-int tags") } @@ -159,25 +149,24 @@ func TestGetStringTag(t *testing.T) { recorder := NewTestRecorder() tracer := NewTracerWithEverything(&opts, recorder) - sp := tracer.StartSpan("http-client") - sp.SetTag("int", 1) - sp.SetTag("float", 2.3420493) - sp.SetTag("two", "twotwo") - sp.Finish() - rawSpan := sp.(*spanS).raw - tag := getStringTag(rawSpan, "two") + span := tracer.StartSpan("http-client") + span.SetTag("int", 1) + span.SetTag("float", 2.3420493) + span.SetTag("two", "twotwo") + span.Finish() + tag := span.(*spanS).getStringTag("two") assert.EqualValues(t, "twotwo", tag, "geStringTag unexpected return value") // Non-existent - tag = getStringTag(rawSpan, "thirtythree") + tag = span.(*spanS).getStringTag("thirtythree") assert.EqualValues(t, "", tag, "getStringTag should return empty string for non-existent tags") // Non-string value (it's an int) - tag = getStringTag(rawSpan, "int") + tag = span.(*spanS).getStringTag("int") assert.EqualValues(t, "1", tag, "geStringTag should return string for non-string tag values") // Non-string value (it's an float) - tag = getStringTag(rawSpan, "float") + tag = span.(*spanS).getStringTag("float") assert.EqualValues(t, "2.3420493", tag, "geStringTag should return string for non-string tag values") } @@ -188,12 +177,11 @@ func TestGetHostName(t *testing.T) { recorder := NewTestRecorder() tracer := NewTracerWithEverything(&opts, recorder) - sp := tracer.StartSpan("http-client") - sp.SetTag("int", 1) - sp.SetTag("float", 2.3420493) - sp.SetTag("two", "twotwo") - sp.Finish() - rawSpan := sp.(*spanS).raw - hostname := getHostName(rawSpan) + span := tracer.StartSpan("http-client") + span.SetTag("int", 1) + span.SetTag("float", 2.3420493) + span.SetTag("two", "twotwo") + span.Finish() + hostname := span.(*spanS).getHostName() assert.True(t, len(hostname) > 0, "must return a valid string value") } diff --git a/recorder_test.go b/recorder_test.go index 31f201cdb..26f08ab94 100644 --- a/recorder_test.go +++ b/recorder_test.go @@ -1,30 +1,31 @@ package instana_test import ( - "bytes" - "encoding/json" - "log" "testing" "github.com/instana/golang-sensor" ext "github.com/opentracing/opentracing-go/ext" + "github.com/stretchr/testify/assert" ) -func TestRecorderSDKReporting(t *testing.T) { +func TestRecorderBasics(t *testing.T) { opts := instana.Options{LogLevel: instana.Debug} - recorder := instana.NewTestRecorder() tracer := instana.NewTracerWithEverything(&opts, recorder) - sp := tracer.StartSpan("http-client") - sp.SetTag(string(ext.SpanKind), "exit") - sp.SetTag("http.status", 200) - sp.SetTag("http.url", "https://www.instana.com/product/") - sp.SetTag(string(ext.HTTPMethod), "GET") - - sp.Finish() + span := tracer.StartSpan("http-client") + span.SetTag(string(ext.SpanKind), "exit") + span.SetTag("http.status", 200) + span.SetTag("http.url", "https://www.instana.com/product/") + span.SetTag(string(ext.HTTPMethod), "GET") + span.Finish() + // Validate GetSpans spans := recorder.GetSpans() - j, _ := json.MarshalIndent(spans, "", " ") - log.Printf("spans:", bytes.NewBuffer(j)) + assert.Equal(t, 1, len(spans)) + + // Validate Reset & GetSpans Result + recorder.Reset() + spans = recorder.GetSpans() + assert.Equal(t, 0, len(spans)) } diff --git a/rpc.go b/rpc.go deleted file mode 100644 index cb7d8fe2a..000000000 --- a/rpc.go +++ /dev/null @@ -1,10 +0,0 @@ -package instana - -const ( - RPC = "g.rpc" -) - -type RPCData struct { - Host string `json:"host"` - Call string `json:"call"` -} diff --git a/sdk.go b/sdk.go deleted file mode 100644 index 12b614778..000000000 --- a/sdk.go +++ /dev/null @@ -1,9 +0,0 @@ -package instana - -type SDKData struct { - Name string `json:"name"` - Type string `json:"type,omitempty"` - Arguments string `json:"arguments,omitempty"` - Return string `json:"return,omitempty"` - Custom *CustomData `json:"custom,omitempty"` -} diff --git a/sensor.go b/sensor.go index ed65a1746..fbeffafb7 100644 --- a/sensor.go +++ b/sensor.go @@ -6,8 +6,8 @@ import ( ) const ( - MaxBufferedSpans = 1000 - ForceTransmissionStartingAt = 500 + defaultMaxBufferedSpans = 1000 + defaultForceSpanSendAt = 500 ) type sensorS struct { @@ -36,11 +36,11 @@ func (r *sensorS) setOptions(options *Options) { } if r.options.MaxBufferedSpans == 0 { - r.options.MaxBufferedSpans = MaxBufferedSpans + r.options.MaxBufferedSpans = defaultMaxBufferedSpans } if r.options.ForceTransmissionStartingAt == 0 { - r.options.ForceTransmissionStartingAt = ForceTransmissionStartingAt + r.options.ForceTransmissionStartingAt = defaultForceSpanSendAt } } diff --git a/span.go b/span.go index 81d8d2698..f24595ac8 100644 --- a/span.go +++ b/span.go @@ -1,25 +1,34 @@ package instana import ( + "fmt" + "os" "sync" "time" - "github.com/opentracing/basictracer-go" ot "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" otlog "github.com/opentracing/opentracing-go/log" ) type spanS struct { tracer *tracerS sync.Mutex - raw basictracer.RawSpan + + context SpanContext + ParentSpanID int64 + Operation string + Start time.Time + Duration time.Duration + Tags ot.Tags + Logs []ot.LogRecord } func (r *spanS) BaggageItem(key string) string { r.Lock() defer r.Unlock() - return r.raw.Context.Baggage[key] + return r.context.Baggage[key] } func (r *spanS) SetBaggageItem(key, val string) ot.Span { @@ -29,13 +38,13 @@ func (r *spanS) SetBaggageItem(key, val string) ot.Span { r.Lock() defer r.Unlock() - r.raw.Context = r.raw.Context.WithBaggageItem(key, val) + r.context = r.context.WithBaggageItem(key, val) return r } func (r *spanS) Context() ot.SpanContext { - return r.raw.Context + return r.context } func (r *spanS) Finish() { @@ -48,7 +57,7 @@ func (r *spanS) FinishWithOptions(opts ot.FinishOptions) { finishTime = time.Now() } - duration := finishTime.Sub(r.raw.Start) + duration := finishTime.Sub(r.Start) r.Lock() defer r.Unlock() for _, lr := range opts.LogRecords { @@ -59,14 +68,14 @@ func (r *spanS) FinishWithOptions(opts ot.FinishOptions) { r.appendLog(ld.ToLogRecord()) } - r.raw.Duration = duration - r.tracer.options.Recorder.RecordSpan(r.raw) + r.Duration = duration + r.tracer.options.Recorder.RecordSpan(r) } func (r *spanS) appendLog(lr ot.LogRecord) { maxLogs := r.tracer.options.MaxLogsPerSpan - if maxLogs == 0 || len(r.raw.Logs) < maxLogs { - r.raw.Logs = append(r.raw.Logs, lr) + if maxLogs == 0 || len(r.Logs) < maxLogs { + r.Logs = append(r.Logs, lr) } } @@ -85,7 +94,7 @@ func (r *spanS) Log(ld ot.LogData) { } func (r *spanS) trim() bool { - return !r.raw.Context.Sampled && r.tracer.options.TrimUnsampledSpans + return !r.context.Sampled && r.tracer.options.TrimUnsampledSpans } func (r *spanS) LogEvent(event string) { @@ -131,7 +140,7 @@ func (r *spanS) LogKV(keyValues ...interface{}) { func (r *spanS) SetOperationName(operationName string) ot.Span { r.Lock() defer r.Unlock() - r.raw.Operation = operationName + r.Operation = operationName return r } @@ -143,11 +152,11 @@ func (r *spanS) SetTag(key string, value interface{}) ot.Span { return r } - if r.raw.Tags == nil { - r.raw.Tags = ot.Tags{} + if r.Tags == nil { + r.Tags = ot.Tags{} } - r.raw.Tags[key] = value + r.Tags[key] = value return r } @@ -155,3 +164,91 @@ func (r *spanS) SetTag(key string, value interface{}) ot.Span { func (r *spanS) Tracer() ot.Tracer { return r.tracer } + +func (r *spanS) getTag(tag string) interface{} { + var x, ok = r.Tags[tag] + if !ok { + x = "" + } + return x +} + +func (r *spanS) getIntTag(tag string) int { + d := r.Tags[tag] + if d == nil { + return -1 + } + + x, ok := d.(int) + if !ok { + return -1 + } + + return x +} + +func (r *spanS) getStringTag(tag string) string { + d := r.Tags[tag] + if d == nil { + return "" + } + return fmt.Sprint(d) +} + +func (r *spanS) getHostName() string { + hostTag := r.getStringTag(string(ext.PeerHostname)) + if hostTag != "" { + return hostTag + } + + h, err := os.Hostname() + if err != nil { + h = "localhost" + } + return h +} + +func (r *spanS) getServiceName() string { + // ServiceName can be determined from multiple sources and has + // the following priority (preferred first): + // 1. If added to the span via the OT component tag + // 2. If added to the span via the OT http.url tag + // 3. Specified in the tracer instantiation via Service option + component := r.getStringTag(string(ext.Component)) + if len(component) > 0 { + return component + } + + httpURL := r.getStringTag(string(ext.HTTPUrl)) + if len(httpURL) > 0 { + return httpURL + } + return sensor.serviceName +} + +func (r *spanS) getSpanKind() string { + kind := r.getStringTag(string(ext.SpanKind)) + + switch kind { + case string(ext.SpanKindRPCServerEnum), "consumer", "entry": + return "entry" + case string(ext.SpanKindRPCClientEnum), "producer", "exit": + return "exit" + } + return "" +} + +func (r *spanS) collectLogs() map[uint64]map[string]interface{} { + logs := make(map[uint64]map[string]interface{}) + for _, l := range r.Logs { + if _, ok := logs[uint64(l.Timestamp.UnixNano())/uint64(time.Millisecond)]; !ok { + logs[uint64(l.Timestamp.UnixNano())/uint64(time.Millisecond)] = make(map[string]interface{}) + } + + for _, f := range l.Fields { + logs[uint64(l.Timestamp.UnixNano())/uint64(time.Millisecond)][f.Key()] = f.Value() + } + } + + return logs +} diff --git a/span_test.go b/span_test.go index cea7dffab..e3b61ad56 100644 --- a/span_test.go +++ b/span_test.go @@ -7,32 +7,34 @@ import ( "github.com/stretchr/testify/assert" "github.com/instana/golang-sensor" - bt "github.com/opentracing/basictracer-go" ot "github.com/opentracing/opentracing-go" ) func TestBasicSpan(t *testing.T) { const op = "test" - recorder := bt.NewInMemoryRecorder() - tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&opts, recorder) sp := tracer.StartSpan(op) sp.Finish() spans := recorder.GetSpans() - assert.Equal(t, len(spans), 1) + assert.Equal(t, 1, len(spans)) span := spans[0] - assert.NotNil(t, span.Context, "Context is nil!") - assert.NotNil(t, span.Duration, "Duration is nil!") - assert.NotNil(t, span.Operation, "Operation is nil!") - assert.Equal(t, span.ParentSpanID, uint64(0), "ParentSpan shouldn't have a value") - assert.NotNil(t, span.Start, "Start is nil!") - assert.Nil(t, span.Tags, "Tags is nil!") + assert.NotZero(t, span.SpanID, "Missing span ID for this span") + assert.NotZero(t, span.TraceID, "Missing trace ID for this span") + assert.NotZero(t, span.Timestamp, "Missing timestamp for this span") + assert.NotNil(t, span.Duration, "Duration is nil") + assert.Equal(t, "sdk", span.Name, "Missing sdk span name") + assert.Equal(t, "test", span.Data.SDK.Name, "Missing span name") + assert.Nil(t, span.Data.SDK.Custom.Tags, "Tags has an unexpected value") + assert.Nil(t, span.Data.SDK.Custom.Baggage, "Baggage has an unexpected value") } func TestSpanHeritage(t *testing.T) { - recorder := bt.NewInMemoryRecorder() + recorder := instana.NewTestRecorder() tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) parentSpan := tracer.StartSpan("parent") @@ -49,27 +51,28 @@ func TestSpanHeritage(t *testing.T) { cSpan := spans[0] pSpan := spans[1] - assert.Equal(t, "child", cSpan.Operation, "Child span name doesn't compute") - assert.Equal(t, "parent", pSpan.Operation, "Parent span name doesn't compute") + assert.Equal(t, "child", cSpan.Data.SDK.Name, "Child span name doesn't compute") + assert.Equal(t, "parent", pSpan.Data.SDK.Name, "Parent span name doesn't compute") // Parent should not have a parent - assert.Equal(t, pSpan.ParentSpanID, uint64(0), "ParentSpanID shouldn't have a value") + assert.Nil(t, pSpan.ParentID, "ParentID shouldn't have a value") // Child must have parent ID set as parent - assert.Equal(t, pSpan.Context.SpanID, cSpan.ParentSpanID, "parentID doesn't match") + assert.Equal(t, pSpan.SpanID, *cSpan.ParentID, "parentID doesn't match") // Must be root span - assert.Equal(t, pSpan.Context.TraceID, pSpan.Context.SpanID, "not a root span") + assert.Equal(t, pSpan.TraceID, pSpan.SpanID, "not a root span") // Trace ID must be consistent across spans - assert.Equal(t, cSpan.Context.TraceID, pSpan.Context.TraceID, "trace IDs don't match") + assert.Equal(t, cSpan.TraceID, pSpan.TraceID, "trace IDs don't match") } func TestSpanBaggage(t *testing.T) { const op = "test" - recorder := bt.NewInMemoryRecorder() - tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&opts, recorder) sp := tracer.StartSpan(op) sp.SetBaggageItem("foo", "bar") @@ -79,13 +82,14 @@ func TestSpanBaggage(t *testing.T) { assert.Equal(t, len(spans), 1) span := spans[0] - assert.NotNil(t, span.Context.Baggage, "Missing Baggage") + assert.NotNil(t, span.Data.SDK.Custom.Baggage, "Missing Baggage") } func TestSpanTags(t *testing.T) { const op = "test" - recorder := bt.NewInMemoryRecorder() - tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&opts, recorder) sp := tracer.StartSpan(op) sp.SetTag("foo", "bar") @@ -95,5 +99,5 @@ func TestSpanTags(t *testing.T) { assert.Equal(t, len(spans), 1) span := spans[0] - assert.NotNil(t, span.Tags, "Missing Tags") + assert.NotNil(t, span.Data.SDK.Custom.Tags, "Missing Tags") } diff --git a/tracer.go b/tracer.go index ca780668f..bcfca3ffb 100644 --- a/tracer.go +++ b/tracer.go @@ -3,7 +3,6 @@ package instana import ( "time" - bt "github.com/opentracing/basictracer-go" ot "github.com/opentracing/opentracing-go" ) @@ -13,7 +12,7 @@ const ( ) type tracerS struct { - options bt.Options + options TracerOptions textPropagator *textMapPropagator } @@ -56,15 +55,15 @@ Loop: for _, ref := range opts.References { switch ref.Type { case ot.ChildOfRef, ot.FollowsFromRef: - refCtx := ref.ReferencedContext.(bt.SpanContext) - span.raw.Context.TraceID = refCtx.TraceID - span.raw.Context.SpanID = randomID() - span.raw.Context.Sampled = refCtx.Sampled - span.raw.ParentSpanID = refCtx.SpanID + refCtx := ref.ReferencedContext.(SpanContext) + span.context.TraceID = refCtx.TraceID + span.context.SpanID = randomID() + span.context.Sampled = refCtx.Sampled + span.ParentSpanID = refCtx.SpanID if l := len(refCtx.Baggage); l > 0 { - span.raw.Context.Baggage = make(map[string]string, l) + span.context.Baggage = make(map[string]string, l) for k, v := range refCtx.Baggage { - span.raw.Context.Baggage[k] = v + span.context.Baggage[k] = v } } @@ -72,10 +71,10 @@ Loop: } } - if span.raw.Context.TraceID == 0 { - span.raw.Context.SpanID = randomID() - span.raw.Context.TraceID = span.raw.Context.SpanID - span.raw.Context.Sampled = r.options.ShouldSample(span.raw.Context.TraceID) + if span.context.TraceID == 0 { + span.context.SpanID = randomID() + span.context.TraceID = span.context.SpanID + span.context.Sampled = r.options.ShouldSample(span.context.TraceID) } return r.startSpanInternal(span, operationName, startTime, tags) @@ -83,15 +82,15 @@ Loop: func (r *tracerS) startSpanInternal(span *spanS, operationName string, startTime time.Time, tags ot.Tags) ot.Span { span.tracer = r - span.raw.Operation = operationName - span.raw.Start = startTime - span.raw.Duration = -1 - span.raw.Tags = tags + span.Operation = operationName + span.Start = startTime + span.Duration = -1 + span.Tags = tags return span } -func shouldSample(traceID uint64) bool { +func shouldSample(traceID int64) bool { return false } @@ -108,9 +107,9 @@ func NewTracerWithOptions(options *Options) ot.Tracer { } // NewTracerWithEverything Get a new Tracer with the works. -func NewTracerWithEverything(options *Options, recorder bt.SpanRecorder) ot.Tracer { +func NewTracerWithEverything(options *Options, recorder SpanRecorder) ot.Tracer { InitSensor(options) - ret := &tracerS{options: bt.Options{ + ret := &tracerS{options: TracerOptions{ Recorder: recorder, ShouldSample: shouldSample, MaxLogsPerSpan: MaxLogsPerSpan}} diff --git a/tracer_options.go b/tracer_options.go new file mode 100644 index 000000000..2c4c29edd --- /dev/null +++ b/tracer_options.go @@ -0,0 +1,91 @@ +package instana + +import ( + bt "github.com/opentracing/basictracer-go" + opentracing "github.com/opentracing/opentracing-go" +) + +// Tracer extends the opentracing.Tracer interface +type Tracer interface { + opentracing.Tracer + + // Options gets the Options used in New() or NewWithOptions(). + Options() TracerOptions +} + +// TracerOptions allows creating a customized Tracer via NewWithOptions. The object +// must not be updated when there is an active tracer using it. +type TracerOptions struct { + // ShouldSample is a function which is called when creating a new Span and + // determines whether that Span is sampled. The randomized TraceID is supplied + // to allow deterministic sampling decisions to be made across different nodes. + // For example, + // + // func(traceID uint64) { return traceID % 64 == 0 } + // + // samples every 64th trace on average. + ShouldSample func(traceID int64) bool + // TrimUnsampledSpans turns potentially expensive operations on unsampled + // Spans into no-ops. More precisely, tags and log events are silently + // discarded. If NewSpanEventListener is set, the callbacks will still fire. + TrimUnsampledSpans bool + // Recorder receives Spans which have been finished. + Recorder SpanRecorder + // NewSpanEventListener can be used to enhance the tracer by effectively + // attaching external code to trace events. See NetTraceIntegrator for a + // practical example, and event.go for the list of possible events. + NewSpanEventListener func() func(bt.SpanEvent) + // DropAllLogs turns log events on all Spans into no-ops. + // If NewSpanEventListener is set, the callbacks will still fire. + DropAllLogs bool + // MaxLogsPerSpan limits the number of Logs in a span (if set to a nonzero + // value). If a span has more logs than this value, logs are dropped as + // necessary (and replaced with a log describing how many were dropped). + // + // About half of the MaxLogPerSpan logs kept are the oldest logs, and about + // half are the newest logs. + // + // If NewSpanEventListener is set, the callbacks will still fire for all log + // events. This value is ignored if DropAllLogs is true. + MaxLogsPerSpan int + // DebugAssertSingleGoroutine internally records the ID of the goroutine + // creating each Span and verifies that no operation is carried out on + // it on a different goroutine. + // Provided strictly for development purposes. + // Passing Spans between goroutine without proper synchronization often + // results in use-after-Finish() errors. For a simple example, consider the + // following pseudocode: + // + // func (s *Server) Handle(req http.Request) error { + // sp := s.StartSpan("server") + // defer sp.Finish() + // wait := s.queueProcessing(opentracing.ContextWithSpan(context.Background(), sp), req) + // select { + // case resp := <-wait: + // return resp.Error + // case <-time.After(10*time.Second): + // sp.LogEvent("timed out waiting for processing") + // return ErrTimedOut + // } + // } + // + // This looks reasonable at first, but a request which spends more than ten + // seconds in the queue is abandoned by the main goroutine and its trace + // finished, leading to use-after-finish when the request is finally + // processed. Note also that even joining on to a finished Span via + // StartSpanWithOptions constitutes an illegal operation. + // + // Code bases which do not require (or decide they do not want) Spans to + // be passed across goroutine boundaries can run with this flag enabled in + // tests to increase their chances of spotting wrong-doers. + DebugAssertSingleGoroutine bool + // DebugAssertUseAfterFinish is provided strictly for development purposes. + // When set, it attempts to exacerbate issues emanating from use of Spans + // after calling Finish by running additional assertions. + DebugAssertUseAfterFinish bool + // EnableSpanPool enables the use of a pool, so that the tracer reuses spans + // after Finish has been called on it. Adds a slight performance gain as it + // reduces allocations. However, if you have any use-after-finish race + // conditions the code may panic. + EnableSpanPool bool +} diff --git a/tracer_test.go b/tracer_test.go index 4fc34bbba..e6d4896f8 100644 --- a/tracer_test.go +++ b/tracer_test.go @@ -6,7 +6,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/instana/golang-sensor" - bt "github.com/opentracing/basictracer-go" //opentracing "github.com/opentracing/opentracing-go" ) @@ -14,8 +13,9 @@ func TestTracerAPI(t *testing.T) { tracer := instana.NewTracer() assert.NotNil(t, tracer, "NewTracer returned nil") - recorder := bt.NewInMemoryRecorder() - tracer = instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer = instana.NewTracerWithEverything(&opts, recorder) assert.NotNil(t, tracer, "NewTracerWithEverything returned nil") tracer = instana.NewTracerWithOptions(&instana.Options{}) @@ -23,8 +23,9 @@ func TestTracerAPI(t *testing.T) { } func TestTracerBasics(t *testing.T) { - recorder := bt.NewInMemoryRecorder() - tracer := instana.NewTracerWithEverything(&instana.Options{}, recorder) + opts := instana.Options{LogLevel: instana.Debug} + recorder := instana.NewTestRecorder() + tracer := instana.NewTracerWithEverything(&opts, recorder) sp := tracer.StartSpan("test") sp.SetBaggageItem("foo", "bar") diff --git a/util.go b/util.go index 68e2cbe92..9638b84dd 100644 --- a/util.go +++ b/util.go @@ -15,10 +15,10 @@ var ( seededIDLock sync.Mutex ) -func randomID() uint64 { +func randomID() int64 { seededIDLock.Lock() defer seededIDLock.Unlock() - return uint64(seededIDGen.Int63()) + return int64(seededIDGen.Int63()) } func getCommandLine() (string, []string) { @@ -40,3 +40,8 @@ func getCommandLine() (string, []string) { log.debug("cmdline says:", parts[0], parts[1:]) return parts[0], parts[1:] } + +func abs(x int64) int64 { + y := x >> 63 + return (x + y) ^ y +} diff --git a/util_internal_test.go b/util_internal_test.go new file mode 100644 index 000000000..4d21e1343 --- /dev/null +++ b/util_internal_test.go @@ -0,0 +1,26 @@ +package instana + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Trace IDs (and Span IDs) are based on Java Signed Long datatype +const MinUint64 = uint64(0) +const MaxUint64 = uint64(18446744073709551615) +const MinInt64 = int64(-9223372036854775808) +const MaxInt64 = int64(9223372036854775807) + +func TestGeneratedIDRange(t *testing.T) { + + var count = 10000 + var id = int64(0) + for index := 0; index < count; index++ { + id = randomID() + + assert.True(t, id <= 9223372036854775807, "Generated ID is out of bounds (+)") + assert.True(t, id >= -9223372036854775808, "Generated ID is out of bounds (-)") + + } +}