diff --git a/Makefile b/Makefile index dcaf3456c57..35778ef6318 100644 --- a/Makefile +++ b/Makefile @@ -81,7 +81,7 @@ generate: go-generate vanity-import-fix .PHONY: go-generate go-generate: $(OTEL_GO_MOD_DIRS:%=go-generate/%) go-generate/%: DIR=$* -go-generate/%: | $(STRINGER) $(PORTO) +go-generate/%: | $(STRINGER) $(GOTMPL) @echo "$(GO) generate $(DIR)/..." \ && cd $(DIR) \ && PATH="$(TOOLS):$${PATH}" $(GO) generate ./... @@ -148,7 +148,7 @@ lint: go-mod-tidy golangci-lint misspell .PHONY: license-check license-check: @licRes=$$(for f in $$(find . -type f \( -iname '*.go' -o -iname '*.sh' \) ! -path './vendor/*' ! -path './exporters/otlp/internal/opentelemetry-proto/*') ; do \ - awk '/Copyright The OpenTelemetry Authors|generated|GENERATED/ && NR<=3 { found=1; next } END { if (!found) print FILENAME }' $$f; \ + awk '/Copyright The OpenTelemetry Authors|generated|GENERATED/ && NR<=4 { found=1; next } END { if (!found) print FILENAME }' $$f; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/gen.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/gen.go new file mode 100644 index 00000000000..cb58065a97f --- /dev/null +++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/httpconv.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..c60d4d8b8b5 --- /dev/null +++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/httpconv_test.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/netconv.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..914556f485b --- /dev/null +++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/netconv_test.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/github.com/emicklei/go-restful/otelrestful/restful.go b/instrumentation/github.com/emicklei/go-restful/otelrestful/restful.go index 22099eadac3..4325368fa3a 100644 --- a/instrumentation/github.com/emicklei/go-restful/otelrestful/restful.go +++ b/instrumentation/github.com/emicklei/go-restful/otelrestful/restful.go @@ -17,10 +17,10 @@ package otelrestful // import "go.opentelemetry.io/contrib/instrumentation/githu import ( "github.com/emicklei/go-restful/v3" + "go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconvutil" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -53,7 +53,7 @@ func OTelFilter(service string, opts ...Option) restful.FilterFunction { spanName := route opts := []oteltrace.SpanStartOption{ - oteltrace.WithAttributes(httpconv.ServerRequest(service, r)...), + oteltrace.WithAttributes(semconvutil.HTTPServerRequest(service, r)...), oteltrace.WithSpanKind(oteltrace.SpanKindServer), } if route != "" { @@ -78,7 +78,7 @@ func OTelFilter(service string, opts ...Option) restful.FilterFunction { chain.ProcessFilter(req, resp) status := resp.StatusCode() - span.SetStatus(httpconv.ServerStatus(status)) + span.SetStatus(semconvutil.HTTPServerStatus(status)) if status > 0 { span.SetAttributes(semconv.HTTPStatusCode(status)) } diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go index 5e7a9e22296..c114d085578 100644 --- a/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/gintrace.go @@ -21,13 +21,12 @@ import ( "github.com/gin-gonic/gin" + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -70,7 +69,7 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { }() ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header)) opts := []oteltrace.SpanStartOption{ - oteltrace.WithAttributes(httpconv.ServerRequest(service, c.Request)...), + oteltrace.WithAttributes(semconvutil.HTTPServerRequest(service, c.Request)...), oteltrace.WithSpanKind(oteltrace.SpanKindServer), } var spanName string @@ -95,7 +94,7 @@ func Middleware(service string, opts ...Option) gin.HandlerFunc { c.Next() status := c.Writer.Status() - span.SetStatus(httpconv.ServerStatus(status)) + span.SetStatus(semconvutil.HTTPServerStatus(status)) if status > 0 { span.SetAttributes(semconv.HTTPStatusCode(status)) } diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/gen.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/gen.go new file mode 100644 index 00000000000..1e9c0c94cac --- /dev/null +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/httpconv.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..99182ee7c34 --- /dev/null +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/httpconv_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/netconv.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..e13c0cd4a41 --- /dev/null +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/netconv_test.go b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/github.com/gin-gonic/gin/otelgin/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/gen.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/gen.go new file mode 100644 index 00000000000..1c9ab4a1b0b --- /dev/null +++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/httpconv.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..390cc0f765f --- /dev/null +++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/httpconv_test.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/netconv.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..50c5a982431 --- /dev/null +++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/netconv_test.go b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/github.com/gorilla/mux/otelmux/mux.go b/instrumentation/github.com/gorilla/mux/otelmux/mux.go index b73170f6ec1..cf0d8f83f8f 100644 --- a/instrumentation/github.com/gorilla/mux/otelmux/mux.go +++ b/instrumentation/github.com/gorilla/mux/otelmux/mux.go @@ -22,11 +22,10 @@ import ( "github.com/felixge/httpsnoop" "github.com/gorilla/mux" + "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux/internal/semconvutil" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" "go.opentelemetry.io/otel/trace" ) @@ -143,7 +142,7 @@ func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) { } opts := []trace.SpanStartOption{ - trace.WithAttributes(httpconv.ServerRequest(tw.service, r)...), + trace.WithAttributes(semconvutil.HTTPServerRequest(tw.service, r)...), trace.WithSpanKind(trace.SpanKindServer), } @@ -171,5 +170,5 @@ func (tw traceware) ServeHTTP(w http.ResponseWriter, r *http.Request) { if rrw.status > 0 { span.SetAttributes(semconv.HTTPStatusCode(rrw.status)) } - span.SetStatus(httpconv.ServerStatus(rrw.status)) + span.SetStatus(semconvutil.HTTPServerStatus(rrw.status)) } diff --git a/instrumentation/github.com/labstack/echo/otelecho/echo.go b/instrumentation/github.com/labstack/echo/otelecho/echo.go index 536e2bfb587..02f797f88df 100644 --- a/instrumentation/github.com/labstack/echo/otelecho/echo.go +++ b/instrumentation/github.com/labstack/echo/otelecho/echo.go @@ -22,10 +22,10 @@ import ( "go.opentelemetry.io/otel" + "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -70,7 +70,7 @@ func Middleware(service string, opts ...Option) echo.MiddlewareFunc { }() ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(request.Header)) opts := []oteltrace.SpanStartOption{ - oteltrace.WithAttributes(httpconv.ServerRequest(service, request)...), + oteltrace.WithAttributes(semconvutil.HTTPServerRequest(service, request)...), oteltrace.WithSpanKind(oteltrace.SpanKindServer), } if path := c.Path(); path != "" { @@ -97,7 +97,7 @@ func Middleware(service string, opts ...Option) echo.MiddlewareFunc { } status := c.Response().Status - span.SetStatus(httpconv.ServerStatus(status)) + span.SetStatus(semconvutil.HTTPServerStatus(status)) if status > 0 { span.SetAttributes(semconv.HTTPStatusCode(status)) } diff --git a/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/gen.go b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/gen.go new file mode 100644 index 00000000000..8e3e5722c4a --- /dev/null +++ b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/httpconv.go b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..e22942433b0 --- /dev/null +++ b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/httpconv_test.go b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/netconv.go b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..0d4da5769e1 --- /dev/null +++ b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/netconv_test.go b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/github.com/labstack/echo/otelecho/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/gen.go b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/gen.go new file mode 100644 index 00000000000..d7d4009f8a4 --- /dev/null +++ b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/httpconv.go b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..608ba5490d7 --- /dev/null +++ b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/httpconv_test.go b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/netconv.go b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..98a4250fba8 --- /dev/null +++ b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/netconv_test.go b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/gopkg.in/macaron.v1/otelmacaron/macaron.go b/instrumentation/gopkg.in/macaron.v1/otelmacaron/macaron.go index 6f60745863b..62321a93545 100644 --- a/instrumentation/gopkg.in/macaron.v1/otelmacaron/macaron.go +++ b/instrumentation/gopkg.in/macaron.v1/otelmacaron/macaron.go @@ -20,9 +20,9 @@ import ( "gopkg.in/macaron.v1" + "go.opentelemetry.io/contrib/instrumentation/gopkg.in/macaron.v1/otelmacaron/internal/semconvutil" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" oteltrace "go.opentelemetry.io/otel/trace" ) @@ -44,7 +44,7 @@ func Middleware(service string, opts ...Option) macaron.Handler { ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(c.Req.Header)) opts := []oteltrace.SpanStartOption{ - oteltrace.WithAttributes(httpconv.ServerRequest(service, c.Req.Request)...), + oteltrace.WithAttributes(semconvutil.HTTPServerRequest(service, c.Req.Request)...), oteltrace.WithSpanKind(oteltrace.SpanKindServer), } // TODO: span name should be router template not the actual request path, eg /user/:id vs /user/123 @@ -62,7 +62,7 @@ func Middleware(service string, opts ...Option) macaron.Handler { c.Next() status := c.Resp.Status() - span.SetStatus(httpconv.ServerStatus(status)) + span.SetStatus(semconvutil.HTTPServerStatus(status)) if status > 0 { span.SetAttributes(semconv.HTTPStatusCode(status)) } diff --git a/instrumentation/net/http/httptrace/otelhttptrace/go.mod b/instrumentation/net/http/httptrace/otelhttptrace/go.mod index dea716f6e35..b3be88ba4d6 100644 --- a/instrumentation/net/http/httptrace/otelhttptrace/go.mod +++ b/instrumentation/net/http/httptrace/otelhttptrace/go.mod @@ -4,12 +4,16 @@ go 1.19 require ( github.com/google/go-cmp v0.5.9 + github.com/stretchr/testify v1.8.3 go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/otel/metric v1.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/instrumentation/net/http/httptrace/otelhttptrace/go.sum b/instrumentation/net/http/httptrace/otelhttptrace/go.sum index f6693051dc6..a1e144aa8e3 100644 --- a/instrumentation/net/http/httptrace/otelhttptrace/go.sum +++ b/instrumentation/net/http/httptrace/otelhttptrace/go.sum @@ -1,4 +1,5 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -7,11 +8,16 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/net/http/httptrace/otelhttptrace/httptrace.go b/instrumentation/net/http/httptrace/otelhttptrace/httptrace.go index 9e243cfddd9..e09a9885dec 100644 --- a/instrumentation/net/http/httptrace/otelhttptrace/httptrace.go +++ b/instrumentation/net/http/httptrace/otelhttptrace/httptrace.go @@ -18,13 +18,12 @@ import ( "context" "net/http" + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/baggage" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" - "go.opentelemetry.io/otel/semconv/v1.17.0/netconv" "go.opentelemetry.io/otel/trace" ) @@ -66,7 +65,7 @@ func Extract(ctx context.Context, req *http.Request, opts ...Option) ([]attribut c := newConfig(opts) ctx = c.propagators.Extract(ctx, propagation.HeaderCarrier(req.Header)) - attrs := append(httpconv.ServerRequest("", req), netconv.Transport("tcp")) + attrs := append(semconvutil.HTTPServerRequest("", req), semconvutil.NetTransport("tcp")) if req.ContentLength > 0 { a := semconv.HTTPRequestContentLength(int(req.ContentLength)) attrs = append(attrs, a) diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/gen.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/gen.go new file mode 100644 index 00000000000..17963499857 --- /dev/null +++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/httpconv.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..5073346456f --- /dev/null +++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/httpconv_test.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/netconv.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..c6116d44c31 --- /dev/null +++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/netconv_test.go b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/net/http/httptrace/otelhttptrace/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/net/http/otelhttp/handler.go b/instrumentation/net/http/otelhttp/handler.go index f2f20e3b930..539917be746 100644 --- a/instrumentation/net/http/otelhttp/handler.go +++ b/instrumentation/net/http/otelhttp/handler.go @@ -21,12 +21,12 @@ import ( "github.com/felixge/httpsnoop" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" "go.opentelemetry.io/otel/trace" ) @@ -130,7 +130,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := h.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) opts := []trace.SpanStartOption{ - trace.WithAttributes(httpconv.ServerRequest(h.server, r)...), + trace.WithAttributes(semconvutil.HTTPServerRequest(h.server, r)...), } if h.server != "" { hostAttr := semconv.NetHostName(h.server) @@ -214,7 +214,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err) // Add metrics - attributes := append(labeler.Get(), httpconv.ServerRequest(h.server, r)...) + attributes := append(labeler.Get(), semconvutil.HTTPServerRequest(h.server, r)...) if rww.statusCode > 0 { attributes = append(attributes, semconv.HTTPStatusCode(rww.statusCode)) } @@ -245,7 +245,7 @@ func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, if statusCode > 0 { attributes = append(attributes, semconv.HTTPStatusCode(statusCode)) } - span.SetStatus(httpconv.ServerStatus(statusCode)) + span.SetStatus(semconvutil.HTTPServerStatus(statusCode)) if werr != nil && werr != io.EOF { attributes = append(attributes, WriteErrorKey.String(werr.Error())) diff --git a/instrumentation/net/http/otelhttp/internal/semconvutil/gen.go b/instrumentation/net/http/otelhttp/internal/semconvutil/gen.go new file mode 100644 index 00000000000..edf4ce3d315 --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconvutil/gen.go @@ -0,0 +1,21 @@ +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" + +// Generate semconvutil package: +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/httpconv_test.go.tmpl "--data={}" --out=httpconv_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/httpconv.go.tmpl "--data={}" --out=httpconv.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/netconv_test.go.tmpl "--data={}" --out=netconv_test.go +//go:generate gotmpl --body=../../../../../../internal/shared/semconvutil/netconv.go.tmpl "--data={}" --out=netconv.go diff --git a/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go b/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go new file mode 100644 index 00000000000..42c44621aab --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv.go @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv_test.go b/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv_test.go new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconvutil/httpconv_test.go @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go b/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go new file mode 100644 index 00000000000..bde8893437d --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconvutil/netconv.go @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/instrumentation/net/http/otelhttp/internal/semconvutil/netconv_test.go b/instrumentation/net/http/otelhttp/internal/semconvutil/netconv_test.go new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/instrumentation/net/http/otelhttp/internal/semconvutil/netconv_test.go @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/instrumentation/net/http/otelhttp/transport.go b/instrumentation/net/http/otelhttp/transport.go index 9dda7e1a957..24c8cb264cb 100644 --- a/instrumentation/net/http/otelhttp/transport.go +++ b/instrumentation/net/http/otelhttp/transport.go @@ -20,10 +20,10 @@ import ( "net/http" "net/http/httptrace" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/semconv/v1.17.0/httpconv" "go.opentelemetry.io/otel/trace" ) @@ -110,7 +110,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { } r = r.WithContext(ctx) - span.SetAttributes(httpconv.ClientRequest(r)...) + span.SetAttributes(semconvutil.HTTPClientRequest(r)...) t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header)) res, err := t.rt.RoundTrip(r) @@ -121,8 +121,8 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { return res, err } - span.SetAttributes(httpconv.ClientResponse(res)...) - span.SetStatus(httpconv.ClientStatus(res.StatusCode)) + span.SetAttributes(semconvutil.HTTPClientResponse(res)...) + span.SetStatus(semconvutil.HTTPClientStatus(res.StatusCode)) res.Body = newWrappedBody(span, res.Body) return res, err diff --git a/internal/shared/README.md b/internal/shared/README.md new file mode 100644 index 00000000000..c30c11df952 --- /dev/null +++ b/internal/shared/README.md @@ -0,0 +1,5 @@ +# Shared + +Code under this directory contains reusable internal code +which is distributed across packages using `//go:generate gotmpl` +in `gen.go` files. diff --git a/internal/shared/semconvutil/httpconv.go.tmpl b/internal/shared/semconvutil/httpconv.go.tmpl new file mode 100644 index 00000000000..8876b4514a4 --- /dev/null +++ b/internal/shared/semconvutil/httpconv.go.tmpl @@ -0,0 +1,519 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv.go.tmpl + +// 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 semconvutil + +import ( + "fmt" + "net/http" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// HTTPClientResponse returns trace attributes for an HTTP response received by a +// client from a server. It will return the following attributes 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(HTTPClientResponse(resp), ClientRequest(resp.Request)...) +func HTTPClientResponse(resp *http.Response) []attribute.KeyValue { + return hc.ClientResponse(resp) +} + +// HTTPClientRequest returns trace 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 HTTPClientRequest(req *http.Request) []attribute.KeyValue { + return hc.ClientRequest(req) +} + +// HTTPClientStatus returns a span status code and message for an HTTP status code +// value received by a client. +func HTTPClientStatus(code int) (codes.Code, string) { + return hc.ClientStatus(code) +} + +// HTTPServerRequest returns trace attributes for an HTTP request received by a +// server. +// +// The server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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 HTTPServerRequest(server string, req *http.Request) []attribute.KeyValue { + return hc.ServerRequest(server, req) +} + +// HTTPServerStatus 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 HTTPServerStatus(code int) (codes.Code, string) { + return hc.ServerStatus(code) +} + +// HTTPRequestHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPRequestHeader(h http.Header) []attribute.KeyValue { + return hc.RequestHeader(h) +} + +// HTTPResponseHeader returns the contents of h as attributes. +// +// Instrumentation should require an explicit configuration of which headers to +// captured and then prune what they pass here. Including all headers can be a +// security risk - explicit configuration helps avoid leaking sensitive +// information. +// +// The User-Agent header is already captured in the http.user_agent attribute +// from ClientRequest and ServerRequest. Instrumentation may provide an option +// to capture that header here even though it is not recommended. Otherwise, +// instrumentation should filter that out of what is passed. +func HTTPResponseHeader(h http.Header) []attribute.KeyValue { + return hc.ResponseHeader(h) +} + +// 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 +} + +var hc = &httpConv{ + NetConv: nc, + + EnduserIDKey: semconv.EnduserIDKey, + HTTPClientIPKey: semconv.HTTPClientIPKey, + HTTPFlavorKey: semconv.HTTPFlavorKey, + HTTPMethodKey: semconv.HTTPMethodKey, + HTTPRequestContentLengthKey: semconv.HTTPRequestContentLengthKey, + HTTPResponseContentLengthKey: semconv.HTTPResponseContentLengthKey, + HTTPRouteKey: semconv.HTTPRouteKey, + HTTPSchemeHTTP: semconv.HTTPSchemeHTTP, + HTTPSchemeHTTPS: semconv.HTTPSchemeHTTPS, + HTTPStatusCodeKey: semconv.HTTPStatusCodeKey, + HTTPTargetKey: semconv.HTTPTargetKey, + HTTPURLKey: semconv.HTTPURLKey, + HTTPUserAgentKey: semconv.HTTPUserAgentKey, +} + +// 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 server must be the primary server name if it is known. For example this +// would be the ServerName directive +// (https://httpd.apache.org/docs/2.4/mod/core.html#servername) for an Apache +// server, and the server_name directive +// (http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name) for an +// nginx server. More generically, the primary server name would be the host +// header value that matches the default virtual host of an HTTP server. It +// should include the host identifier and if a port is used to route to the +// server that port identifier should be included as an appropriate port +// suffix. +// +// If the primary server name is not known, server should be an empty string. +// The req Host will be used to determine the server instead. +// +// 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(server string, req *http.Request) []attribute.KeyValue { + // TODO: This currently does not add the specification required + // `http.target` attribute. It has too high of a cardinality to safely be + // added. An alternate should be added, or this comment removed, when it is + // addressed by the specification. If it is ultimately decided to continue + // not including the attribute, the HTTPTargetKey field of the httpConv + // should be removed as well. + + n := 4 // Method, scheme, proto, and host name. + var host string + var p int + if server == "" { + host, p = splitHostPort(req.Host) + } else { + // Prioritize the primary server name. + host, p = splitHostPort(server) + if p < 0 { + _, 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 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 +} diff --git a/internal/shared/semconvutil/httpconv_test.go.tmpl b/internal/shared/semconvutil/httpconv_test.go.tmpl new file mode 100644 index 00000000000..3aaaa87154a --- /dev/null +++ b/internal/shared/semconvutil/httpconv_test.go.tmpl @@ -0,0 +1,474 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/httpconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func TestHTTPClientResponse(t *testing.T) { + const stat, n = 201, 397 + resp := &http.Response{ + StatusCode: stat, + ContentLength: n, + } + got := HTTPClientResponse(resp) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.Key("http.status_code").Int(stat), + attribute.Key("http.response_content_length").Int(n), + }, got) +} + +func TestHTTPSClientRequest(t *testing.T) { + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "127.0.0.1:443", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + } + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "https://127.0.0.1:443/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequest(t *testing.T) { + const ( + user = "alice" + n = 128 + agent = "Go-http-client/1.1" + ) + req := &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "http", + Host: "127.0.0.1:8080", + Path: "/resource", + }, + Proto: "HTTP/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{ + "User-Agent": []string{agent}, + }, + ContentLength: n, + } + req.SetBasicAuth(user, "pswrd") + + assert.Equal( + t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", "1.0"), + attribute.String("http.url", "http://127.0.0.1:8080/resource"), + attribute.String("net.peer.name", "127.0.0.1"), + attribute.Int("net.peer.port", 8080), + attribute.String("http.user_agent", agent), + attribute.Int("http.request_content_length", n), + attribute.String("enduser.id", user), + }, + HTTPClientRequest(req), + ) +} + +func TestHTTPClientRequestRequired(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPClientRequest(req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.flavor", ""), + attribute.String("http.url", ""), + attribute.String("net.peer.name", ""), + } + assert.Equal(t, want, got) +} + +func TestHTTPServerRequest(t *testing.T) { + got := make(chan *http.Request, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + got <- r + w.WriteHeader(http.StatusOK) + } + + srv := httptest.NewServer(http.HandlerFunc(handler)) + defer srv.Close() + + srvURL, err := url.Parse(srv.URL) + require.NoError(t, err) + srvPort, err := strconv.ParseInt(srvURL.Port(), 10, 32) + require.NoError(t, err) + + resp, err := srv.Client().Get(srv.URL) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + req := <-got + peer, peerPort := splitHostPort(req.RemoteAddr) + + const user = "alice" + req.SetBasicAuth(user, "pswrd") + + const clientIP = "127.0.0.5" + req.Header.Add("X-Forwarded-For", clientIP) + + assert.ElementsMatch(t, + []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", "1.1"), + attribute.String("net.host.name", srvURL.Hostname()), + attribute.Int("net.host.port", int(srvPort)), + attribute.String("net.sock.peer.addr", peer), + attribute.Int("net.sock.peer.port", peerPort), + attribute.String("http.user_agent", "Go-http-client/1.1"), + attribute.String("enduser.id", user), + attribute.String("http.client_ip", clientIP), + }, + HTTPServerRequest("", req)) +} + +func TestHTTPServerName(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + const ( + host = "test.semconv.server" + port = 8080 + ) + portStr := strconv.Itoa(port) + server := host + ":" + portStr + assert.NotPanics(t, func() { got = HTTPServerRequest(server, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) + + req = &http.Request{Host: "alt.host.name:" + portStr} + // The server parameter does not include a port, ServerRequest should use + // the port in the request Host field. + assert.NotPanics(t, func() { got = HTTPServerRequest(host, req) }) + assert.Contains(t, got, attribute.String("net.host.name", host)) + assert.Contains(t, got, attribute.Int("net.host.port", port)) +} + +func TestHTTPServerRequestFailsGracefully(t *testing.T) { + req := new(http.Request) + var got []attribute.KeyValue + assert.NotPanics(t, func() { got = HTTPServerRequest("", req) }) + want := []attribute.KeyValue{ + attribute.String("http.method", "GET"), + attribute.String("http.scheme", "http"), + attribute.String("http.flavor", ""), + attribute.String("net.host.name", ""), + } + assert.ElementsMatch(t, want, got) +} + +func TestHTTPMethod(t *testing.T) { + assert.Equal(t, attribute.String("http.method", "POST"), hc.method("POST")) + assert.Equal(t, attribute.String("http.method", "GET"), hc.method("")) + assert.Equal(t, attribute.String("http.method", "garbage"), hc.method("garbage")) +} + +func TestHTTPScheme(t *testing.T) { + assert.Equal(t, attribute.String("http.scheme", "http"), hc.scheme(false)) + assert.Equal(t, attribute.String("http.scheme", "https"), hc.scheme(true)) +} + +func TestHTTPProto(t *testing.T) { + tests := map[string]string{ + "HTTP/1.0": "1.0", + "HTTP/1.1": "1.1", + "HTTP/2": "2.0", + "HTTP/3": "3.0", + "SPDY": "SPDY", + "QUIC": "QUIC", + "other": "other", + } + + for proto, want := range tests { + expect := attribute.String("http.flavor", want) + assert.Equal(t, expect, hc.proto(proto), proto) + } +} + +func TestHTTPServerClientIP(t *testing.T) { + tests := []struct { + xForwardedFor string + want string + }{ + {"", ""}, + {"127.0.0.1", "127.0.0.1"}, + {"127.0.0.1,127.0.0.5", "127.0.0.1"}, + } + for _, test := range tests { + got := serverClientIP(test.xForwardedFor) + assert.Equal(t, test.want, got, test.xForwardedFor) + } +} + +func TestRequiredHTTPPort(t *testing.T) { + tests := []struct { + https bool + port int + want int + }{ + {true, 443, -1}, + {true, 80, 80}, + {true, 8081, 8081}, + {false, 443, 443}, + {false, 80, -1}, + {false, 8080, 8080}, + } + for _, test := range tests { + got := requiredHTTPPort(test.https, test.port) + assert.Equal(t, test.want, got, test.https, test.port) + } +} + +func TestFirstHostPort(t *testing.T) { + host, port := "127.0.0.1", 8080 + hostport := "127.0.0.1:8080" + sources := [][]string{ + {hostport}, + {"", hostport}, + {"", "", hostport}, + {"", "", hostport, ""}, + {"", "", hostport, "127.0.0.3:80"}, + } + + for _, src := range sources { + h, p := firstHostPort(src...) + assert.Equal(t, host, h, src) + assert.Equal(t, port, p, src) + } +} + +func TestHTTPRequestHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPRequestHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.request.header.ips", ips), + attribute.StringSlice("http.request.header.user", user), + }, got) +} + +func TestHTTPReponseHeader(t *testing.T) { + ips := []string{"127.0.0.5", "127.0.0.9"} + user := []string{"alice"} + h := http.Header{"ips": ips, "user": user} + + got := HTTPResponseHeader(h) + assert.Equal(t, 2, cap(got), "slice capacity") + assert.ElementsMatch(t, []attribute.KeyValue{ + attribute.StringSlice("http.response.header.ips", ips), + attribute.StringSlice("http.response.header.user", user), + }, got) +} + +func TestHTTPClientStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Error, false}, + {http.StatusUnauthorized, codes.Error, false}, + {http.StatusPaymentRequired, codes.Error, false}, + {http.StatusForbidden, codes.Error, false}, + {http.StatusNotFound, codes.Error, false}, + {http.StatusMethodNotAllowed, codes.Error, false}, + {http.StatusNotAcceptable, codes.Error, false}, + {http.StatusProxyAuthRequired, codes.Error, false}, + {http.StatusRequestTimeout, codes.Error, false}, + {http.StatusConflict, codes.Error, false}, + {http.StatusGone, codes.Error, false}, + {http.StatusLengthRequired, codes.Error, false}, + {http.StatusPreconditionFailed, codes.Error, false}, + {http.StatusRequestEntityTooLarge, codes.Error, false}, + {http.StatusRequestURITooLong, codes.Error, false}, + {http.StatusUnsupportedMediaType, codes.Error, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Error, false}, + {http.StatusExpectationFailed, codes.Error, false}, + {http.StatusTeapot, codes.Error, false}, + {http.StatusMisdirectedRequest, codes.Error, false}, + {http.StatusUnprocessableEntity, codes.Error, false}, + {http.StatusLocked, codes.Error, false}, + {http.StatusFailedDependency, codes.Error, false}, + {http.StatusTooEarly, codes.Error, false}, + {http.StatusUpgradeRequired, codes.Error, false}, + {http.StatusPreconditionRequired, codes.Error, false}, + {http.StatusTooManyRequests, codes.Error, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Error, false}, + {http.StatusUnavailableForLegalReasons, codes.Error, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPClientStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} + +func TestHTTPServerStatus(t *testing.T) { + tests := []struct { + code int + stat codes.Code + msg bool + }{ + {0, codes.Error, true}, + {http.StatusContinue, codes.Unset, false}, + {http.StatusSwitchingProtocols, codes.Unset, false}, + {http.StatusProcessing, codes.Unset, false}, + {http.StatusEarlyHints, codes.Unset, false}, + {http.StatusOK, codes.Unset, false}, + {http.StatusCreated, codes.Unset, false}, + {http.StatusAccepted, codes.Unset, false}, + {http.StatusNonAuthoritativeInfo, codes.Unset, false}, + {http.StatusNoContent, codes.Unset, false}, + {http.StatusResetContent, codes.Unset, false}, + {http.StatusPartialContent, codes.Unset, false}, + {http.StatusMultiStatus, codes.Unset, false}, + {http.StatusAlreadyReported, codes.Unset, false}, + {http.StatusIMUsed, codes.Unset, false}, + {http.StatusMultipleChoices, codes.Unset, false}, + {http.StatusMovedPermanently, codes.Unset, false}, + {http.StatusFound, codes.Unset, false}, + {http.StatusSeeOther, codes.Unset, false}, + {http.StatusNotModified, codes.Unset, false}, + {http.StatusUseProxy, codes.Unset, false}, + {306, codes.Error, true}, + {http.StatusTemporaryRedirect, codes.Unset, false}, + {http.StatusPermanentRedirect, codes.Unset, false}, + {http.StatusBadRequest, codes.Unset, false}, + {http.StatusUnauthorized, codes.Unset, false}, + {http.StatusPaymentRequired, codes.Unset, false}, + {http.StatusForbidden, codes.Unset, false}, + {http.StatusNotFound, codes.Unset, false}, + {http.StatusMethodNotAllowed, codes.Unset, false}, + {http.StatusNotAcceptable, codes.Unset, false}, + {http.StatusProxyAuthRequired, codes.Unset, false}, + {http.StatusRequestTimeout, codes.Unset, false}, + {http.StatusConflict, codes.Unset, false}, + {http.StatusGone, codes.Unset, false}, + {http.StatusLengthRequired, codes.Unset, false}, + {http.StatusPreconditionFailed, codes.Unset, false}, + {http.StatusRequestEntityTooLarge, codes.Unset, false}, + {http.StatusRequestURITooLong, codes.Unset, false}, + {http.StatusUnsupportedMediaType, codes.Unset, false}, + {http.StatusRequestedRangeNotSatisfiable, codes.Unset, false}, + {http.StatusExpectationFailed, codes.Unset, false}, + {http.StatusTeapot, codes.Unset, false}, + {http.StatusMisdirectedRequest, codes.Unset, false}, + {http.StatusUnprocessableEntity, codes.Unset, false}, + {http.StatusLocked, codes.Unset, false}, + {http.StatusFailedDependency, codes.Unset, false}, + {http.StatusTooEarly, codes.Unset, false}, + {http.StatusUpgradeRequired, codes.Unset, false}, + {http.StatusPreconditionRequired, codes.Unset, false}, + {http.StatusTooManyRequests, codes.Unset, false}, + {http.StatusRequestHeaderFieldsTooLarge, codes.Unset, false}, + {http.StatusUnavailableForLegalReasons, codes.Unset, false}, + {http.StatusInternalServerError, codes.Error, false}, + {http.StatusNotImplemented, codes.Error, false}, + {http.StatusBadGateway, codes.Error, false}, + {http.StatusServiceUnavailable, codes.Error, false}, + {http.StatusGatewayTimeout, codes.Error, false}, + {http.StatusHTTPVersionNotSupported, codes.Error, false}, + {http.StatusVariantAlsoNegotiates, codes.Error, false}, + {http.StatusInsufficientStorage, codes.Error, false}, + {http.StatusLoopDetected, codes.Error, false}, + {http.StatusNotExtended, codes.Error, false}, + {http.StatusNetworkAuthenticationRequired, codes.Error, false}, + {600, codes.Error, true}, + } + + for _, test := range tests { + c, msg := HTTPServerStatus(test.code) + assert.Equal(t, test.stat, c) + if test.msg && msg == "" { + t.Errorf("expected non-empty message for %d", test.code) + } else if !test.msg && msg != "" { + t.Errorf("expected empty message for %d, got: %s", test.code, msg) + } + } +} diff --git a/internal/shared/semconvutil/netconv.go.tmpl b/internal/shared/semconvutil/netconv.go.tmpl new file mode 100644 index 00000000000..e5fee78a4a9 --- /dev/null +++ b/internal/shared/semconvutil/netconv.go.tmpl @@ -0,0 +1,368 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv.go.tmpl + +// 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 semconvutil // import "go.opentelemetry.io/contrib/internal/shared/semconvutil" + +import ( + "net" + "strconv" + "strings" + + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" +) + +// NetTransport returns a trace attribute describing the transport protocol of the +// passed network. See the net.Dial for information about acceptable network +// values. +func NetTransport(network string) attribute.KeyValue { + return nc.Transport(network) +} + +// NetClient returns trace attributes for a client network connection to address. +// See net.Dial for information about acceptable address values, address should +// be the same as the one used to create conn. If conn is nil, only network +// peer attributes will be returned that describe address. Otherwise, the +// socket level information about conn will also be included. +func NetClient(address string, conn net.Conn) []attribute.KeyValue { + return nc.Client(address, conn) +} + +// NetServer returns trace attributes for a network listener listening at address. +// See net.Listen for information about acceptable address values, address +// should be the same as the one used to create ln. If ln is nil, only network +// host attributes will be returned that describe address. Otherwise, the +// socket level information about ln will also be included. +func NetServer(address string, ln net.Listener) []attribute.KeyValue { + return nc.Server(address, ln) +} + +// netConv are the network semantic convention attributes defined for a version +// of the OpenTelemetry specification. +type netConv struct { + NetHostNameKey attribute.Key + NetHostPortKey attribute.Key + NetPeerNameKey attribute.Key + NetPeerPortKey attribute.Key + NetSockFamilyKey attribute.Key + NetSockPeerAddrKey attribute.Key + NetSockPeerPortKey attribute.Key + NetSockHostAddrKey attribute.Key + NetSockHostPortKey attribute.Key + NetTransportOther attribute.KeyValue + NetTransportTCP attribute.KeyValue + NetTransportUDP attribute.KeyValue + NetTransportInProc attribute.KeyValue +} + +var nc = &netConv{ + NetHostNameKey: semconv.NetHostNameKey, + NetHostPortKey: semconv.NetHostPortKey, + NetPeerNameKey: semconv.NetPeerNameKey, + NetPeerPortKey: semconv.NetPeerPortKey, + NetSockFamilyKey: semconv.NetSockFamilyKey, + NetSockPeerAddrKey: semconv.NetSockPeerAddrKey, + NetSockPeerPortKey: semconv.NetSockPeerPortKey, + NetSockHostAddrKey: semconv.NetSockHostAddrKey, + NetSockHostPortKey: semconv.NetSockHostPortKey, + NetTransportOther: semconv.NetTransportOther, + NetTransportTCP: semconv.NetTransportTCP, + NetTransportUDP: semconv.NetTransportUDP, + NetTransportInProc: semconv.NetTransportInProc, +} + +func (c *netConv) Transport(network string) attribute.KeyValue { + switch network { + case "tcp", "tcp4", "tcp6": + return c.NetTransportTCP + case "udp", "udp4", "udp6": + return c.NetTransportUDP + case "unix", "unixgram", "unixpacket": + return c.NetTransportInProc + default: + // "ip:*", "ip4:*", and "ip6:*" all are considered other. + return c.NetTransportOther + } +} + +// Host returns attributes for a network host address. +func (c *netConv) Host(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.HostName(h)) + if p > 0 { + attrs = append(attrs, c.HostPort(int(p))) + } + return attrs +} + +// Server returns attributes for a network listener listening at address. See +// net.Listen for information about acceptable address values, address should +// be the same as the one used to create ln. If ln is nil, only network host +// attributes will be returned that describe address. Otherwise, the socket +// level information about ln will also be included. +func (c *netConv) Server(address string, ln net.Listener) []attribute.KeyValue { + if ln == nil { + return c.Host(address) + } + + lAddr := ln.Addr() + if lAddr == nil { + return c.Host(address) + } + + hostName, hostPort := splitHostPort(address) + sockHostAddr, sockHostPort := splitHostPort(lAddr.String()) + network := lAddr.Network() + sockFamily := family(network, sockHostAddr) + + n := nonZeroStr(hostName, network, sockHostAddr, sockFamily) + n += positiveInt(hostPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if hostName != "" { + attr = append(attr, c.HostName(hostName)) + if hostPort > 0 { + // Only if net.host.name is set should net.host.port be. + attr = append(attr, c.HostPort(hostPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func (c *netConv) HostName(name string) attribute.KeyValue { + return c.NetHostNameKey.String(name) +} + +func (c *netConv) HostPort(port int) attribute.KeyValue { + return c.NetHostPortKey.Int(port) +} + +// Client returns attributes for a client network connection to address. See +// net.Dial for information about acceptable address values, address should be +// the same as the one used to create conn. If conn is nil, only network peer +// attributes will be returned that describe address. Otherwise, the socket +// level information about conn will also be included. +func (c *netConv) Client(address string, conn net.Conn) []attribute.KeyValue { + if conn == nil { + return c.Peer(address) + } + + lAddr, rAddr := conn.LocalAddr(), conn.RemoteAddr() + + var network string + switch { + case lAddr != nil: + network = lAddr.Network() + case rAddr != nil: + network = rAddr.Network() + default: + return c.Peer(address) + } + + peerName, peerPort := splitHostPort(address) + var ( + sockFamily string + sockPeerAddr string + sockPeerPort int + sockHostAddr string + sockHostPort int + ) + + if lAddr != nil { + sockHostAddr, sockHostPort = splitHostPort(lAddr.String()) + } + + if rAddr != nil { + sockPeerAddr, sockPeerPort = splitHostPort(rAddr.String()) + } + + switch { + case sockHostAddr != "": + sockFamily = family(network, sockHostAddr) + case sockPeerAddr != "": + sockFamily = family(network, sockPeerAddr) + } + + n := nonZeroStr(peerName, network, sockPeerAddr, sockHostAddr, sockFamily) + n += positiveInt(peerPort, sockPeerPort, sockHostPort) + attr := make([]attribute.KeyValue, 0, n) + if peerName != "" { + attr = append(attr, c.PeerName(peerName)) + if peerPort > 0 { + // Only if net.peer.name is set should net.peer.port be. + attr = append(attr, c.PeerPort(peerPort)) + } + } + if network != "" { + attr = append(attr, c.Transport(network)) + } + if sockFamily != "" { + attr = append(attr, c.NetSockFamilyKey.String(sockFamily)) + } + if sockPeerAddr != "" { + attr = append(attr, c.NetSockPeerAddrKey.String(sockPeerAddr)) + if sockPeerPort > 0 { + // Only if net.sock.peer.addr is set should net.sock.peer.port be. + attr = append(attr, c.NetSockPeerPortKey.Int(sockPeerPort)) + } + } + if sockHostAddr != "" { + attr = append(attr, c.NetSockHostAddrKey.String(sockHostAddr)) + if sockHostPort > 0 { + // Only if net.sock.host.addr is set should net.sock.host.port be. + attr = append(attr, c.NetSockHostPortKey.Int(sockHostPort)) + } + } + return attr +} + +func family(network, address string) string { + switch network { + case "unix", "unixgram", "unixpacket": + return "unix" + default: + if ip := net.ParseIP(address); ip != nil { + if ip.To4() == nil { + return "inet6" + } + return "inet" + } + } + return "" +} + +func nonZeroStr(strs ...string) int { + var n int + for _, str := range strs { + if str != "" { + n++ + } + } + return n +} + +func positiveInt(ints ...int) int { + var n int + for _, i := range ints { + if i > 0 { + n++ + } + } + return n +} + +// Peer returns attributes for a network peer address. +func (c *netConv) Peer(address string) []attribute.KeyValue { + h, p := splitHostPort(address) + var n int + if h != "" { + n++ + if p > 0 { + n++ + } + } + + if n == 0 { + return nil + } + + attrs := make([]attribute.KeyValue, 0, n) + attrs = append(attrs, c.PeerName(h)) + if p > 0 { + attrs = append(attrs, c.PeerPort(int(p))) + } + return attrs +} + +func (c *netConv) PeerName(name string) attribute.KeyValue { + return c.NetPeerNameKey.String(name) +} + +func (c *netConv) PeerPort(port int) attribute.KeyValue { + return c.NetPeerPortKey.Int(port) +} + +func (c *netConv) SockPeerAddr(addr string) attribute.KeyValue { + return c.NetSockPeerAddrKey.String(addr) +} + +func (c *netConv) SockPeerPort(port int) attribute.KeyValue { + return c.NetSockPeerPortKey.Int(port) +} + +// splitHostPort splits a network address hostport of the form "host", +// "host%zone", "[host]", "[host%zone], "host:port", "host%zone:port", +// "[host]:port", "[host%zone]:port", or ":port" into host or host%zone and +// port. +// +// An empty host is returned if it is not provided or unparsable. A negative +// port is returned if it is not provided or unparsable. +func splitHostPort(hostport string) (host string, port int) { + port = -1 + + if strings.HasPrefix(hostport, "[") { + addrEnd := strings.LastIndex(hostport, "]") + if addrEnd < 0 { + // Invalid hostport. + return + } + if i := strings.LastIndex(hostport[addrEnd:], ":"); i < 0 { + host = hostport[1:addrEnd] + return + } + } else { + if i := strings.LastIndex(hostport, ":"); i < 0 { + host = hostport + return + } + } + + host, pStr, err := net.SplitHostPort(hostport) + if err != nil { + return + } + + p, err := strconv.ParseUint(pStr, 10, 16) + if err != nil { + return + } + return host, int(p) +} diff --git a/internal/shared/semconvutil/netconv_test.go.tmpl b/internal/shared/semconvutil/netconv_test.go.tmpl new file mode 100644 index 00000000000..680e7a000f3 --- /dev/null +++ b/internal/shared/semconvutil/netconv_test.go.tmpl @@ -0,0 +1,334 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/semconvutil/netconv_test.go.tmpl + +// 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 semconvutil + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/attribute" +) + +const ( + addr = "127.0.0.1" + port = 1834 +) + +func TestNetTransport(t *testing.T) { + transports := map[string]attribute.KeyValue{ + "tcp": attribute.String("net.transport", "ip_tcp"), + "tcp4": attribute.String("net.transport", "ip_tcp"), + "tcp6": attribute.String("net.transport", "ip_tcp"), + "udp": attribute.String("net.transport", "ip_udp"), + "udp4": attribute.String("net.transport", "ip_udp"), + "udp6": attribute.String("net.transport", "ip_udp"), + "unix": attribute.String("net.transport", "inproc"), + "unixgram": attribute.String("net.transport", "inproc"), + "unixpacket": attribute.String("net.transport", "inproc"), + "ip:1": attribute.String("net.transport", "other"), + "ip:icmp": attribute.String("net.transport", "other"), + "ip4:proto": attribute.String("net.transport", "other"), + "ip6:proto": attribute.String("net.transport", "other"), + } + + for network, want := range transports { + assert.Equal(t, want, NetTransport(network)) + } +} + +func TestNetServerNilListener(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, nil) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type listener struct{ net.Listener } + +func (listener) Addr() net.Addr { return nil } + +func TestNetServerNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetServer(addr, listener{}) + expected := nc.Host(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPListener() (net.Listener, error) { + return net.Listen("tcp4", "127.0.0.1:0") +} + +func TestNetServerTCP(t *testing.T) { + ln, err := newTCPListener() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + + host, pStr, err := net.SplitHostPort(ln.Addr().String()) + require.NoError(t, err) + port, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetServer("example.com:8080", ln) + expected := []attribute.KeyValue{ + nc.HostName("example.com"), + nc.HostPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockHostAddrKey.String(host), + nc.NetSockHostPortKey.Int(port), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetHost(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + }}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.HostName("192.0.0.1"), + nc.HostPort(9090), + }}, + }, nc.Host) +} + +func TestNetHostName(t *testing.T) { + expected := attribute.Key("net.host.name").String(addr) + assert.Equal(t, expected, nc.HostName(addr)) +} + +func TestNetHostPort(t *testing.T) { + expected := attribute.Key("net.host.port").Int(port) + assert.Equal(t, expected, nc.HostPort(port)) +} + +func TestNetClientNilConn(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, nil) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type conn struct{ net.Conn } + +func (conn) LocalAddr() net.Addr { return nil } +func (conn) RemoteAddr() net.Addr { return nil } + +func TestNetClientNilAddr(t *testing.T) { + const addr = "127.0.0.1:8080" + got := NetClient(addr, conn{}) + expected := nc.Peer(addr) + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func newTCPConn() (net.Conn, net.Listener, error) { + ln, err := newTCPListener() + if err != nil { + return nil, nil, err + } + + conn, err := net.Dial("tcp4", ln.Addr().String()) + if err != nil { + _ = ln.Close() + return nil, nil, err + } + + return conn, ln, nil +} + +func TestNetClientTCP(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + lHost, pStr, err := net.SplitHostPort(conn.LocalAddr().String()) + require.NoError(t, err) + lPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + nc.NetSockHostAddrKey.String(lHost), + nc.NetSockHostPortKey.Int(lPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +type remoteOnlyConn struct{ net.Conn } + +func (remoteOnlyConn) LocalAddr() net.Addr { return nil } + +func TestNetClientTCPNilLocal(t *testing.T) { + conn, ln, err := newTCPConn() + require.NoError(t, err) + defer func() { require.NoError(t, ln.Close()) }() + defer func() { require.NoError(t, conn.Close()) }() + + conn = remoteOnlyConn{conn} + + rHost, pStr, err := net.SplitHostPort(conn.RemoteAddr().String()) + require.NoError(t, err) + rPort, err := strconv.Atoi(pStr) + require.NoError(t, err) + + got := NetClient("example.com:8080", conn) + expected := []attribute.KeyValue{ + nc.PeerName("example.com"), + nc.PeerPort(8080), + nc.NetTransportTCP, + nc.NetSockFamilyKey.String("inet"), + nc.NetSockPeerAddrKey.String(rHost), + nc.NetSockPeerPortKey.Int(rPort), + } + assert.Equal(t, cap(expected), cap(got), "slice capacity") + assert.ElementsMatch(t, expected, got) +} + +func TestNetPeer(t *testing.T) { + testAddrs(t, []addrTest{ + {address: "", expected: nil}, + {address: "example.com", expected: []attribute.KeyValue{ + nc.PeerName("example.com"), + }}, + {address: "/tmp/file", expected: []attribute.KeyValue{ + nc.PeerName("/tmp/file"), + }}, + {address: "192.0.0.1", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + }}, + {address: ":9090", expected: nil}, + {address: "192.0.0.1:9090", expected: []attribute.KeyValue{ + nc.PeerName("192.0.0.1"), + nc.PeerPort(9090), + }}, + }, nc.Peer) +} + +func TestNetPeerName(t *testing.T) { + expected := attribute.Key("net.peer.name").String(addr) + assert.Equal(t, expected, nc.PeerName(addr)) +} + +func TestNetPeerPort(t *testing.T) { + expected := attribute.Key("net.peer.port").Int(port) + assert.Equal(t, expected, nc.PeerPort(port)) +} + +func TestNetSockPeerName(t *testing.T) { + expected := attribute.Key("net.sock.peer.addr").String(addr) + assert.Equal(t, expected, nc.SockPeerAddr(addr)) +} + +func TestNetSockPeerPort(t *testing.T) { + expected := attribute.Key("net.sock.peer.port").Int(port) + assert.Equal(t, expected, nc.SockPeerPort(port)) +} + +func TestNetFamily(t *testing.T) { + tests := []struct { + network string + address string + expect string + }{ + {"", "", ""}, + {"unix", "", "unix"}, + {"unix", "gibberish", "unix"}, + {"unixgram", "", "unix"}, + {"unixgram", "gibberish", "unix"}, + {"unixpacket", "gibberish", "unix"}, + {"tcp", "123.0.2.8", "inet"}, + {"tcp", "gibberish", ""}, + {"", "123.0.2.8", "inet"}, + {"", "gibberish", ""}, + {"tcp", "fe80::1", "inet6"}, + {"", "fe80::1", "inet6"}, + } + + for _, test := range tests { + got := family(test.network, test.address) + assert.Equal(t, test.expect, got, test.network+"/"+test.address) + } +} + +func TestSplitHostPort(t *testing.T) { + tests := []struct { + hostport string + host string + port int + }{ + {"", "", -1}, + {":8080", "", 8080}, + {"127.0.0.1", "127.0.0.1", -1}, + {"www.example.com", "www.example.com", -1}, + {"127.0.0.1%25en0", "127.0.0.1%25en0", -1}, + {"[]", "", -1}, // Ensure this doesn't panic. + {"[fe80::1", "", -1}, + {"[fe80::1]", "fe80::1", -1}, + {"[fe80::1%25en0]", "fe80::1%25en0", -1}, + {"[fe80::1]:8080", "fe80::1", 8080}, + {"[fe80::1]::", "", -1}, // Too many colons. + {"127.0.0.1:", "127.0.0.1", -1}, + {"127.0.0.1:port", "127.0.0.1", -1}, + {"127.0.0.1:8080", "127.0.0.1", 8080}, + {"www.example.com:8080", "www.example.com", 8080}, + {"127.0.0.1%25en0:8080", "127.0.0.1%25en0", 8080}, + } + + for _, test := range tests { + h, p := splitHostPort(test.hostport) + assert.Equal(t, test.host, h, test.hostport) + assert.Equal(t, test.port, p, test.hostport) + } +} + +type addrTest struct { + address string + expected []attribute.KeyValue +} + +func testAddrs(t *testing.T, tests []addrTest, f func(string) []attribute.KeyValue) { + t.Helper() + + for _, test := range tests { + got := f(test.address) + assert.Equal(t, cap(test.expected), cap(got), "slice capacity") + assert.ElementsMatch(t, test.expected, got, test.address) + } +} diff --git a/tools/go.mod b/tools/go.mod index 454f87b18d8..5c45dd0298a 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -11,6 +11,7 @@ require ( github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad go.opentelemetry.io/build-tools/crosslink v0.7.0 go.opentelemetry.io/build-tools/dbotconf v0.7.0 + go.opentelemetry.io/build-tools/gotmpl v0.7.0 go.opentelemetry.io/build-tools/multimod v0.7.0 golang.org/x/tools v0.9.1 ) diff --git a/tools/go.sum b/tools/go.sum index cce0f0ef245..330d3d015e8 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -609,6 +609,8 @@ go.opentelemetry.io/build-tools/crosslink v0.7.0 h1:N7Cue57OJ+MtmBxN9HxJhy6L3xsM go.opentelemetry.io/build-tools/crosslink v0.7.0/go.mod h1:4b21YR5+5/qwGmzOwIeTKoQayCX5tlnQKDal2POYZR8= go.opentelemetry.io/build-tools/dbotconf v0.7.0 h1:6EV61QuJGB5Xz0YyPC7V3yO+0ZGB3SLQPBq3fb1Y1mU= go.opentelemetry.io/build-tools/dbotconf v0.7.0/go.mod h1:7HqRvzmX+8wt8xy3zwSCSWufeT8gJiWhICpzNxn93yk= +go.opentelemetry.io/build-tools/gotmpl v0.7.0 h1:BWdcgsuKQOwQ9BS/dd5MrOxMo9tjuAlIczIBCoqmJXw= +go.opentelemetry.io/build-tools/gotmpl v0.7.0/go.mod h1:3PcJ8l/uckT0hG7vxM8S54mmyq4GXw0Xr63jZS3eynE= go.opentelemetry.io/build-tools/multimod v0.7.0 h1:Af1wVNaJRMVqRkiiRjeMrAfKJDejvpsLoBF9H5UrjSo= go.opentelemetry.io/build-tools/multimod v0.7.0/go.mod h1:WAwBtJC42JbRDzfi3erCYQ9M1RnyoIcqI/H8eqBBPQo= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= diff --git a/tools/tools.go b/tools/tools.go index 34f859fcc3b..c0090ee9f0b 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -24,6 +24,7 @@ import ( _ "github.com/wadey/gocovmerge" _ "go.opentelemetry.io/build-tools/crosslink" _ "go.opentelemetry.io/build-tools/dbotconf" + _ "go.opentelemetry.io/build-tools/gotmpl" _ "go.opentelemetry.io/build-tools/multimod" _ "golang.org/x/tools/cmd/stringer" )