...

Source file src/go.opentelemetry.io/otel/semconv/internal/v4/http.go

Documentation: go.opentelemetry.io/otel/semconv/internal/v4

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

View as plain text