// Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package semconv // import "go.opentelemetry.io/otel/semconv/v1.7.0" import ( "fmt" "net" "net/http" "strconv" "strings" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" ) // HTTP scheme attributes. var ( HTTPSchemeHTTP = HTTPSchemeKey.String("http") HTTPSchemeHTTPS = HTTPSchemeKey.String("https") ) // NetAttributesFromHTTPRequest generates attributes of the net // namespace as specified by the OpenTelemetry specification for a // span. The network parameter is a string that net.Dial function // from standard library can understand. func NetAttributesFromHTTPRequest(network string, request *http.Request) []attribute.KeyValue { attrs := []attribute.KeyValue{} switch network { case "tcp", "tcp4", "tcp6": attrs = append(attrs, NetTransportTCP) case "udp", "udp4", "udp6": attrs = append(attrs, NetTransportUDP) case "ip", "ip4", "ip6": attrs = append(attrs, NetTransportIP) case "unix", "unixgram", "unixpacket": attrs = append(attrs, NetTransportUnix) default: attrs = append(attrs, NetTransportOther) } peerIP, peerName, peerPort := hostIPNamePort(request.RemoteAddr) if peerIP != "" { attrs = append(attrs, NetPeerIPKey.String(peerIP)) } if peerName != "" { attrs = append(attrs, NetPeerNameKey.String(peerName)) } if peerPort != 0 { attrs = append(attrs, NetPeerPortKey.Int(peerPort)) } hostIP, hostName, hostPort := "", "", 0 for _, someHost := range []string{request.Host, request.Header.Get("Host"), request.URL.Host} { hostIP, hostName, hostPort = hostIPNamePort(someHost) if hostIP != "" || hostName != "" || hostPort != 0 { break } } if hostIP != "" { attrs = append(attrs, NetHostIPKey.String(hostIP)) } if hostName != "" { attrs = append(attrs, NetHostNameKey.String(hostName)) } if hostPort != 0 { attrs = append(attrs, NetHostPortKey.Int(hostPort)) } return attrs } // hostIPNamePort extracts the IP address, name and (optional) port from hostWithPort. // It handles both IPv4 and IPv6 addresses. If the host portion is not recognized // as a valid IPv4 or IPv6 address, the `ip` result will be empty and the // host portion will instead be returned in `name`. func hostIPNamePort(hostWithPort string) (ip string, name string, port int) { var ( hostPart, portPart string parsedPort uint64 err error ) if hostPart, portPart, err = net.SplitHostPort(hostWithPort); err != nil { hostPart, portPart = hostWithPort, "" } if parsedIP := net.ParseIP(hostPart); parsedIP != nil { ip = parsedIP.String() } else { name = hostPart } if parsedPort, err = strconv.ParseUint(portPart, 10, 16); err == nil { port = int(parsedPort) } return } // EndUserAttributesFromHTTPRequest generates attributes of the // enduser namespace as specified by the OpenTelemetry specification // for a span. func EndUserAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { if username, _, ok := request.BasicAuth(); ok { return []attribute.KeyValue{EnduserIDKey.String(username)} } return nil } // HTTPClientAttributesFromHTTPRequest generates attributes of the // http namespace as specified by the OpenTelemetry specification for // a span on the client side. func HTTPClientAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { attrs := []attribute.KeyValue{} if request.Method != "" { attrs = append(attrs, HTTPMethodKey.String(request.Method)) } else { attrs = append(attrs, HTTPMethodKey.String(http.MethodGet)) } // remove any username/password info that may be in the URL // before adding it to the attributes userinfo := request.URL.User request.URL.User = nil attrs = append(attrs, HTTPURLKey.String(request.URL.String())) // restore any username/password info that was removed request.URL.User = userinfo return append(attrs, httpCommonAttributesFromHTTPRequest(request)...) } func httpCommonAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { attrs := []attribute.KeyValue{} if ua := request.UserAgent(); ua != "" { attrs = append(attrs, HTTPUserAgentKey.String(ua)) } if request.ContentLength > 0 { attrs = append(attrs, HTTPRequestContentLengthKey.Int64(request.ContentLength)) } return append(attrs, httpBasicAttributesFromHTTPRequest(request)...) } func httpBasicAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { // as these attributes are used by HTTPServerMetricAttributesFromHTTPRequest, they should be low-cardinality attrs := []attribute.KeyValue{} if request.TLS != nil { attrs = append(attrs, HTTPSchemeHTTPS) } else { attrs = append(attrs, HTTPSchemeHTTP) } if request.Host != "" { attrs = append(attrs, HTTPHostKey.String(request.Host)) } flavor := "" if request.ProtoMajor == 1 { flavor = fmt.Sprintf("1.%d", request.ProtoMinor) } else if request.ProtoMajor == 2 { flavor = "2" } if flavor != "" { attrs = append(attrs, HTTPFlavorKey.String(flavor)) } return attrs } // HTTPServerMetricAttributesFromHTTPRequest generates low-cardinality attributes // to be used with server-side HTTP metrics. func HTTPServerMetricAttributesFromHTTPRequest(serverName string, request *http.Request) []attribute.KeyValue { attrs := []attribute.KeyValue{} if serverName != "" { attrs = append(attrs, HTTPServerNameKey.String(serverName)) } return append(attrs, httpBasicAttributesFromHTTPRequest(request)...) } // HTTPServerAttributesFromHTTPRequest generates attributes of the // http namespace as specified by the OpenTelemetry specification for // a span on the server side. Currently, only basic authentication is // supported. func HTTPServerAttributesFromHTTPRequest(serverName, route string, request *http.Request) []attribute.KeyValue { attrs := []attribute.KeyValue{ HTTPMethodKey.String(request.Method), HTTPTargetKey.String(request.RequestURI), } if serverName != "" { attrs = append(attrs, HTTPServerNameKey.String(serverName)) } if route != "" { attrs = append(attrs, HTTPRouteKey.String(route)) } if values, ok := request.Header["X-Forwarded-For"]; ok && len(values) > 0 { if addresses := strings.SplitN(values[0], ",", 2); len(addresses) > 0 { attrs = append(attrs, HTTPClientIPKey.String(addresses[0])) } } return append(attrs, httpCommonAttributesFromHTTPRequest(request)...) } // HTTPAttributesFromHTTPStatusCode generates attributes of the http // namespace as specified by the OpenTelemetry specification for a // span. func HTTPAttributesFromHTTPStatusCode(code int) []attribute.KeyValue { attrs := []attribute.KeyValue{ HTTPStatusCodeKey.Int(code), } return attrs } type codeRange struct { fromInclusive int toInclusive int } func (r codeRange) contains(code int) bool { return r.fromInclusive <= code && code <= r.toInclusive } var validRangesPerCategory = map[int][]codeRange{ 1: { {http.StatusContinue, http.StatusEarlyHints}, }, 2: { {http.StatusOK, http.StatusAlreadyReported}, {http.StatusIMUsed, http.StatusIMUsed}, }, 3: { {http.StatusMultipleChoices, http.StatusUseProxy}, {http.StatusTemporaryRedirect, http.StatusPermanentRedirect}, }, 4: { {http.StatusBadRequest, http.StatusTeapot}, // yes, teapot is so useful… {http.StatusMisdirectedRequest, http.StatusUpgradeRequired}, {http.StatusPreconditionRequired, http.StatusTooManyRequests}, {http.StatusRequestHeaderFieldsTooLarge, http.StatusRequestHeaderFieldsTooLarge}, {http.StatusUnavailableForLegalReasons, http.StatusUnavailableForLegalReasons}, }, 5: { {http.StatusInternalServerError, http.StatusLoopDetected}, {http.StatusNotExtended, http.StatusNetworkAuthenticationRequired}, }, } // SpanStatusFromHTTPStatusCode generates a status code and a message // as specified by the OpenTelemetry specification for a span. func SpanStatusFromHTTPStatusCode(code int) (codes.Code, string) { spanCode, valid := validateHTTPStatusCode(code) if !valid { return spanCode, fmt.Sprintf("Invalid HTTP status code %d", code) } return spanCode, "" } // SpanStatusFromHTTPStatusCodeAndSpanKind generates a status code and a message // as specified by the OpenTelemetry specification for a span. // Exclude 4xx for SERVER to set the appropriate status. func SpanStatusFromHTTPStatusCodeAndSpanKind(code int, spanKind trace.SpanKind) (codes.Code, string) { spanCode, valid := validateHTTPStatusCode(code) if !valid { return spanCode, fmt.Sprintf("Invalid HTTP status code %d", code) } category := code / 100 if spanKind == trace.SpanKindServer && category == 4 { return codes.Unset, "" } return spanCode, "" } // Validates the HTTP status code and returns corresponding span status code. // If the `code` is not a valid HTTP status code, returns span status Error // and false. func validateHTTPStatusCode(code int) (codes.Code, bool) { category := code / 100 ranges, ok := validRangesPerCategory[category] if !ok { return codes.Error, false } ok = false for _, crange := range ranges { ok = crange.contains(code) if ok { break } } if !ok { return codes.Error, false } if category > 0 && category < 4 { return codes.Unset, true } return codes.Error, true }