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 }