module/apmot/span.go (243 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 apmot // import "go.elastic.co/apm/module/apmot/v2" import ( "fmt" "net/http" "net/url" "sync" "time" opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/log" "go.elastic.co/apm/module/apmhttp/v2" "go.elastic.co/apm/v2" ) // otSpan wraps apm objects to implement the opentracing.Span interface. type otSpan struct { tracer *otTracer mu sync.Mutex span *apm.Span tags opentracing.Tags ctx spanContext } // Span returns s.span, the underlying apm.Span. This is used to satisfy // SpanFromContext. func (s *otSpan) Span() *apm.Span { return s.span } // SetOperationName sets or changes the operation name. func (s *otSpan) SetOperationName(operationName string) opentracing.Span { if s.span != nil { s.span.Name = operationName } else { s.ctx.tx.Name = operationName } return s } // SetTag adds or changes a tag. func (s *otSpan) SetTag(key string, value interface{}) opentracing.Span { s.mu.Lock() defer s.mu.Unlock() if s.tags == nil { s.tags = make(opentracing.Tags, 1) } s.tags[key] = value return s } // Finish ends the span; this (or FinishWithOptions) must be the last method // call on the span, except for calls to Context which may be called at any // time. func (s *otSpan) Finish() { s.FinishWithOptions(opentracing.FinishOptions{}) } // FinishWithOptions is like Finish, but provides explicit control over the // end timestamp and log data. func (s *otSpan) FinishWithOptions(opts opentracing.FinishOptions) { s.mu.Lock() defer s.mu.Unlock() if !opts.FinishTime.IsZero() { duration := opts.FinishTime.Sub(s.ctx.startTime) if s.span != nil { s.span.Duration = duration } else { s.ctx.tx.Duration = duration } } if s.span != nil { for _, record := range opts.LogRecords { timestamp := record.Timestamp if timestamp.IsZero() { timestamp = opts.FinishTime } logFields(s.tracer.tracer, nil, s.span, timestamp, record.Fields) } s.setSpanContext() s.span.End() } else { s.setTransactionContext() for _, record := range opts.LogRecords { timestamp := record.Timestamp if timestamp.IsZero() { timestamp = opts.FinishTime } logFields(s.tracer.tracer, s.ctx.tx, nil, timestamp, record.Fields) } s.ctx.tx.End() } } // Tracer returns the Tracer that created this span. func (s *otSpan) Tracer() opentracing.Tracer { return s.tracer } // Context returns the span's current context. // // It is valid to call Context after calling Finish or FinishWithOptions. // The resulting context is also valid after the span is finished. func (s *otSpan) Context() opentracing.SpanContext { return &s.ctx } // BaggageItem returns the empty string; we do not support baggage. func (*otSpan) BaggageItem(key string) string { return "" } // SetBaggageItem is a no-op; we do not support baggage. func (s *otSpan) SetBaggageItem(key, val string) opentracing.Span { // We do not support baggage. return s } func stringify(v interface{}) string { if v, ok := v.(string); ok { return v } return fmt.Sprint(v) } func (s *otSpan) setSpanContext() { var ( dbContext apm.DatabaseSpanContext component string httpURL string httpMethod string httpHost string haveDBContext bool haveHTTPContext bool haveHTTPHostTag bool ) for k, v := range s.tags { switch k { case "component": component = stringify(v) case "db.instance": dbContext.Instance = stringify(v) haveDBContext = true case "db.statement": dbContext.Statement = stringify(v) haveDBContext = true case "db.type": dbContext.Type = stringify(v) haveDBContext = true case "db.user": dbContext.User = stringify(v) haveDBContext = true case "http.url": haveHTTPContext = true httpURL = stringify(v) case "http.method": haveHTTPContext = true httpMethod = stringify(v) case "http.host": haveHTTPContext = true haveHTTPHostTag = true httpHost = stringify(v) // Elastic APM-specific tags: case "type": s.span.Type = stringify(v) default: s.span.Context.SetLabel(k, stringify(v)) } } switch { case haveHTTPContext: if s.span.Type == "" { s.span.Type = "external" s.span.Subtype = "http" } url, err := url.Parse(httpURL) if err == nil { // handles the case where the url.Host hasn't been set. // Tries obtaining the host value from the "http.host" tag. // If not found, or if the url.Host has a value, it won't // mutate the existing host. if url.Host == "" && haveHTTPHostTag { url.Host = httpHost } s.span.Context.SetHTTPRequest(&http.Request{ ProtoMinor: 1, ProtoMajor: 1, Method: httpMethod, URL: url, }) } case haveDBContext: if s.span.Type == "" { s.span.Type = "db" s.span.Subtype = dbContext.Type } s.span.Context.SetDatabase(dbContext) } if s.span.Type == "" { s.span.Type = "custom" s.span.Subtype = component } } func (s *otSpan) setTransactionContext() { var ( component string httpMethod string httpStatusCode = -1 httpURL string isError bool ) for k, v := range s.tags { switch k { case "component": component = stringify(v) case "http.method": httpMethod = stringify(v) case "http.status_code": if code, ok := v.(uint16); ok { httpStatusCode = int(code) } case "http.url": httpURL = stringify(v) case "error": isError, _ = v.(bool) // Elastic APM-specific tags: case "type": s.ctx.tx.Type = stringify(v) case "result": s.ctx.tx.Result = stringify(v) case "user.id": s.ctx.tx.Context.SetUserID(stringify(v)) case "user.email": s.ctx.tx.Context.SetUserEmail(stringify(v)) case "user.username": s.ctx.tx.Context.SetUsername(stringify(v)) default: s.ctx.tx.Context.SetLabel(k, stringify(v)) } } if s.ctx.tx.Type == "" { if httpURL != "" { s.ctx.tx.Type = "request" } else if component != "" { s.ctx.tx.Type = component } else { s.ctx.tx.Type = "custom" } } if s.ctx.tx.Result == "" { if httpStatusCode != -1 { s.ctx.tx.Result = apmhttp.StatusCodeResult(httpStatusCode) s.ctx.tx.Context.SetHTTPStatusCode(httpStatusCode) } else if isError { s.ctx.tx.Result = "error" } } if httpURL != "" { uri, err := url.ParseRequestURI(httpURL) if err == nil { var req http.Request req.ProtoMajor = 1 // Assume HTTP/1.1 req.ProtoMinor = 1 req.Method = httpMethod req.URL = uri s.ctx.tx.Context.SetHTTPRequest(&req) } } } // LogKV is part of the opentracing.Span interface. // We send error events to Elastic APM. func (s *otSpan) LogKV(keyValues ...interface{}) { logKV(s.tracer.tracer, s.ctx.tx, s.span, time.Time{}, keyValues) } // LogFields is part of the opentracing.Span interface. // We send error events to Elastic APM. func (s *otSpan) LogFields(fields ...log.Field) { logFields(s.tracer.tracer, s.ctx.tx, s.span, time.Time{}, fields) } // LogEvent is deprecated, and is a no-op. func (s *otSpan) LogEvent(event string) { // Deprecated, no-op. } // LogEventWithPayload is deprecated, and is a no-op. func (s *otSpan) LogEventWithPayload(event string, payload interface{}) { // Deprecated, no-op. } // Log is deprecated, and is a no-op. func (s *otSpan) Log(ld opentracing.LogData) { // Deprecated, no-op. }