...

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

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

     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  }
   405  

View as plain text