github.com/waldiirawan/apm-agent-go/v2@v2.2.2/context.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package apm // import "github.com/waldiirawan/apm-agent-go/v2"
    19  
    20  import (
    21  	"fmt"
    22  	"net/http"
    23  
    24  	"github.com/waldiirawan/apm-agent-go/v2/internal/apmhttputil"
    25  	"github.com/waldiirawan/apm-agent-go/v2/internal/wildcard"
    26  	"github.com/waldiirawan/apm-agent-go/v2/model"
    27  )
    28  
    29  // Context provides methods for setting transaction and error context.
    30  //
    31  // NOTE this is entirely unrelated to the standard library's context.Context.
    32  type Context struct {
    33  	model               model.Context
    34  	request             model.Request
    35  	httpRequest         *http.Request
    36  	requestBody         model.RequestBody
    37  	requestSocket       model.RequestSocket
    38  	response            model.Response
    39  	user                model.User
    40  	service             model.Service
    41  	serviceFramework    model.Framework
    42  	otel                *model.OTel
    43  	captureHeaders      bool
    44  	captureBodyMask     CaptureBodyMode
    45  	sanitizedFieldNames wildcard.Matchers
    46  }
    47  
    48  func (c *Context) build() *model.Context {
    49  	switch {
    50  	case c.model.Request != nil:
    51  	case c.model.Response != nil:
    52  	case c.model.User != nil:
    53  	case c.model.Service != nil:
    54  	case len(c.model.Tags) != 0:
    55  	case len(c.model.Custom) != 0:
    56  	default:
    57  		return nil
    58  	}
    59  	if len(c.sanitizedFieldNames) != 0 {
    60  		if c.model.Request != nil {
    61  			sanitizeRequest(c.model.Request, c.sanitizedFieldNames)
    62  		}
    63  		if c.model.Response != nil {
    64  			sanitizeResponse(c.model.Response, c.sanitizedFieldNames)
    65  		}
    66  
    67  	}
    68  	return &c.model
    69  }
    70  
    71  func (c *Context) reset() {
    72  	*c = Context{
    73  		model: model.Context{
    74  			Custom: c.model.Custom[:0],
    75  			Tags:   c.model.Tags[:0],
    76  		},
    77  		captureBodyMask: c.captureBodyMask,
    78  		request: model.Request{
    79  			Headers: c.request.Headers[:0],
    80  		},
    81  		response: model.Response{
    82  			Headers: c.response.Headers[:0],
    83  		},
    84  	}
    85  }
    86  
    87  // SetOTelAttributes sets the provided OpenTelemetry attributes.
    88  func (c *Context) SetOTelAttributes(m map[string]interface{}) {
    89  	if c.otel == nil {
    90  		c.otel = &model.OTel{}
    91  	}
    92  	c.otel.Attributes = m
    93  }
    94  
    95  // SetOTelSpanKind sets the provided SpanKind.
    96  func (c *Context) SetOTelSpanKind(spanKind string) {
    97  	if c.otel == nil {
    98  		c.otel = &model.OTel{}
    99  	}
   100  	c.otel.SpanKind = spanKind
   101  }
   102  
   103  // SetLabel sets a label in the context.
   104  //
   105  // Invalid characters ('.', '*', and '"') in the key will be replaced with
   106  // underscores.
   107  //
   108  // If the value is numerical or boolean, then it will be sent to the server
   109  // as a JSON number or boolean; otherwise it will converted to a string, using
   110  // `fmt.Sprint` if necessary. String values longer than 1024 characters will
   111  // be truncated.
   112  func (c *Context) SetLabel(key string, value interface{}) {
   113  	// Note that we do not attempt to de-duplicate the keys.
   114  	// This is OK, since json.Unmarshal will always take the
   115  	// final instance.
   116  	c.model.Tags = append(c.model.Tags, model.IfaceMapItem{
   117  		Key:   cleanLabelKey(key),
   118  		Value: makeLabelValue(value),
   119  	})
   120  }
   121  
   122  // SetCustom sets custom context.
   123  //
   124  // Invalid characters ('.', '*', and '"') in the key will be
   125  // replaced with an underscore. The value may be any JSON-encodable
   126  // value.
   127  func (c *Context) SetCustom(key string, value interface{}) {
   128  	// Note that we do not attempt to de-duplicate the keys.
   129  	// This is OK, since json.Unmarshal will always take the
   130  	// final instance.
   131  	c.model.Custom = append(c.model.Custom, model.IfaceMapItem{
   132  		Key:   cleanLabelKey(key),
   133  		Value: value,
   134  	})
   135  }
   136  
   137  // SetFramework sets the framework name and version in the context.
   138  //
   139  // This is used for identifying the framework in which the context
   140  // was created, such as Gin or Echo.
   141  //
   142  // If the name is empty, this is a no-op. If version is empty, then
   143  // it will be set to "unspecified".
   144  func (c *Context) SetFramework(name, version string) {
   145  	if name == "" {
   146  		return
   147  	}
   148  	if version == "" {
   149  		// Framework version is required.
   150  		version = "unspecified"
   151  	}
   152  	c.serviceFramework = model.Framework{
   153  		Name:    truncateString(name),
   154  		Version: truncateString(version),
   155  	}
   156  	c.service.Framework = &c.serviceFramework
   157  	c.model.Service = &c.service
   158  }
   159  
   160  // SetHTTPRequest sets details of the HTTP request in the context.
   161  //
   162  // This function relates to server-side requests. Various proxy
   163  // forwarding headers are taken into account to reconstruct the URL,
   164  // and determining the client address.
   165  //
   166  // If the request URL contains user info, it will be removed and
   167  // excluded from the URL's "full" field.
   168  //
   169  // If the request contains HTTP Basic Authentication, the username
   170  // from that will be recorded in the context. Otherwise, if the
   171  // request contains user info in the URL (i.e. a client-side URL),
   172  // that will be used. An explicit call to SetUsername always takes
   173  // precedence.
   174  func (c *Context) SetHTTPRequest(req *http.Request) {
   175  	// Special cases to avoid calling into fmt.Sprintf in most cases.
   176  	var httpVersion string
   177  	switch {
   178  	case req.ProtoMajor == 1 && req.ProtoMinor == 1:
   179  		httpVersion = "1.1"
   180  	case req.ProtoMajor == 2 && req.ProtoMinor == 0:
   181  		httpVersion = "2.0"
   182  	default:
   183  		httpVersion = fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor)
   184  	}
   185  
   186  	c.httpRequest = req
   187  
   188  	c.request = model.Request{
   189  		Body:        c.request.Body,
   190  		URL:         apmhttputil.RequestURL(req),
   191  		Method:      truncateString(req.Method),
   192  		HTTPVersion: httpVersion,
   193  		Cookies:     req.Cookies(),
   194  	}
   195  	c.model.Request = &c.request
   196  
   197  	if c.captureHeaders {
   198  		for k, values := range req.Header {
   199  			if k == "Cookie" {
   200  				// We capture cookies in the request structure.
   201  				continue
   202  			}
   203  			c.request.Headers = append(c.request.Headers, model.Header{
   204  				Key: k, Values: values,
   205  			})
   206  		}
   207  	}
   208  
   209  	c.requestSocket = model.RequestSocket{
   210  		RemoteAddress: apmhttputil.RemoteAddr(req),
   211  	}
   212  	if c.requestSocket != (model.RequestSocket{}) {
   213  		c.request.Socket = &c.requestSocket
   214  	}
   215  
   216  	if c.model.User == nil {
   217  		username, _, ok := req.BasicAuth()
   218  		if !ok && req.URL.User != nil {
   219  			username = req.URL.User.Username()
   220  		}
   221  		c.user.Username = truncateString(username)
   222  		if c.user.Username != "" {
   223  			c.model.User = &c.user
   224  		}
   225  	}
   226  }
   227  
   228  // SetHTTPRequestBody sets the request body in context given a (possibly nil)
   229  // BodyCapturer returned by Tracer.CaptureHTTPRequestBody.
   230  func (c *Context) SetHTTPRequestBody(bc *BodyCapturer) {
   231  	if bc == nil || bc.captureBody&c.captureBodyMask == 0 {
   232  		return
   233  	}
   234  	if bc.setContext(&c.requestBody, c.httpRequest) {
   235  		c.request.Body = &c.requestBody
   236  	}
   237  }
   238  
   239  // SetHTTPResponseHeaders sets the HTTP response headers in the context.
   240  func (c *Context) SetHTTPResponseHeaders(h http.Header) {
   241  	if !c.captureHeaders {
   242  		return
   243  	}
   244  	for k, values := range h {
   245  		c.response.Headers = append(c.response.Headers, model.Header{
   246  			Key: k, Values: values,
   247  		})
   248  	}
   249  	if len(c.response.Headers) != 0 {
   250  		c.model.Response = &c.response
   251  	}
   252  }
   253  
   254  // SetHTTPStatusCode records the HTTP response status code.
   255  //
   256  // If, when the transaction ends, its Outcome field has not
   257  // been explicitly set, it will be set based on the status code:
   258  // "success" if statusCode < 500, and "failure" otherwise.
   259  func (c *Context) SetHTTPStatusCode(statusCode int) {
   260  	c.response.StatusCode = statusCode
   261  	c.model.Response = &c.response
   262  }
   263  
   264  // SetUserID sets the ID of the authenticated user.
   265  func (c *Context) SetUserID(id string) {
   266  	c.user.ID = truncateString(id)
   267  	if c.user.ID != "" {
   268  		c.model.User = &c.user
   269  	}
   270  }
   271  
   272  // SetUserEmail sets the email for the authenticated user.
   273  func (c *Context) SetUserEmail(email string) {
   274  	c.user.Email = truncateString(email)
   275  	if c.user.Email != "" {
   276  		c.model.User = &c.user
   277  	}
   278  }
   279  
   280  // SetUsername sets the username of the authenticated user.
   281  func (c *Context) SetUsername(username string) {
   282  	c.user.Username = truncateString(username)
   283  	if c.user.Username != "" {
   284  		c.model.User = &c.user
   285  	}
   286  }
   287  
   288  // outcome returns the outcome to assign to the associated transaction,
   289  // based on context (e.g. HTTP status code).
   290  func (c *Context) outcome() string {
   291  	if c.response.StatusCode != 0 {
   292  		if c.response.StatusCode < 500 {
   293  			return "success"
   294  		}
   295  		return "failure"
   296  	}
   297  	return ""
   298  }