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.
}