mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-04-11 11:21:59 +02:00
* Update ClientRequest HTTPS determination The ClientRequest function will only report a peer port attribute if that peer port differs from the standard 80 for HTTP and 443 for HTTPS. In determining if the request is for HTTPS use the request URL scheme. This is not perfect. If a user doesn't provide a scheme this will not be correctly detected. However, the current approach of checking if the `TLS` field is non-nil will always be wrong, requests made by client ignore this field and it is always nil. Therefore, switching to using the URL field is the best we can do without having already made the request. * Test HTTPS detection for ClientRequest
381 lines
11 KiB
Go
381 lines
11 KiB
Go
// 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 internal // import "go.opentelemetry.io/otel/semconv/internal/v2"
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
)
|
|
|
|
// HTTPConv are the HTTP semantic convention attributes defined for a version
|
|
// of the OpenTelemetry specification.
|
|
type HTTPConv struct {
|
|
NetConv *NetConv
|
|
|
|
EnduserIDKey attribute.Key
|
|
HTTPClientIPKey attribute.Key
|
|
HTTPFlavorKey attribute.Key
|
|
HTTPMethodKey attribute.Key
|
|
HTTPRequestContentLengthKey attribute.Key
|
|
HTTPResponseContentLengthKey attribute.Key
|
|
HTTPRouteKey attribute.Key
|
|
HTTPSchemeHTTP attribute.KeyValue
|
|
HTTPSchemeHTTPS attribute.KeyValue
|
|
HTTPStatusCodeKey attribute.Key
|
|
HTTPTargetKey attribute.Key
|
|
HTTPURLKey attribute.Key
|
|
HTTPUserAgentKey attribute.Key
|
|
}
|
|
|
|
// ClientResponse returns attributes for an HTTP response received by a client
|
|
// from a server. The following attributes are returned if the related values
|
|
// are defined in resp: "http.status.code", "http.response_content_length".
|
|
//
|
|
// This does not add all OpenTelemetry required attributes for an HTTP event,
|
|
// it assumes ClientRequest was used to create the span with a complete set of
|
|
// attributes. If a complete set of attributes can be generated using the
|
|
// request contained in resp. For example:
|
|
//
|
|
// append(ClientResponse(resp), ClientRequest(resp.Request)...)
|
|
func (c *HTTPConv) ClientResponse(resp http.Response) []attribute.KeyValue {
|
|
var n int
|
|
if resp.StatusCode > 0 {
|
|
n++
|
|
}
|
|
if resp.ContentLength > 0 {
|
|
n++
|
|
}
|
|
|
|
attrs := make([]attribute.KeyValue, 0, n)
|
|
if resp.StatusCode > 0 {
|
|
attrs = append(attrs, c.HTTPStatusCodeKey.Int(resp.StatusCode))
|
|
}
|
|
if resp.ContentLength > 0 {
|
|
attrs = append(attrs, c.HTTPResponseContentLengthKey.Int(int(resp.ContentLength)))
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
// ClientRequest returns attributes for an HTTP request made by a client. The
|
|
// following attributes are always returned: "http.url", "http.flavor",
|
|
// "http.method", "net.peer.name". The following attributes are returned if the
|
|
// related values are defined in req: "net.peer.port", "http.user_agent",
|
|
// "http.request_content_length", "enduser.id".
|
|
func (c *HTTPConv) ClientRequest(req *http.Request) []attribute.KeyValue {
|
|
n := 3 // URL, peer name, proto, and method.
|
|
var h string
|
|
if req.URL != nil {
|
|
h = req.URL.Host
|
|
}
|
|
peer, p := firstHostPort(h, req.Header.Get("Host"))
|
|
port := requiredHTTPPort(req.URL != nil && req.URL.Scheme == "https", p)
|
|
if port > 0 {
|
|
n++
|
|
}
|
|
useragent := req.UserAgent()
|
|
if useragent != "" {
|
|
n++
|
|
}
|
|
if req.ContentLength > 0 {
|
|
n++
|
|
}
|
|
userID, _, hasUserID := req.BasicAuth()
|
|
if hasUserID {
|
|
n++
|
|
}
|
|
attrs := make([]attribute.KeyValue, 0, n)
|
|
|
|
attrs = append(attrs, c.method(req.Method))
|
|
attrs = append(attrs, c.proto(req.Proto))
|
|
|
|
var u string
|
|
if req.URL != nil {
|
|
// Remove any username/password info that may be in the URL.
|
|
userinfo := req.URL.User
|
|
req.URL.User = nil
|
|
u = req.URL.String()
|
|
// Restore any username/password info that was removed.
|
|
req.URL.User = userinfo
|
|
}
|
|
attrs = append(attrs, c.HTTPURLKey.String(u))
|
|
|
|
attrs = append(attrs, c.NetConv.PeerName(peer))
|
|
if port > 0 {
|
|
attrs = append(attrs, c.NetConv.PeerPort(port))
|
|
}
|
|
|
|
if useragent != "" {
|
|
attrs = append(attrs, c.HTTPUserAgentKey.String(useragent))
|
|
}
|
|
|
|
if l := req.ContentLength; l > 0 {
|
|
attrs = append(attrs, c.HTTPRequestContentLengthKey.Int64(l))
|
|
}
|
|
|
|
if hasUserID {
|
|
attrs = append(attrs, c.EnduserIDKey.String(userID))
|
|
}
|
|
|
|
return attrs
|
|
}
|
|
|
|
// ServerRequest returns attributes for an HTTP request received by a server.
|
|
// The following attributes are always returned: "http.method", "http.scheme",
|
|
// "http.flavor", "http.target", "net.host.name". The following attributes are
|
|
// returned if they related values are defined in req: "net.host.port",
|
|
// "net.sock.peer.addr", "net.sock.peer.port", "http.user_agent", "enduser.id",
|
|
// "http.client_ip".
|
|
func (c *HTTPConv) ServerRequest(req *http.Request) []attribute.KeyValue {
|
|
n := 5 // Method, scheme, target, proto, and host name.
|
|
host, p := splitHostPort(req.Host)
|
|
hostPort := requiredHTTPPort(req.TLS != nil, p)
|
|
if hostPort > 0 {
|
|
n++
|
|
}
|
|
peer, peerPort := splitHostPort(req.RemoteAddr)
|
|
if peer != "" {
|
|
n++
|
|
if peerPort > 0 {
|
|
n++
|
|
}
|
|
}
|
|
useragent := req.UserAgent()
|
|
if useragent != "" {
|
|
n++
|
|
}
|
|
userID, _, hasUserID := req.BasicAuth()
|
|
if hasUserID {
|
|
n++
|
|
}
|
|
clientIP := serverClientIP(req.Header.Get("X-Forwarded-For"))
|
|
if clientIP != "" {
|
|
n++
|
|
}
|
|
attrs := make([]attribute.KeyValue, 0, n)
|
|
|
|
attrs = append(attrs, c.method(req.Method))
|
|
attrs = append(attrs, c.scheme(req.TLS != nil))
|
|
attrs = append(attrs, c.proto(req.Proto))
|
|
attrs = append(attrs, c.NetConv.HostName(host))
|
|
|
|
if req.URL != nil {
|
|
attrs = append(attrs, c.HTTPTargetKey.String(req.URL.RequestURI()))
|
|
} else {
|
|
// This should never occur if the request was generated by the net/http
|
|
// package. Fail gracefully, if it does though.
|
|
attrs = append(attrs, c.HTTPTargetKey.String(req.RequestURI))
|
|
}
|
|
|
|
if hostPort > 0 {
|
|
attrs = append(attrs, c.NetConv.HostPort(hostPort))
|
|
}
|
|
|
|
if peer != "" {
|
|
// The Go HTTP server sets RemoteAddr to "IP:port", this will not be a
|
|
// file-path that would be interpreted with a sock family.
|
|
attrs = append(attrs, c.NetConv.SockPeerAddr(peer))
|
|
if peerPort > 0 {
|
|
attrs = append(attrs, c.NetConv.SockPeerPort(peerPort))
|
|
}
|
|
}
|
|
|
|
if useragent != "" {
|
|
attrs = append(attrs, c.HTTPUserAgentKey.String(useragent))
|
|
}
|
|
|
|
if hasUserID {
|
|
attrs = append(attrs, c.EnduserIDKey.String(userID))
|
|
}
|
|
|
|
if clientIP != "" {
|
|
attrs = append(attrs, c.HTTPClientIPKey.String(clientIP))
|
|
}
|
|
|
|
return attrs
|
|
}
|
|
|
|
func (c *HTTPConv) method(method string) attribute.KeyValue {
|
|
if method == "" {
|
|
return c.HTTPMethodKey.String(http.MethodGet)
|
|
}
|
|
return c.HTTPMethodKey.String(method)
|
|
}
|
|
|
|
func (c *HTTPConv) scheme(https bool) attribute.KeyValue { // nolint:revive
|
|
if https {
|
|
return c.HTTPSchemeHTTPS
|
|
}
|
|
return c.HTTPSchemeHTTP
|
|
}
|
|
|
|
func (c *HTTPConv) proto(proto string) attribute.KeyValue {
|
|
switch proto {
|
|
case "HTTP/1.0":
|
|
return c.HTTPFlavorKey.String("1.0")
|
|
case "HTTP/1.1":
|
|
return c.HTTPFlavorKey.String("1.1")
|
|
case "HTTP/2":
|
|
return c.HTTPFlavorKey.String("2.0")
|
|
case "HTTP/3":
|
|
return c.HTTPFlavorKey.String("3.0")
|
|
default:
|
|
return c.HTTPFlavorKey.String(proto)
|
|
}
|
|
}
|
|
|
|
func serverClientIP(xForwardedFor string) string {
|
|
if idx := strings.Index(xForwardedFor, ","); idx >= 0 {
|
|
xForwardedFor = xForwardedFor[:idx]
|
|
}
|
|
return xForwardedFor
|
|
}
|
|
|
|
func requiredHTTPPort(https bool, port int) int { // nolint:revive
|
|
if https {
|
|
if port > 0 && port != 443 {
|
|
return port
|
|
}
|
|
} else {
|
|
if port > 0 && port != 80 {
|
|
return port
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Return the request host and port from the first non-empty source.
|
|
func firstHostPort(source ...string) (host string, port int) {
|
|
for _, hostport := range source {
|
|
host, port = splitHostPort(hostport)
|
|
if host != "" || port > 0 {
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// RequestHeader returns the contents of h as OpenTelemetry attributes.
|
|
func (c *HTTPConv) RequestHeader(h http.Header) []attribute.KeyValue {
|
|
return c.header("http.request.header", h)
|
|
}
|
|
|
|
// ResponseHeader returns the contents of h as OpenTelemetry attributes.
|
|
func (c *HTTPConv) ResponseHeader(h http.Header) []attribute.KeyValue {
|
|
return c.header("http.response.header", h)
|
|
}
|
|
|
|
func (c *HTTPConv) header(prefix string, h http.Header) []attribute.KeyValue {
|
|
key := func(k string) attribute.Key {
|
|
k = strings.ToLower(k)
|
|
k = strings.ReplaceAll(k, "-", "_")
|
|
k = fmt.Sprintf("%s.%s", prefix, k)
|
|
return attribute.Key(k)
|
|
}
|
|
|
|
attrs := make([]attribute.KeyValue, 0, len(h))
|
|
for k, v := range h {
|
|
attrs = append(attrs, key(k).StringSlice(v))
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
// ClientStatus returns a span status code and message for an HTTP status code
|
|
// value received by a client.
|
|
func (c *HTTPConv) ClientStatus(code int) (codes.Code, string) {
|
|
stat, valid := validateHTTPStatusCode(code)
|
|
if !valid {
|
|
return stat, fmt.Sprintf("Invalid HTTP status code %d", code)
|
|
}
|
|
return stat, ""
|
|
}
|
|
|
|
// ServerStatus returns a span status code and message for an HTTP status code
|
|
// value returned by a server. Status codes in the 400-499 range are not
|
|
// returned as errors.
|
|
func (c *HTTPConv) ServerStatus(code int) (codes.Code, string) {
|
|
stat, valid := validateHTTPStatusCode(code)
|
|
if !valid {
|
|
return stat, fmt.Sprintf("Invalid HTTP status code %d", code)
|
|
}
|
|
|
|
if code/100 == 4 {
|
|
return codes.Unset, ""
|
|
}
|
|
return stat, ""
|
|
}
|
|
|
|
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},
|
|
},
|
|
}
|
|
|
|
// 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
|
|
}
|