diff --git a/.gitignore b/.gitignore index e43b0f988..0fad2664d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +debug.test diff --git a/custom.go b/custom.go index a176c1a3b..cd1c8736d 100644 --- a/custom.go +++ b/custom.go @@ -5,6 +5,7 @@ import ( ) type CustomData struct { - Tags ot.Tags `json:"tags,omitempty"` - Logs map[uint64]map[string]interface{} `json:"logs,omitempty"` + 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 index b960eed88..6db91ba01 100644 --- a/data.go +++ b/data.go @@ -1,9 +1,6 @@ package instana type Data struct { - Service string `json:"service"` - HTTP *HTTPData `json:"http,omitempty"` - RPC *RPCData `json:"rpc,omitempty"` - Baggage map[string]string `json:"baggage,omitempty"` - Custom *CustomData `json:"custom,omitempty"` + Service string `json:"service,omitempty"` + SDK *SDKData `json:"sdk"` } diff --git a/recorder.go b/recorder.go index 75ef811a1..d7970e225 100644 --- a/recorder.go +++ b/recorder.go @@ -1,6 +1,7 @@ package instana import ( + "fmt" "os" "sync" "time" @@ -11,7 +12,8 @@ import ( type SpanRecorder struct { sync.RWMutex - spans []Span + spans []Span + testMode bool } type Span struct { @@ -25,15 +27,36 @@ type Span struct { Data interface{} `json:"data"` } +// NewRecorder Establish a new span recorder func NewRecorder() *SpanRecorder { r := new(SpanRecorder) r.init() + return r +} +// NewTestRecorder Establish a new span recorder used for testing +func NewTestRecorder() *SpanRecorder { + r := new(SpanRecorder) + r.testMode = true + r.init() return r } +// GetSpans returns a copy of the array of spans accumulated so far. +func (r *SpanRecorder) GetSpans() []Span { + r.RLock() + defer r.RUnlock() + spans := make([]Span, len(r.spans)) + copy(spans, r.spans) + return spans +} + func getTag(rawSpan basictracer.RawSpan, tag string) interface{} { - return rawSpan.Tags[tag] + var x, ok = rawSpan.Tags[tag] + if !ok { + x = "" + } + return x } func getIntTag(rawSpan basictracer.RawSpan, tag string) int { @@ -51,12 +74,11 @@ func getIntTag(rawSpan basictracer.RawSpan, tag string) int { } func getStringTag(rawSpan basictracer.RawSpan, tag string) string { - d := getTag(rawSpan, tag) + d := rawSpan.Tags[tag] if d == nil { return "" } - - return d.(string) + return fmt.Sprint(d) } func getHostName(rawSpan basictracer.RawSpan) string { @@ -74,24 +96,35 @@ func getHostName(rawSpan basictracer.RawSpan) string { } func getServiceName(rawSpan basictracer.RawSpan) string { - s := getStringTag(rawSpan, string(ext.Component)) - if s == "" { - s = getStringTag(rawSpan, string(ext.PeerService)) - if s == "" { - return sensor.serviceName + // 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 s + return sensor.serviceName } -func getHTTPType(rawSpan basictracer.RawSpan) string { +func getSpanKind(rawSpan basictracer.RawSpan) string { kind := getStringTag(rawSpan, string(ext.SpanKind)) - if kind == string(ext.SpanKindRPCServerEnum) { - return HTTPServer - } - return HTTPClient + 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{} { @@ -111,14 +144,19 @@ func collectLogs(rawSpan basictracer.RawSpan) map[uint64]map[string]interface{} func (r *SpanRecorder) init() { r.reset() - ticker := time.NewTicker(1 * time.Second) - go func() { - for range ticker.C { - log.debug("Sending spans to agent", len(r.spans)) - r.send() - } - }() + if r.testMode { + log.debug("Recorder in test mode. Not reporting spans to the backend.") + } else { + ticker := time.NewTicker(1 * time.Second) + go func() { + for range ticker.C { + log.debug("Sending spans to agent", len(r.spans)) + + r.send() + } + }() + } } func (r *SpanRecorder) reset() { @@ -129,27 +167,12 @@ func (r *SpanRecorder) reset() { func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { var data = &Data{} - var tp string - h := getHostName(rawSpan) - status := getIntTag(rawSpan, string(ext.HTTPStatusCode)) - if status >= 0 { - tp = getHTTPType(rawSpan) - data = &Data{HTTP: &HTTPData{ - Host: h, - URL: getStringTag(rawSpan, string(ext.HTTPUrl)), - Method: getStringTag(rawSpan, string(ext.HTTPMethod)), - Status: status}} - } else { - log.debug("No HTTP status code provided or invalid status code, opting out to RPC") - - tp = RPC - data = &Data{RPC: &RPCData{ - Host: h, - Call: rawSpan.Operation}} - } + kind := getSpanKind(rawSpan) - data.Custom = &CustomData{Tags: rawSpan.Tags, - Logs: collectLogs(rawSpan)} + data.SDK = &SDKData{ + Name: rawSpan.Operation, + Type: kind, + Custom: &CustomData{Tags: rawSpan.Tags, Logs: collectLogs(rawSpan)}} baggage := make(map[string]string) rawSpan.Context.ForeachBaggageItem(func(k string, v string) bool { @@ -159,7 +182,7 @@ func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { }) if len(baggage) > 0 { - data.Baggage = baggage + data.SDK.Custom.Baggage = baggage } data.Service = getServiceName(rawSpan) @@ -184,11 +207,11 @@ func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { SpanID: rawSpan.Context.SpanID, Timestamp: uint64(rawSpan.Start.UnixNano()) / uint64(time.Millisecond), Duration: uint64(rawSpan.Duration) / uint64(time.Millisecond), - Name: tp, + Name: "sdk", From: sensor.agent.from, Data: &data}) - if len(r.spans) == sensor.options.ForceTransmissionStartingAt { + if !r.testMode && (len(r.spans) == sensor.options.ForceTransmissionStartingAt) { log.debug("Forcing spans to agent", len(r.spans)) r.send() @@ -196,7 +219,7 @@ func (r *SpanRecorder) RecordSpan(rawSpan basictracer.RawSpan) { } func (r *SpanRecorder) send() { - if sensor.agent.canSend() { + if sensor.agent.canSend() && !r.testMode { go func() { _, err := sensor.agent.request(sensor.agent.makeURL(AgentTracesURL), "POST", r.spans) diff --git a/recorder_internal_test.go b/recorder_internal_test.go new file mode 100644 index 000000000..fd6b0a864 --- /dev/null +++ b/recorder_internal_test.go @@ -0,0 +1,199 @@ +package instana + +import ( + "testing" + + ext "github.com/opentracing/opentracing-go/ext" + "github.com/stretchr/testify/assert" +) + +func TestGetServiceNameByTracer(t *testing.T) { + opts := Options{LogLevel: Debug, Service: "tracer-named-service"} + + InitSensor(&opts) + recorder := NewTestRecorder() + tracer := NewTracerWithEverything(&opts, recorder) + + sp := tracer.StartSpan("http-client") + sp.SetTag(string(ext.SpanKind), "exit") + sp.SetTag("http.status", 200) + sp.SetTag(string(ext.HTTPMethod), "GET") + + sp.Finish() + + rawSpan := sp.(*spanS).raw + serviceName := getServiceName(rawSpan) + assert.EqualValues(t, "tracer-named-service", serviceName, "Wrong Service Name") +} + +func TestGetServiceNameByHTTP(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + 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") + + sp.Finish() + + rawSpan := sp.(*spanS).raw + serviceName := getServiceName(rawSpan) + assert.EqualValues(t, "https://www.instana.com/product/", serviceName, "Wrong Service Name") +} + +func TestGetServiceNameByComponent(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + 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") + + sp.Finish() + + rawSpan := sp.(*spanS).raw + serviceName := getServiceName(rawSpan) + assert.EqualValues(t, "component-named-service", serviceName, "Wrong Service Name") +} + +func TestSpanKind(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + recorder := NewTestRecorder() + 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) + 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) + 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) + 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) + assert.EqualValues(t, "exit", kind, "Wrong span kind") +} + +func TestGetTag(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + recorder := NewTestRecorder() + tracer := NewTracerWithEverything(&opts, recorder) + + // Exit + sp := tracer.StartSpan("http-client") + sp.SetTag("foo", "bar") + sp.Finish() + rawSpan := sp.(*spanS).raw + tag := getTag(rawSpan, "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") + assert.EqualValues(t, "", tag, "getTag should return empty string for non-existent tags") +} + +func TestGetIntTag(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + 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") + assert.EqualValues(t, 1, tag, "geIntTag unexpected return value") + + // Non-existent + tag = getIntTag(rawSpan, "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") + assert.EqualValues(t, -1, tag, "geIntTag should return -1 for non-int tags") +} + +func TestGetStringTag(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + 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") + assert.EqualValues(t, "twotwo", tag, "geStringTag unexpected return value") + + // Non-existent + tag = getStringTag(rawSpan, "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") + 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") + assert.EqualValues(t, "2.3420493", tag, "geStringTag should return string for non-string tag values") +} + +func TestGetHostName(t *testing.T) { + opts := Options{LogLevel: Debug} + + InitSensor(&opts) + 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) + assert.True(t, len(hostname) > 0, "must return a valid string value") +} diff --git a/recorder_test.go b/recorder_test.go new file mode 100644 index 000000000..31f201cdb --- /dev/null +++ b/recorder_test.go @@ -0,0 +1,30 @@ +package instana_test + +import ( + "bytes" + "encoding/json" + "log" + "testing" + + "github.com/instana/golang-sensor" + ext "github.com/opentracing/opentracing-go/ext" +) + +func TestRecorderSDKReporting(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() + + spans := recorder.GetSpans() + j, _ := json.MarshalIndent(spans, "", " ") + log.Printf("spans:", bytes.NewBuffer(j)) +} diff --git a/sdk.go b/sdk.go new file mode 100644 index 000000000..12b614778 --- /dev/null +++ b/sdk.go @@ -0,0 +1,9 @@ +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 344020816..e269a0c07 100644 --- a/sensor.go +++ b/sensor.go @@ -61,9 +61,12 @@ func (r *sensorS) configureServiceName() { // InitSensor Intializes the sensor (without tracing) to begin collecting // and reporting metrics. func InitSensor(options *Options) { - sensor = new(sensorS) - sensor.initLog() - sensor.init(options) - - log.debug("initialized sensor") + if sensor == nil { + sensor = new(sensorS) + sensor.initLog() + sensor.init(options) + log.debug("initialized sensor") + } else { + log.debug("not initializing already init'd sensor") + } } diff --git a/tracer.go b/tracer.go index cc6ace22c..ca780668f 100644 --- a/tracer.go +++ b/tracer.go @@ -109,6 +109,7 @@ func NewTracerWithOptions(options *Options) ot.Tracer { // NewTracerWithEverything Get a new Tracer with the works. func NewTracerWithEverything(options *Options, recorder bt.SpanRecorder) ot.Tracer { + InitSensor(options) ret := &tracerS{options: bt.Options{ Recorder: recorder, ShouldSample: shouldSample,