context.go (195 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"
"go.elastic.co/apm/v2/internal/apmhttputil"
"go.elastic.co/apm/v2/internal/wildcard"
"go.elastic.co/apm/v2/model"
)
// Context provides methods for setting transaction and error context.
//
// NOTE this is entirely unrelated to the standard library's context.Context.
type Context struct {
model model.Context
request model.Request
httpRequest *http.Request
requestBody model.RequestBody
requestSocket model.RequestSocket
response model.Response
user model.User
service model.Service
serviceFramework model.Framework
otel *model.OTel
captureHeaders bool
captureBodyMask CaptureBodyMode
sanitizedFieldNames wildcard.Matchers
}
func (c *Context) build() *model.Context {
switch {
case c.model.Request != nil:
case c.model.Response != nil:
case c.model.User != nil:
case c.model.Service != nil:
case len(c.model.Tags) != 0:
case len(c.model.Custom) != 0:
default:
return nil
}
if len(c.sanitizedFieldNames) != 0 {
if c.model.Request != nil {
sanitizeRequest(c.model.Request, c.sanitizedFieldNames)
}
if c.model.Response != nil {
sanitizeResponse(c.model.Response, c.sanitizedFieldNames)
}
}
return &c.model
}
func (c *Context) reset() {
*c = Context{
model: model.Context{
Custom: c.model.Custom[:0],
Tags: c.model.Tags[:0],
},
captureBodyMask: c.captureBodyMask,
request: model.Request{
Headers: c.request.Headers[:0],
},
response: model.Response{
Headers: c.response.Headers[:0],
},
}
}
// SetOTelAttributes sets the provided OpenTelemetry attributes.
func (c *Context) SetOTelAttributes(m map[string]interface{}) {
if c.otel == nil {
c.otel = &model.OTel{}
}
c.otel.Attributes = m
}
// SetOTelSpanKind sets the provided SpanKind.
func (c *Context) 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 *Context) 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),
})
}
// SetCustom sets custom context.
//
// Invalid characters ('.', '*', and '"') in the key will be
// replaced with an underscore. The value may be any JSON-encodable
// value.
func (c *Context) SetCustom(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.Custom = append(c.model.Custom, model.IfaceMapItem{
Key: cleanLabelKey(key),
Value: value,
})
}
// SetFramework sets the framework name and version in the context.
//
// This is used for identifying the framework in which the context
// was created, such as Gin or Echo.
//
// If the name is empty, this is a no-op. If version is empty, then
// it will be set to "unspecified".
func (c *Context) SetFramework(name, version string) {
if name == "" {
return
}
if version == "" {
// Framework version is required.
version = "unspecified"
}
c.serviceFramework = model.Framework{
Name: truncateString(name),
Version: truncateString(version),
}
c.service.Framework = &c.serviceFramework
c.model.Service = &c.service
}
// SetHTTPRequest sets details of the HTTP request in the context.
//
// This function relates to server-side requests. Various proxy
// forwarding headers are taken into account to reconstruct the URL,
// and determining the client address.
//
// If the request URL contains user info, it will be removed and
// excluded from the URL's "full" field.
//
// If the request contains HTTP Basic Authentication, the username
// from that will be recorded in the context. Otherwise, if the
// request contains user info in the URL (i.e. a client-side URL),
// that will be used. An explicit call to SetUsername always takes
// precedence.
func (c *Context) SetHTTPRequest(req *http.Request) {
// Special cases to avoid calling into fmt.Sprintf in most cases.
var httpVersion string
switch {
case req.ProtoMajor == 1 && req.ProtoMinor == 1:
httpVersion = "1.1"
case req.ProtoMajor == 2 && req.ProtoMinor == 0:
httpVersion = "2.0"
default:
httpVersion = fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor)
}
c.httpRequest = req
c.request = model.Request{
Body: c.request.Body,
URL: apmhttputil.RequestURL(req),
Method: truncateString(req.Method),
HTTPVersion: httpVersion,
Cookies: req.Cookies(),
}
c.model.Request = &c.request
if c.captureHeaders {
for k, values := range req.Header {
if k == "Cookie" {
// We capture cookies in the request structure.
continue
}
c.request.Headers = append(c.request.Headers, model.Header{
Key: k, Values: values,
})
}
}
c.requestSocket = model.RequestSocket{
RemoteAddress: apmhttputil.RemoteAddr(req),
}
if c.requestSocket != (model.RequestSocket{}) {
c.request.Socket = &c.requestSocket
}
if c.model.User == nil {
username, _, ok := req.BasicAuth()
if !ok && req.URL.User != nil {
username = req.URL.User.Username()
}
c.user.Username = truncateString(username)
if c.user.Username != "" {
c.model.User = &c.user
}
}
}
// SetHTTPRequestBody sets the request body in context given a (possibly nil)
// BodyCapturer returned by Tracer.CaptureHTTPRequestBody.
func (c *Context) SetHTTPRequestBody(bc *BodyCapturer) {
if bc == nil || bc.captureBody&c.captureBodyMask == 0 {
return
}
if bc.setContext(&c.requestBody, c.httpRequest) {
c.request.Body = &c.requestBody
}
}
// SetHTTPResponseHeaders sets the HTTP response headers in the context.
func (c *Context) SetHTTPResponseHeaders(h http.Header) {
if !c.captureHeaders {
return
}
for k, values := range h {
c.response.Headers = append(c.response.Headers, model.Header{
Key: k, Values: values,
})
}
if len(c.response.Headers) != 0 {
c.model.Response = &c.response
}
}
// 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 < 500, and "failure" otherwise.
func (c *Context) SetHTTPStatusCode(statusCode int) {
c.response.StatusCode = statusCode
c.model.Response = &c.response
}
// SetUserID sets the ID of the authenticated user.
func (c *Context) SetUserID(id string) {
c.user.ID = truncateString(id)
if c.user.ID != "" {
c.model.User = &c.user
}
}
// SetUserEmail sets the email for the authenticated user.
func (c *Context) SetUserEmail(email string) {
c.user.Email = truncateString(email)
if c.user.Email != "" {
c.model.User = &c.user
}
}
// SetUsername sets the username of the authenticated user.
func (c *Context) SetUsername(username string) {
c.user.Username = truncateString(username)
if c.user.Username != "" {
c.model.User = &c.user
}
}
// outcome returns the outcome to assign to the associated transaction,
// based on context (e.g. HTTP status code).
func (c *Context) outcome() string {
if c.response.StatusCode != 0 {
if c.response.StatusCode < 500 {
return "success"
}
return "failure"
}
return ""
}