gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }