| // Copyright The OpenTelemetry Authors | |
| // SPDX-License-Identifier: Apache-2.0 | |
| // Package internal provides common semconv functionality. | |
| package internal | |
| import ( | |
| "fmt" | |
| "net" | |
| "net/http" | |
| "strconv" | |
| "strings" | |
| "go.opentelemetry.io/otel/attribute" | |
| "go.opentelemetry.io/otel/codes" | |
| "go.opentelemetry.io/otel/trace" | |
| ) | |
| // SemanticConventions are the semantic convention values defined for a | |
| // version of the OpenTelemetry specification. | |
| type SemanticConventions struct { | |
| EnduserIDKey attribute.Key | |
| HTTPClientIPKey attribute.Key | |
| HTTPFlavorKey attribute.Key | |
| HTTPHostKey attribute.Key | |
| HTTPMethodKey attribute.Key | |
| HTTPRequestContentLengthKey attribute.Key | |
| HTTPRouteKey attribute.Key | |
| HTTPSchemeHTTP attribute.KeyValue | |
| HTTPSchemeHTTPS attribute.KeyValue | |
| HTTPServerNameKey attribute.Key | |
| HTTPStatusCodeKey attribute.Key | |
| HTTPTargetKey attribute.Key | |
| HTTPURLKey attribute.Key | |
| HTTPUserAgentKey attribute.Key | |
| NetHostIPKey attribute.Key | |
| NetHostNameKey attribute.Key | |
| NetHostPortKey attribute.Key | |
| NetPeerIPKey attribute.Key | |
| NetPeerNameKey attribute.Key | |
| NetPeerPortKey attribute.Key | |
| NetTransportIP attribute.KeyValue | |
| NetTransportOther attribute.KeyValue | |
| NetTransportTCP attribute.KeyValue | |
| NetTransportUDP attribute.KeyValue | |
| NetTransportUnix attribute.KeyValue | |
| } | |
| // 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 (sc *SemanticConventions) NetAttributesFromHTTPRequest( | |
| network string, | |
| request *http.Request, | |
| ) []attribute.KeyValue { | |
| attrs := []attribute.KeyValue{} | |
| switch network { | |
| case "tcp", "tcp4", "tcp6": | |
| attrs = append(attrs, sc.NetTransportTCP) | |
| case "udp", "udp4", "udp6": | |
| attrs = append(attrs, sc.NetTransportUDP) | |
| case "ip", "ip4", "ip6": | |
| attrs = append(attrs, sc.NetTransportIP) | |
| case "unix", "unixgram", "unixpacket": | |
| attrs = append(attrs, sc.NetTransportUnix) | |
| default: | |
| attrs = append(attrs, sc.NetTransportOther) | |
| } | |
| peerIP, peerName, peerPort := hostIPNamePort(request.RemoteAddr) | |
| if peerIP != "" { | |
| attrs = append(attrs, sc.NetPeerIPKey.String(peerIP)) | |
| } | |
| if peerName != "" { | |
| attrs = append(attrs, sc.NetPeerNameKey.String(peerName)) | |
| } | |
| if peerPort != 0 { | |
| attrs = append(attrs, sc.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, sc.NetHostIPKey.String(hostIP)) | |
| } | |
| if hostName != "" { | |
| attrs = append(attrs, sc.NetHostNameKey.String(hostName)) | |
| } | |
| if hostPort != 0 { | |
| attrs = append(attrs, sc.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, 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) // nolint: gosec // Bit size of 16 checked above. | |
| } | |
| return ip, name, port | |
| } | |
| // EndUserAttributesFromHTTPRequest generates attributes of the | |
| // enduser namespace as specified by the OpenTelemetry specification | |
| // for a span. | |
| func (sc *SemanticConventions) EndUserAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { | |
| if username, _, ok := request.BasicAuth(); ok { | |
| return []attribute.KeyValue{sc.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 (sc *SemanticConventions) HTTPClientAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { | |
| attrs := []attribute.KeyValue{} | |
| // 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, sc.HTTPURLKey.String(request.URL.String())) | |
| // restore any username/password info that was removed | |
| request.URL.User = userinfo | |
| return append(attrs, sc.httpCommonAttributesFromHTTPRequest(request)...) | |
| } | |
| func (sc *SemanticConventions) httpCommonAttributesFromHTTPRequest(request *http.Request) []attribute.KeyValue { | |
| attrs := []attribute.KeyValue{} | |
| if ua := request.UserAgent(); ua != "" { | |
| attrs = append(attrs, sc.HTTPUserAgentKey.String(ua)) | |
| } | |
| if request.ContentLength > 0 { | |
| attrs = append(attrs, sc.HTTPRequestContentLengthKey.Int64(request.ContentLength)) | |
| } | |
| return append(attrs, sc.httpBasicAttributesFromHTTPRequest(request)...) | |
| } | |
| func (sc *SemanticConventions) 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, sc.HTTPSchemeHTTPS) | |
| } else { | |
| attrs = append(attrs, sc.HTTPSchemeHTTP) | |
| } | |
| if request.Host != "" { | |
| attrs = append(attrs, sc.HTTPHostKey.String(request.Host)) | |
| } else if request.URL != nil && request.URL.Host != "" { | |
| attrs = append(attrs, sc.HTTPHostKey.String(request.URL.Host)) | |
| } | |
| flavor := "" | |
| switch request.ProtoMajor { | |
| case 1: | |
| flavor = fmt.Sprintf("1.%d", request.ProtoMinor) | |
| case 2: | |
| flavor = "2" | |
| } | |
| if flavor != "" { | |
| attrs = append(attrs, sc.HTTPFlavorKey.String(flavor)) | |
| } | |
| if request.Method != "" { | |
| attrs = append(attrs, sc.HTTPMethodKey.String(request.Method)) | |
| } else { | |
| attrs = append(attrs, sc.HTTPMethodKey.String(http.MethodGet)) | |
| } | |
| return attrs | |
| } | |
| // HTTPServerMetricAttributesFromHTTPRequest generates low-cardinality attributes | |
| // to be used with server-side HTTP metrics. | |
| func (sc *SemanticConventions) HTTPServerMetricAttributesFromHTTPRequest( | |
| serverName string, | |
| request *http.Request, | |
| ) []attribute.KeyValue { | |
| attrs := []attribute.KeyValue{} | |
| if serverName != "" { | |
| attrs = append(attrs, sc.HTTPServerNameKey.String(serverName)) | |
| } | |
| return append(attrs, sc.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 (sc *SemanticConventions) HTTPServerAttributesFromHTTPRequest( | |
| serverName, route string, | |
| request *http.Request, | |
| ) []attribute.KeyValue { | |
| attrs := []attribute.KeyValue{ | |
| sc.HTTPTargetKey.String(request.RequestURI), | |
| } | |
| if serverName != "" { | |
| attrs = append(attrs, sc.HTTPServerNameKey.String(serverName)) | |
| } | |
| if route != "" { | |
| attrs = append(attrs, sc.HTTPRouteKey.String(route)) | |
| } | |
| if values := request.Header["X-Forwarded-For"]; len(values) > 0 { | |
| addr := values[0] | |
| if i := strings.Index(addr, ","); i > 0 { | |
| addr = addr[:i] | |
| } | |
| attrs = append(attrs, sc.HTTPClientIPKey.String(addr)) | |
| } | |
| return append(attrs, sc.httpCommonAttributesFromHTTPRequest(request)...) | |
| } | |
| // HTTPAttributesFromHTTPStatusCode generates attributes of the http | |
| // namespace as specified by the OpenTelemetry specification for a | |
| // span. | |
| func (sc *SemanticConventions) HTTPAttributesFromHTTPStatusCode(code int) []attribute.KeyValue { | |
| attrs := []attribute.KeyValue{ | |
| sc.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, "" | |
| } | |
| // validateHTTPStatusCode 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 | |
| } |