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 18package apm 19 20import ( 21 "fmt" 22 "net/http" 23 24 "go.elastic.co/apm/internal/apmhttputil" 25 "go.elastic.co/apm/model" 26) 27 28// Context provides methods for setting transaction and error context. 29// 30// NOTE this is entirely unrelated to the standard library's context.Context. 31type Context struct { 32 model model.Context 33 request model.Request 34 requestBody model.RequestBody 35 requestSocket model.RequestSocket 36 response model.Response 37 user model.User 38 service model.Service 39 serviceFramework model.Framework 40 captureHeaders bool 41 captureBodyMask CaptureBodyMode 42} 43 44func (c *Context) build() *model.Context { 45 switch { 46 case c.model.Request != nil: 47 case c.model.Response != nil: 48 case c.model.User != nil: 49 case c.model.Service != nil: 50 case len(c.model.Tags) != 0: 51 case len(c.model.Custom) != 0: 52 default: 53 return nil 54 } 55 return &c.model 56} 57 58func (c *Context) reset() { 59 *c = Context{ 60 model: model.Context{ 61 Custom: c.model.Custom[:0], 62 Tags: c.model.Tags[:0], 63 }, 64 captureBodyMask: c.captureBodyMask, 65 request: model.Request{ 66 Headers: c.request.Headers[:0], 67 }, 68 response: model.Response{ 69 Headers: c.response.Headers[:0], 70 }, 71 } 72} 73 74// SetTag sets a tag in the context. Invalid characters 75// ('.', '*', and '"') in the key will be replaced with 76// an underscore. 77func (c *Context) SetTag(key, value string) { 78 // Note that we do not attempt to de-duplicate the keys. 79 // This is OK, since json.Unmarshal will always take the 80 // final instance. 81 c.model.Tags = append(c.model.Tags, model.StringMapItem{ 82 Key: cleanTagKey(key), 83 Value: truncateString(value), 84 }) 85} 86 87// SetCustom sets custom context. 88// 89// Invalid characters ('.', '*', and '"') in the key will be 90// replaced with an underscore. The value may be any JSON-encodable 91// value. 92func (c *Context) SetCustom(key string, value interface{}) { 93 // Note that we do not attempt to de-duplicate the keys. 94 // This is OK, since json.Unmarshal will always take the 95 // final instance. 96 c.model.Custom = append(c.model.Custom, model.IfaceMapItem{ 97 Key: cleanTagKey(key), 98 Value: value, 99 }) 100} 101 102// SetFramework sets the framework name and version in the context. 103// 104// This is used for identifying the framework in which the context 105// was created, such as Gin or Echo. 106// 107// If the name is empty, this is a no-op. If version is empty, then 108// it will be set to "unspecified". 109func (c *Context) SetFramework(name, version string) { 110 if name == "" { 111 return 112 } 113 if version == "" { 114 // Framework version is required. 115 version = "unspecified" 116 } 117 c.serviceFramework = model.Framework{ 118 Name: truncateString(name), 119 Version: truncateString(version), 120 } 121 c.service.Framework = &c.serviceFramework 122 c.model.Service = &c.service 123} 124 125// SetHTTPRequest sets details of the HTTP request in the context. 126// 127// This function relates to server-side requests. Various proxy 128// forwarding headers are taken into account to reconstruct the URL, 129// and determining the client address. 130// 131// If the request URL contains user info, it will be removed and 132// excluded from the URL's "full" field. 133// 134// If the request contains HTTP Basic Authentication, the username 135// from that will be recorded in the context. Otherwise, if the 136// request contains user info in the URL (i.e. a client-side URL), 137// that will be used. 138func (c *Context) SetHTTPRequest(req *http.Request) { 139 // Special cases to avoid calling into fmt.Sprintf in most cases. 140 var httpVersion string 141 switch { 142 case req.ProtoMajor == 1 && req.ProtoMinor == 1: 143 httpVersion = "1.1" 144 case req.ProtoMajor == 2 && req.ProtoMinor == 0: 145 httpVersion = "2.0" 146 default: 147 httpVersion = fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor) 148 } 149 150 var forwarded *apmhttputil.ForwardedHeader 151 if fwd := req.Header.Get("Forwarded"); fwd != "" { 152 parsed := apmhttputil.ParseForwarded(fwd) 153 forwarded = &parsed 154 } 155 c.request = model.Request{ 156 Body: c.request.Body, 157 URL: apmhttputil.RequestURL(req, forwarded), 158 Method: truncateString(req.Method), 159 HTTPVersion: httpVersion, 160 Cookies: req.Cookies(), 161 } 162 c.model.Request = &c.request 163 164 if c.captureHeaders { 165 for k, values := range req.Header { 166 if k == "Cookie" { 167 // We capture cookies in the request structure. 168 continue 169 } 170 c.request.Headers = append(c.request.Headers, model.Header{ 171 Key: k, Values: values, 172 }) 173 } 174 } 175 176 c.requestSocket = model.RequestSocket{ 177 Encrypted: req.TLS != nil, 178 RemoteAddress: apmhttputil.RemoteAddr(req, forwarded), 179 } 180 if c.requestSocket != (model.RequestSocket{}) { 181 c.request.Socket = &c.requestSocket 182 } 183 184 username, _, ok := req.BasicAuth() 185 if !ok && req.URL.User != nil { 186 username = req.URL.User.Username() 187 } 188 c.user.Username = truncateString(username) 189 if c.user.Username != "" { 190 c.model.User = &c.user 191 } 192} 193 194// SetHTTPRequestBody sets the request body in context given a (possibly nil) 195// BodyCapturer returned by Tracer.CaptureHTTPRequestBody. 196func (c *Context) SetHTTPRequestBody(bc *BodyCapturer) { 197 if bc == nil || bc.captureBody&c.captureBodyMask == 0 { 198 return 199 } 200 if bc.setContext(&c.requestBody) { 201 c.request.Body = &c.requestBody 202 } 203} 204 205// SetHTTPResponseHeaders sets the HTTP response headers in the context. 206func (c *Context) SetHTTPResponseHeaders(h http.Header) { 207 if !c.captureHeaders { 208 return 209 } 210 for k, values := range h { 211 c.response.Headers = append(c.response.Headers, model.Header{ 212 Key: k, Values: values, 213 }) 214 } 215 if len(c.response.Headers) != 0 { 216 c.model.Response = &c.response 217 } 218} 219 220// SetHTTPStatusCode records the HTTP response status code. 221func (c *Context) SetHTTPStatusCode(statusCode int) { 222 c.response.StatusCode = statusCode 223 c.model.Response = &c.response 224} 225 226// SetUserID sets the ID of the authenticated user. 227func (c *Context) SetUserID(id string) { 228 c.user.ID = truncateString(id) 229 if c.user.ID != "" { 230 c.model.User = &c.user 231 } 232} 233 234// SetUserEmail sets the email for the authenticated user. 235func (c *Context) SetUserEmail(email string) { 236 c.user.Email = truncateString(email) 237 if c.user.Email != "" { 238 c.model.User = &c.user 239 } 240} 241 242// SetUsername sets the username of the authenticated user. 243func (c *Context) SetUsername(username string) { 244 c.user.Username = truncateString(username) 245 if c.user.Username != "" { 246 c.model.User = &c.user 247 } 248} 249