http.go (11906B)
1 // Copyright The OpenTelemetry Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package internal // import "go.opentelemetry.io/otel/semconv/internal/v2" 16 17 import ( 18 "fmt" 19 "net/http" 20 "strings" 21 22 "go.opentelemetry.io/otel/attribute" 23 "go.opentelemetry.io/otel/codes" 24 ) 25 26 // HTTPConv are the HTTP semantic convention attributes defined for a version 27 // of the OpenTelemetry specification. 28 type HTTPConv struct { 29 NetConv *NetConv 30 31 EnduserIDKey attribute.Key 32 HTTPClientIPKey attribute.Key 33 HTTPFlavorKey attribute.Key 34 HTTPMethodKey attribute.Key 35 HTTPRequestContentLengthKey attribute.Key 36 HTTPResponseContentLengthKey attribute.Key 37 HTTPRouteKey attribute.Key 38 HTTPSchemeHTTP attribute.KeyValue 39 HTTPSchemeHTTPS attribute.KeyValue 40 HTTPStatusCodeKey attribute.Key 41 HTTPTargetKey attribute.Key 42 HTTPURLKey attribute.Key 43 HTTPUserAgentKey attribute.Key 44 } 45 46 // ClientResponse returns attributes for an HTTP response received by a client 47 // from a server. The following attributes are returned if the related values 48 // are defined in resp: "http.status.code", "http.response_content_length". 49 // 50 // This does not add all OpenTelemetry required attributes for an HTTP event, 51 // it assumes ClientRequest was used to create the span with a complete set of 52 // attributes. If a complete set of attributes can be generated using the 53 // request contained in resp. For example: 54 // 55 // append(ClientResponse(resp), ClientRequest(resp.Request)...) 56 func (c *HTTPConv) ClientResponse(resp *http.Response) []attribute.KeyValue { 57 var n int 58 if resp.StatusCode > 0 { 59 n++ 60 } 61 if resp.ContentLength > 0 { 62 n++ 63 } 64 65 attrs := make([]attribute.KeyValue, 0, n) 66 if resp.StatusCode > 0 { 67 attrs = append(attrs, c.HTTPStatusCodeKey.Int(resp.StatusCode)) 68 } 69 if resp.ContentLength > 0 { 70 attrs = append(attrs, c.HTTPResponseContentLengthKey.Int(int(resp.ContentLength))) 71 } 72 return attrs 73 } 74 75 // ClientRequest returns attributes for an HTTP request made by a client. The 76 // following attributes are always returned: "http.url", "http.flavor", 77 // "http.method", "net.peer.name". The following attributes are returned if the 78 // related values are defined in req: "net.peer.port", "http.user_agent", 79 // "http.request_content_length", "enduser.id". 80 func (c *HTTPConv) ClientRequest(req *http.Request) []attribute.KeyValue { 81 n := 3 // URL, peer name, proto, and method. 82 var h string 83 if req.URL != nil { 84 h = req.URL.Host 85 } 86 peer, p := firstHostPort(h, req.Header.Get("Host")) 87 port := requiredHTTPPort(req.URL != nil && req.URL.Scheme == "https", p) 88 if port > 0 { 89 n++ 90 } 91 useragent := req.UserAgent() 92 if useragent != "" { 93 n++ 94 } 95 if req.ContentLength > 0 { 96 n++ 97 } 98 userID, _, hasUserID := req.BasicAuth() 99 if hasUserID { 100 n++ 101 } 102 attrs := make([]attribute.KeyValue, 0, n) 103 104 attrs = append(attrs, c.method(req.Method)) 105 attrs = append(attrs, c.proto(req.Proto)) 106 107 var u string 108 if req.URL != nil { 109 // Remove any username/password info that may be in the URL. 110 userinfo := req.URL.User 111 req.URL.User = nil 112 u = req.URL.String() 113 // Restore any username/password info that was removed. 114 req.URL.User = userinfo 115 } 116 attrs = append(attrs, c.HTTPURLKey.String(u)) 117 118 attrs = append(attrs, c.NetConv.PeerName(peer)) 119 if port > 0 { 120 attrs = append(attrs, c.NetConv.PeerPort(port)) 121 } 122 123 if useragent != "" { 124 attrs = append(attrs, c.HTTPUserAgentKey.String(useragent)) 125 } 126 127 if l := req.ContentLength; l > 0 { 128 attrs = append(attrs, c.HTTPRequestContentLengthKey.Int64(l)) 129 } 130 131 if hasUserID { 132 attrs = append(attrs, c.EnduserIDKey.String(userID)) 133 } 134 135 return attrs 136 } 137 138 // ServerRequest returns attributes for an HTTP request received by a server. 139 // 140 // The server must be the primary server name if it is known. For example this 141 // would be the ServerName directive 142 // (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache 143 // server, and the server_name directive 144 // (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an 145 // nginx server. More generically, the primary server name would be the host 146 // header value that matches the default virtual host of an HTTP server. It 147 // should include the host identifier and if a port is used to route to the 148 // server that port identifier should be included as an appropriate port 149 // suffix. 150 // 151 // If the primary server name is not known, server should be an empty string. 152 // The req Host will be used to determine the server instead. 153 // 154 // The following attributes are always returned: "http.method", "http.scheme", 155 // "http.flavor", "http.target", "net.host.name". The following attributes are 156 // returned if they related values are defined in req: "net.host.port", 157 // "net.sock.peer.addr", "net.sock.peer.port", "http.user_agent", "enduser.id", 158 // "http.client_ip". 159 func (c *HTTPConv) ServerRequest(server string, req *http.Request) []attribute.KeyValue { 160 // TODO: This currently does not add the specification required 161 // `http.target` attribute. It has too high of a cardinality to safely be 162 // added. An alternate should be added, or this comment removed, when it is 163 // addressed by the specification. If it is ultimately decided to continue 164 // not including the attribute, the HTTPTargetKey field of the HTTPConv 165 // should be removed as well. 166 167 n := 4 // Method, scheme, proto, and host name. 168 var host string 169 var p int 170 if server == "" { 171 host, p = splitHostPort(req.Host) 172 } else { 173 // Prioritize the primary server name. 174 host, p = splitHostPort(server) 175 if p < 0 { 176 _, p = splitHostPort(req.Host) 177 } 178 } 179 hostPort := requiredHTTPPort(req.TLS != nil, p) 180 if hostPort > 0 { 181 n++ 182 } 183 peer, peerPort := splitHostPort(req.RemoteAddr) 184 if peer != "" { 185 n++ 186 if peerPort > 0 { 187 n++ 188 } 189 } 190 useragent := req.UserAgent() 191 if useragent != "" { 192 n++ 193 } 194 userID, _, hasUserID := req.BasicAuth() 195 if hasUserID { 196 n++ 197 } 198 clientIP := serverClientIP(req.Header.Get("X-Forwarded-For")) 199 if clientIP != "" { 200 n++ 201 } 202 attrs := make([]attribute.KeyValue, 0, n) 203 204 attrs = append(attrs, c.method(req.Method)) 205 attrs = append(attrs, c.scheme(req.TLS != nil)) 206 attrs = append(attrs, c.proto(req.Proto)) 207 attrs = append(attrs, c.NetConv.HostName(host)) 208 209 if hostPort > 0 { 210 attrs = append(attrs, c.NetConv.HostPort(hostPort)) 211 } 212 213 if peer != "" { 214 // The Go HTTP server sets RemoteAddr to "IP:port", this will not be a 215 // file-path that would be interpreted with a sock family. 216 attrs = append(attrs, c.NetConv.SockPeerAddr(peer)) 217 if peerPort > 0 { 218 attrs = append(attrs, c.NetConv.SockPeerPort(peerPort)) 219 } 220 } 221 222 if useragent != "" { 223 attrs = append(attrs, c.HTTPUserAgentKey.String(useragent)) 224 } 225 226 if hasUserID { 227 attrs = append(attrs, c.EnduserIDKey.String(userID)) 228 } 229 230 if clientIP != "" { 231 attrs = append(attrs, c.HTTPClientIPKey.String(clientIP)) 232 } 233 234 return attrs 235 } 236 237 func (c *HTTPConv) method(method string) attribute.KeyValue { 238 if method == "" { 239 return c.HTTPMethodKey.String(http.MethodGet) 240 } 241 return c.HTTPMethodKey.String(method) 242 } 243 244 func (c *HTTPConv) scheme(https bool) attribute.KeyValue { // nolint:revive 245 if https { 246 return c.HTTPSchemeHTTPS 247 } 248 return c.HTTPSchemeHTTP 249 } 250 251 func (c *HTTPConv) proto(proto string) attribute.KeyValue { 252 switch proto { 253 case "HTTP/1.0": 254 return c.HTTPFlavorKey.String("1.0") 255 case "HTTP/1.1": 256 return c.HTTPFlavorKey.String("1.1") 257 case "HTTP/2": 258 return c.HTTPFlavorKey.String("2.0") 259 case "HTTP/3": 260 return c.HTTPFlavorKey.String("3.0") 261 default: 262 return c.HTTPFlavorKey.String(proto) 263 } 264 } 265 266 func serverClientIP(xForwardedFor string) string { 267 if idx := strings.Index(xForwardedFor, ","); idx >= 0 { 268 xForwardedFor = xForwardedFor[:idx] 269 } 270 return xForwardedFor 271 } 272 273 func requiredHTTPPort(https bool, port int) int { // nolint:revive 274 if https { 275 if port > 0 && port != 443 { 276 return port 277 } 278 } else { 279 if port > 0 && port != 80 { 280 return port 281 } 282 } 283 return -1 284 } 285 286 // Return the request host and port from the first non-empty source. 287 func firstHostPort(source ...string) (host string, port int) { 288 for _, hostport := range source { 289 host, port = splitHostPort(hostport) 290 if host != "" || port > 0 { 291 break 292 } 293 } 294 return 295 } 296 297 // RequestHeader returns the contents of h as OpenTelemetry attributes. 298 func (c *HTTPConv) RequestHeader(h http.Header) []attribute.KeyValue { 299 return c.header("http.request.header", h) 300 } 301 302 // ResponseHeader returns the contents of h as OpenTelemetry attributes. 303 func (c *HTTPConv) ResponseHeader(h http.Header) []attribute.KeyValue { 304 return c.header("http.response.header", h) 305 } 306 307 func (c *HTTPConv) header(prefix string, h http.Header) []attribute.KeyValue { 308 key := func(k string) attribute.Key { 309 k = strings.ToLower(k) 310 k = strings.ReplaceAll(k, "-", "_") 311 k = fmt.Sprintf("%s.%s", prefix, k) 312 return attribute.Key(k) 313 } 314 315 attrs := make([]attribute.KeyValue, 0, len(h)) 316 for k, v := range h { 317 attrs = append(attrs, key(k).StringSlice(v)) 318 } 319 return attrs 320 } 321 322 // ClientStatus returns a span status code and message for an HTTP status code 323 // value received by a client. 324 func (c *HTTPConv) ClientStatus(code int) (codes.Code, string) { 325 stat, valid := validateHTTPStatusCode(code) 326 if !valid { 327 return stat, fmt.Sprintf("Invalid HTTP status code %d", code) 328 } 329 return stat, "" 330 } 331 332 // ServerStatus returns a span status code and message for an HTTP status code 333 // value returned by a server. Status codes in the 400-499 range are not 334 // returned as errors. 335 func (c *HTTPConv) ServerStatus(code int) (codes.Code, string) { 336 stat, valid := validateHTTPStatusCode(code) 337 if !valid { 338 return stat, fmt.Sprintf("Invalid HTTP status code %d", code) 339 } 340 341 if code/100 == 4 { 342 return codes.Unset, "" 343 } 344 return stat, "" 345 } 346 347 type codeRange struct { 348 fromInclusive int 349 toInclusive int 350 } 351 352 func (r codeRange) contains(code int) bool { 353 return r.fromInclusive <= code && code <= r.toInclusive 354 } 355 356 var validRangesPerCategory = map[int][]codeRange{ 357 1: { 358 {http.StatusContinue, http.StatusEarlyHints}, 359 }, 360 2: { 361 {http.StatusOK, http.StatusAlreadyReported}, 362 {http.StatusIMUsed, http.StatusIMUsed}, 363 }, 364 3: { 365 {http.StatusMultipleChoices, http.StatusUseProxy}, 366 {http.StatusTemporaryRedirect, http.StatusPermanentRedirect}, 367 }, 368 4: { 369 {http.StatusBadRequest, http.StatusTeapot}, // yes, teapot is so useful… 370 {http.StatusMisdirectedRequest, http.StatusUpgradeRequired}, 371 {http.StatusPreconditionRequired, http.StatusTooManyRequests}, 372 {http.StatusRequestHeaderFieldsTooLarge, http.StatusRequestHeaderFieldsTooLarge}, 373 {http.StatusUnavailableForLegalReasons, http.StatusUnavailableForLegalReasons}, 374 }, 375 5: { 376 {http.StatusInternalServerError, http.StatusLoopDetected}, 377 {http.StatusNotExtended, http.StatusNetworkAuthenticationRequired}, 378 }, 379 } 380 381 // validateHTTPStatusCode validates the HTTP status code and returns 382 // corresponding span status code. If the `code` is not a valid HTTP status 383 // code, returns span status Error and false. 384 func validateHTTPStatusCode(code int) (codes.Code, bool) { 385 category := code / 100 386 ranges, ok := validRangesPerCategory[category] 387 if !ok { 388 return codes.Error, false 389 } 390 ok = false 391 for _, crange := range ranges { 392 ok = crange.contains(code) 393 if ok { 394 break 395 } 396 } 397 if !ok { 398 return codes.Error, false 399 } 400 if category > 0 && category < 4 { 401 return codes.Unset, true 402 } 403 return codes.Error, true 404 }