spancontext.go (183 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 apm // import "go.elastic.co/apm/v2"
import (
"fmt"
"net/http"
"net/url"
"strings"
"go.elastic.co/apm/v2/internal/apmhttputil"
"go.elastic.co/apm/v2/model"
)
// SpanContext provides methods for setting span context.
type SpanContext struct {
model model.SpanContext
destination model.DestinationSpanContext
destinationService model.DestinationServiceSpanContext
service model.ServiceSpanContext
serviceTarget model.ServiceTargetSpanContext
destinationCloud model.DestinationCloudSpanContext
message model.MessageSpanContext
databaseRowsAffected int64
database model.DatabaseSpanContext
http model.HTTPSpanContext
otel *model.OTel
// If SetDestinationService has been called, we do not auto-set its
// resource value on span end.
setDestinationServiceCalled bool
// If SetServiceTarget has been called, we do not auto-set its
// values on span end.
setServiceTargetCalled bool
}
// DatabaseSpanContext holds database span context.
type DatabaseSpanContext struct {
// Instance holds the database instance name.
Instance string
// Statement holds the statement executed in the span,
// e.g. "SELECT * FROM foo".
Statement string
// Type holds the database type, e.g. "sql".
Type string
// User holds the username used for database access.
User string
}
// ServiceSpanContext holds contextual information about the service
// for a span that relates to an operation involving an external service.
type ServiceSpanContext struct {
// Target holds the destination service.
Target *ServiceTargetSpanContext
}
// ServiceTargetSpanContext fields replace the `span.destination.service.*`
// fields that are deprecated.
type ServiceTargetSpanContext struct {
// Type holds the destination service type.
Type string
// Name holds the destination service name.
Name string
}
// DestinationServiceSpanContext holds destination service span span.
type DestinationServiceSpanContext struct {
// Name holds a name for the destination service, which may be used
// for grouping and labeling in service maps. Deprecated.
//
// Deprecated: replaced by `service.target.{type,name}`.
Name string
// Resource holds an identifier for a destination service resource,
// such as a message queue.
//
// Deprecated: replaced by `service.target.{type,name}`.
Resource string
}
// DestinationCloudSpanContext holds contextual information about a
// destination cloud.
type DestinationCloudSpanContext struct {
// Region holds the destination cloud region.
Region string
}
// MessageSpanContext holds contextual information about a message.
type MessageSpanContext struct {
// QueueName holds the message queue name.
QueueName string
}
func (c *SpanContext) build() *model.SpanContext {
switch {
case len(c.model.Tags) != 0:
case c.model.Message != nil:
case c.model.Database != nil:
case c.model.HTTP != nil:
case c.model.Destination != nil:
default:
return nil
}
return &c.model
}
func (c *SpanContext) reset() {
*c = SpanContext{
model: model.SpanContext{
Tags: c.model.Tags[:0],
},
}
}
// SetOTelAttributes sets the provided OpenTelemetry attributes.
func (c *SpanContext) SetOTelAttributes(m map[string]interface{}) {
if c.otel == nil {
c.otel = &model.OTel{}
}
c.otel.Attributes = m
}
// SetOTelSpanKind sets the provided SpanKind.
func (c *SpanContext) SetOTelSpanKind(spanKind string) {
if c.otel == nil {
c.otel = &model.OTel{}
}
c.otel.SpanKind = spanKind
}
// SetLabel sets a label in the context.
//
// Invalid characters ('.', '*', and '"') in the key will be replaced with
// underscores.
//
// If the value is numerical or boolean, then it will be sent to the server
// as a JSON number or boolean; otherwise it will converted to a string, using
// `fmt.Sprint` if necessary. String values longer than 1024 characters will
// be truncated.
func (c *SpanContext) SetLabel(key string, value interface{}) {
// Note that we do not attempt to de-duplicate the keys.
// This is OK, since json.Unmarshal will always take the
// final instance.
c.model.Tags = append(c.model.Tags, model.IfaceMapItem{
Key: cleanLabelKey(key),
Value: makeLabelValue(value),
})
}
// SetDatabase sets the span context for database-related operations.
func (c *SpanContext) SetDatabase(db DatabaseSpanContext) {
c.database = model.DatabaseSpanContext{
Instance: truncateString(db.Instance),
Statement: truncateLongString(db.Statement),
Type: truncateString(db.Type),
User: truncateString(db.User),
}
c.model.Database = &c.database
}
// SetDatabaseRowsAffected records the number of rows affected by
// a database operation.
func (c *SpanContext) SetDatabaseRowsAffected(n int64) {
c.databaseRowsAffected = n
c.database.RowsAffected = &c.databaseRowsAffected
}
// SetHTTPRequest sets the details of the HTTP request in the context.
//
// This function relates to client requests. If the request URL contains
// user info, it will be removed and excluded from the stored URL.
//
// SetHTTPRequest makes implicit calls to SetDestinationAddress and
// SetDestinationService, using details from req.URL.
func (c *SpanContext) SetHTTPRequest(req *http.Request) {
if req.URL == nil {
return
}
// Clone the URL, since callers of http.RoundTrip may mutate the
// Request after the response body is closed.
clonedURL := *req.URL
if req.URL.User != nil {
clonedURLUser := *req.URL.User
clonedURL.User = &clonedURLUser
}
c.http.URL = &clonedURL
c.model.HTTP = &c.http
addr, port := apmhttputil.DestinationAddr(req)
c.SetDestinationAddress(addr, port)
destinationServiceURL := url.URL{Scheme: req.URL.Scheme, Host: req.URL.Host}
destinationServiceResource := destinationServiceURL.Host
if port != 0 && port == apmhttputil.SchemeDefaultPort(req.URL.Scheme) {
var hasDefaultPort bool
if n := len(destinationServiceURL.Host); n > 0 && destinationServiceURL.Host[n-1] != ']' {
if i := strings.LastIndexByte(destinationServiceURL.Host, ':'); i != -1 {
// Remove the default port from destination.service.name.
destinationServiceURL.Host = destinationServiceURL.Host[:i]
hasDefaultPort = true
}
}
if !hasDefaultPort {
// Add the default port to destination.service.resource.
destinationServiceResource = fmt.Sprintf("%s:%d", destinationServiceResource, port)
}
}
c.SetDestinationService(DestinationServiceSpanContext{
Name: destinationServiceURL.String(),
Resource: destinationServiceResource,
})
c.SetServiceTarget(ServiceTargetSpanContext{
Name: destinationServiceResource,
})
}
// SetHTTPStatusCode records the HTTP response status code.
//
// If, when the transaction ends, its Outcome field has not
// been explicitly set, it will be set based on the status code:
// "success" if statusCode < 400, and "failure" otherwise.
func (c *SpanContext) SetHTTPStatusCode(statusCode int) {
c.http.StatusCode = statusCode
c.model.HTTP = &c.http
}
// SetDestinationAddress sets the destination address and port in the context.
//
// SetDestinationAddress has no effect when called with an empty addr.
func (c *SpanContext) SetDestinationAddress(addr string, port int) {
if addr != "" {
c.destination.Address = truncateString(addr)
c.destination.Port = port
c.model.Destination = &c.destination
}
}
// SetMessage sets the message info in the context.
//
// message.Name is required. If it is empty, then SetMessage is a no-op.
func (c *SpanContext) SetMessage(message MessageSpanContext) {
if message.QueueName == "" {
return
}
c.message.Queue = &model.MessageQueueSpanContext{
Name: truncateString(message.QueueName),
}
c.model.Message = &c.message
}
// SetDestinationService sets the destination service info in the context.
//
// Both service.Name and service.Resource are required. If either is empty,
// then SetDestinationService is a no-op.
//
// Deprecated: use SetServiceTarget
func (c *SpanContext) SetDestinationService(service DestinationServiceSpanContext) {
c.setDestinationServiceCalled = true
if service.Resource == "" {
return
}
c.destinationService.Name = truncateString(service.Name)
c.destinationService.Resource = truncateString(service.Resource)
c.destination.Service = &c.destinationService
c.model.Destination = &c.destination
}
// SetServiceTarget sets the service target info in the context.
func (c *SpanContext) SetServiceTarget(service ServiceTargetSpanContext) {
c.setServiceTargetCalled = true
c.serviceTarget.Type = truncateString(service.Type)
c.serviceTarget.Name = truncateString(service.Name)
c.service.Target = &c.serviceTarget
c.model.Service = &c.service
}
// SetDestinationCloud sets the destination cloud info in the context.
func (c *SpanContext) SetDestinationCloud(cloud DestinationCloudSpanContext) {
c.destinationCloud.Region = truncateString(cloud.Region)
c.destination.Cloud = &c.destinationCloud
c.model.Destination = &c.destination
}
// outcome returns the outcome to assign to the associated span, based on
// context (e.g. HTTP status code).
func (c *SpanContext) outcome() string {
if c.http.StatusCode != 0 {
if c.http.StatusCode < 400 {
return "success"
}
return "failure"
}
return ""
}