1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-11-30 08:46:54 +02:00

Merge pull request #769 from Aneurysm9/http_semantics

Integrate HTTP semantics helpers from contrib
This commit is contained in:
Tyler Yahn 2020-05-27 11:08:49 -07:00 committed by GitHub
commit 49043f6f97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1101 additions and 37 deletions

303
api/standard/http.go Normal file
View File

@ -0,0 +1,303 @@
// 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 standard
import (
"fmt"
"net"
"net/http"
"strconv"
"strings"
"google.golang.org/grpc/codes"
"go.opentelemetry.io/otel/api/kv"
)
// NetAttributesFromHTTPRequest generates attributes of the net
// namespace as specified by the OpenTelemetry specification for a
// span. The network parameter is a string that net.Dial function
// from standard library can understand.
func NetAttributesFromHTTPRequest(network string, request *http.Request) []kv.KeyValue {
attrs := []kv.KeyValue{}
switch network {
case "tcp", "tcp4", "tcp6":
attrs = append(attrs, NetTransportTCP)
case "udp", "udp4", "udp6":
attrs = append(attrs, NetTransportUDP)
case "ip", "ip4", "ip6":
attrs = append(attrs, NetTransportIP)
case "unix", "unixgram", "unixpacket":
attrs = append(attrs, NetTransportUnix)
default:
attrs = append(attrs, NetTransportOther)
}
peerName, peerIP, peerPort := "", "", 0
{
hostPart := request.RemoteAddr
portPart := ""
if idx := strings.LastIndex(hostPart, ":"); idx >= 0 {
hostPart = request.RemoteAddr[:idx]
portPart = request.RemoteAddr[idx+1:]
}
if hostPart != "" {
if ip := net.ParseIP(hostPart); ip != nil {
peerIP = ip.String()
} else {
peerName = hostPart
}
if portPart != "" {
numPort, err := strconv.ParseUint(portPart, 10, 16)
if err == nil {
peerPort = (int)(numPort)
} else {
peerName, peerIP = "", ""
}
}
}
}
if peerName != "" {
attrs = append(attrs, NetPeerNameKey.String(peerName))
}
if peerIP != "" {
attrs = append(attrs, NetPeerIPKey.String(peerIP))
}
if peerPort != 0 {
attrs = append(attrs, NetPeerPortKey.Int(peerPort))
}
hostIP, hostName, hostPort := "", "", 0
for _, someHost := range []string{request.Host, request.Header.Get("Host"), request.URL.Host} {
hostPart := ""
if idx := strings.LastIndex(someHost, ":"); idx >= 0 {
strPort := someHost[idx+1:]
numPort, err := strconv.ParseUint(strPort, 10, 16)
if err == nil {
hostPort = (int)(numPort)
}
hostPart = someHost[:idx]
} else {
hostPart = someHost
}
if hostPart != "" {
ip := net.ParseIP(hostPart)
if ip != nil {
hostIP = ip.String()
} else {
hostName = hostPart
}
break
} else {
hostPort = 0
}
}
if hostIP != "" {
attrs = append(attrs, NetHostIPKey.String(hostIP))
}
if hostName != "" {
attrs = append(attrs, NetHostNameKey.String(hostName))
}
if hostPort != 0 {
attrs = append(attrs, NetHostPortKey.Int(hostPort))
}
return attrs
}
// EndUserAttributesFromHTTPRequest generates attributes of the
// enduser namespace as specified by the OpenTelemetry specification
// for a span.
func EndUserAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
if username, _, ok := request.BasicAuth(); ok {
return []kv.KeyValue{EnduserIDKey.String(username)}
}
return nil
}
// HTTPClientAttributesFromHTTPRequest generates attributes of the
// http namespace as specified by the OpenTelemetry specification for
// a span on the client side.
func HTTPClientAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
attrs := []kv.KeyValue{}
if request.Method != "" {
attrs = append(attrs, HTTPMethodKey.String(request.Method))
} else {
attrs = append(attrs, HTTPMethodKey.String(http.MethodGet))
}
attrs = append(attrs, HTTPUrlKey.String(request.URL.String()))
return append(attrs, httpCommonAttributesFromHTTPRequest(request)...)
}
func httpCommonAttributesFromHTTPRequest(request *http.Request) []kv.KeyValue {
attrs := []kv.KeyValue{}
if request.TLS != nil {
attrs = append(attrs, HTTPSchemeHTTPS)
} else {
attrs = append(attrs, HTTPSchemeHTTP)
}
if request.Host != "" {
attrs = append(attrs, HTTPHostKey.String(request.Host))
}
if ua := request.UserAgent(); ua != "" {
attrs = append(attrs, HTTPUserAgentKey.String(ua))
}
flavor := ""
if request.ProtoMajor == 1 {
flavor = fmt.Sprintf("1.%d", request.ProtoMinor)
} else if request.ProtoMajor == 2 {
flavor = "2"
}
if flavor != "" {
attrs = append(attrs, HTTPFlavorKey.String(flavor))
}
return attrs
}
// HTTPServerAttributesFromHTTPRequest generates attributes of the
// http namespace as specified by the OpenTelemetry specification for
// a span on the server side. Currently, only basic authentication is
// supported.
func HTTPServerAttributesFromHTTPRequest(serverName, route string, request *http.Request) []kv.KeyValue {
attrs := []kv.KeyValue{
HTTPMethodKey.String(request.Method),
HTTPTargetKey.String(request.RequestURI),
}
if serverName != "" {
attrs = append(attrs, HTTPServerNameKey.String(serverName))
}
if route != "" {
attrs = append(attrs, HTTPRouteKey.String(route))
}
if values, ok := request.Header["X-Forwarded-For"]; ok && len(values) > 0 {
attrs = append(attrs, HTTPClientIPKey.String(values[0]))
}
return append(attrs, httpCommonAttributesFromHTTPRequest(request)...)
}
// HTTPAttributesFromHTTPStatusCode generates attributes of the http
// namespace as specified by the OpenTelemetry specification for a
// span.
func HTTPAttributesFromHTTPStatusCode(code int) []kv.KeyValue {
attrs := []kv.KeyValue{
HTTPStatusCodeKey.Int(code),
}
text := http.StatusText(code)
if text != "" {
attrs = append(attrs, HTTPStatusTextKey.String(text))
}
return attrs
}
type codeRange struct {
fromInclusive int
toInclusive int
}
func (r codeRange) contains(code int) bool {
return r.fromInclusive <= code && code <= r.toInclusive
}
var validRangesPerCategory = map[int][]codeRange{
1: {
{http.StatusContinue, http.StatusEarlyHints},
},
2: {
{http.StatusOK, http.StatusAlreadyReported},
{http.StatusIMUsed, http.StatusIMUsed},
},
3: {
{http.StatusMultipleChoices, http.StatusUseProxy},
{http.StatusTemporaryRedirect, http.StatusPermanentRedirect},
},
4: {
{http.StatusBadRequest, http.StatusTeapot}, // yes, teapot is so useful…
{http.StatusMisdirectedRequest, http.StatusUpgradeRequired},
{http.StatusPreconditionRequired, http.StatusTooManyRequests},
{http.StatusRequestHeaderFieldsTooLarge, http.StatusRequestHeaderFieldsTooLarge},
{http.StatusUnavailableForLegalReasons, http.StatusUnavailableForLegalReasons},
},
5: {
{http.StatusInternalServerError, http.StatusLoopDetected},
{http.StatusNotExtended, http.StatusNetworkAuthenticationRequired},
},
}
// SpanStatusFromHTTPStatusCode generates a status code and a message
// as specified by the OpenTelemetry specification for a span.
func SpanStatusFromHTTPStatusCode(code int) (codes.Code, string) {
spanCode := func() codes.Code {
category := code / 100
ranges, ok := validRangesPerCategory[category]
if !ok {
return codes.Unknown
}
ok = false
for _, crange := range ranges {
ok = crange.contains(code)
if ok {
break
}
}
if !ok {
return codes.Unknown
}
switch code {
case http.StatusUnauthorized:
return codes.Unauthenticated
case http.StatusForbidden:
return codes.PermissionDenied
case http.StatusNotFound:
return codes.NotFound
case http.StatusTooManyRequests:
return codes.ResourceExhausted
case http.StatusNotImplemented:
return codes.Unimplemented
case http.StatusServiceUnavailable:
return codes.Unavailable
case http.StatusGatewayTimeout:
return codes.DeadlineExceeded
}
if category > 0 && category < 4 {
return codes.OK
}
if category == 4 {
return codes.InvalidArgument
}
if category == 5 {
return codes.Internal
}
// this really should not happen, if we get there then
// it means that the code got out of sync with
// validRangesPerCategory map
return codes.Unknown
}()
if spanCode == codes.Unknown {
return spanCode, fmt.Sprintf("Invalid HTTP status code %d", code)
}
return spanCode, fmt.Sprintf("HTTP status code: %d", code)
}

777
api/standard/http_test.go Normal file
View File

@ -0,0 +1,777 @@
// 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 standard
import (
"crypto/tls"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
otelkv "go.opentelemetry.io/otel/api/kv"
)
type tlsOption int
const (
noTLS tlsOption = iota
withTLS
)
func TestNetAttributesFromHTTPRequest(t *testing.T) {
type testcase struct {
name string
network string
method string
requestURI string
proto string
remoteAddr string
host string
url *url.URL
header http.Header
expected []otelkv.KeyValue
}
testcases := []testcase{
{
name: "stripped, tcp",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
},
},
{
name: "stripped, udp",
network: "udp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.UDP"),
},
},
{
name: "stripped, ip",
network: "ip",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP"),
},
},
{
name: "stripped, unix",
network: "unix",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "Unix"),
},
},
{
name: "stripped, other",
network: "nih",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "other"),
},
},
{
name: "with remote ip and port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
},
},
{
name: "with remote name and port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "example.com:56",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.name", "example.com"),
otelkv.Int("net.peer.port", 56),
},
},
{
name: "with remote ip only",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
},
},
{
name: "with remote name only",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "example.com",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.name", "example.com"),
},
},
{
name: "with remote port only",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: ":56",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
},
},
{
name: "with host name only",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "example.com",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.name", "example.com"),
},
},
{
name: "with host ip only",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "4.3.2.1",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.ip", "4.3.2.1"),
},
},
{
name: "with host name and port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "example.com:78",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.name", "example.com"),
otelkv.Int("net.host.port", 78),
},
},
{
name: "with host ip and port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "4.3.2.1:78",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.ip", "4.3.2.1"),
otelkv.Int("net.host.port", 78),
},
},
{
name: "with host name and bogus port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "example.com:qwerty",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.name", "example.com"),
},
},
{
name: "with host ip and bogus port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "4.3.2.1:qwerty",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.ip", "4.3.2.1"),
},
},
{
name: "with empty host and port",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: ":80",
url: &url.URL{
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
},
},
{
name: "with host ip and port in headers",
network: "tcp",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: http.Header{
"Host": []string{"4.3.2.1:78"},
},
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.ip", "4.3.2.1"),
otelkv.Int("net.host.port", 78),
},
},
{
name: "with host ip and port in url",
network: "tcp",
method: "GET",
requestURI: "http://4.3.2.1:78/user/123",
proto: "HTTP/1.0",
remoteAddr: "1.2.3.4:56",
host: "",
url: &url.URL{
Host: "4.3.2.1:78",
Path: "/user/123",
},
header: nil,
expected: []otelkv.KeyValue{
otelkv.String("net.transport", "IP.TCP"),
otelkv.String("net.peer.ip", "1.2.3.4"),
otelkv.Int("net.peer.port", 56),
otelkv.String("net.host.ip", "4.3.2.1"),
otelkv.Int("net.host.port", 78),
},
},
}
for idx, tc := range testcases {
r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, noTLS)
got := NetAttributesFromHTTPRequest(tc.network, r)
assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name)
}
}
func TestEndUserAttributesFromHTTPRequest(t *testing.T) {
r := testRequest("GET", "/user/123", "HTTP/1.1", "", "", nil, http.Header{}, withTLS)
var expected []otelkv.KeyValue
got := EndUserAttributesFromHTTPRequest(r)
assert.ElementsMatch(t, expected, got)
r.SetBasicAuth("admin", "password")
expected = []otelkv.KeyValue{otelkv.String("enduser.id", "admin")}
got = EndUserAttributesFromHTTPRequest(r)
assert.ElementsMatch(t, expected, got)
}
func TestHTTPServerAttributesFromHTTPRequest(t *testing.T) {
type testcase struct {
name string
serverName string
route string
method string
requestURI string
proto string
remoteAddr string
host string
url *url.URL
header http.Header
tls tlsOption
expected []otelkv.KeyValue
}
testcases := []testcase{
{
name: "stripped",
serverName: "",
route: "",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
tls: noTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "http"),
otelkv.String("http.flavor", "1.0"),
},
},
{
name: "with server name",
serverName: "my-server-name",
route: "",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
tls: noTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "http"),
otelkv.String("http.flavor", "1.0"),
otelkv.String("http.server_name", "my-server-name"),
},
},
{
name: "with tls",
serverName: "my-server-name",
route: "",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "1.0"),
otelkv.String("http.server_name", "my-server-name"),
},
},
{
name: "with route",
serverName: "my-server-name",
route: "/user/:id",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "",
url: &url.URL{
Path: "/user/123",
},
header: nil,
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "1.0"),
otelkv.String("http.server_name", "my-server-name"),
otelkv.String("http.route", "/user/:id"),
},
},
{
name: "with host",
serverName: "my-server-name",
route: "/user/:id",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "example.com",
url: &url.URL{
Path: "/user/123",
},
header: nil,
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "1.0"),
otelkv.String("http.server_name", "my-server-name"),
otelkv.String("http.route", "/user/:id"),
otelkv.String("http.host", "example.com"),
},
},
{
name: "with user agent",
serverName: "my-server-name",
route: "/user/:id",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "example.com",
url: &url.URL{
Path: "/user/123",
},
header: http.Header{
"User-Agent": []string{"foodownloader"},
},
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "1.0"),
otelkv.String("http.server_name", "my-server-name"),
otelkv.String("http.route", "/user/:id"),
otelkv.String("http.host", "example.com"),
otelkv.String("http.user_agent", "foodownloader"),
},
},
{
name: "with proxy info",
serverName: "my-server-name",
route: "/user/:id",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.0",
remoteAddr: "",
host: "example.com",
url: &url.URL{
Path: "/user/123",
},
header: http.Header{
"User-Agent": []string{"foodownloader"},
"X-Forwarded-For": []string{"1.2.3.4"},
},
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "1.0"),
otelkv.String("http.server_name", "my-server-name"),
otelkv.String("http.route", "/user/:id"),
otelkv.String("http.host", "example.com"),
otelkv.String("http.user_agent", "foodownloader"),
otelkv.String("http.client_ip", "1.2.3.4"),
},
},
{
name: "with http 1.1",
serverName: "my-server-name",
route: "/user/:id",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/1.1",
remoteAddr: "",
host: "example.com",
url: &url.URL{
Path: "/user/123",
},
header: http.Header{
"User-Agent": []string{"foodownloader"},
"X-Forwarded-For": []string{"1.2.3.4"},
},
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "1.1"),
otelkv.String("http.server_name", "my-server-name"),
otelkv.String("http.route", "/user/:id"),
otelkv.String("http.host", "example.com"),
otelkv.String("http.user_agent", "foodownloader"),
otelkv.String("http.client_ip", "1.2.3.4"),
},
},
{
name: "with http 2",
serverName: "my-server-name",
route: "/user/:id",
method: "GET",
requestURI: "/user/123",
proto: "HTTP/2.0",
remoteAddr: "",
host: "example.com",
url: &url.URL{
Path: "/user/123",
},
header: http.Header{
"User-Agent": []string{"foodownloader"},
"X-Forwarded-For": []string{"1.2.3.4"},
},
tls: withTLS,
expected: []otelkv.KeyValue{
otelkv.String("http.method", "GET"),
otelkv.String("http.target", "/user/123"),
otelkv.String("http.scheme", "https"),
otelkv.String("http.flavor", "2"),
otelkv.String("http.server_name", "my-server-name"),
otelkv.String("http.route", "/user/:id"),
otelkv.String("http.host", "example.com"),
otelkv.String("http.user_agent", "foodownloader"),
otelkv.String("http.client_ip", "1.2.3.4"),
},
},
}
for idx, tc := range testcases {
r := testRequest(tc.method, tc.requestURI, tc.proto, tc.remoteAddr, tc.host, tc.url, tc.header, tc.tls)
got := HTTPServerAttributesFromHTTPRequest(tc.serverName, tc.route, r)
assertElementsMatch(t, tc.expected, got, "testcase %d - %s", idx, tc.name)
}
}
func TestHTTPAttributesFromHTTPStatusCode(t *testing.T) {
expected := []otelkv.KeyValue{
otelkv.Int("http.status_code", 404),
otelkv.String("http.status_text", "Not Found"),
}
got := HTTPAttributesFromHTTPStatusCode(http.StatusNotFound)
assertElementsMatch(t, expected, got, "with valid HTTP status code")
assert.ElementsMatch(t, expected, got)
expected = []otelkv.KeyValue{
otelkv.Int("http.status_code", 499),
}
got = HTTPAttributesFromHTTPStatusCode(499)
assertElementsMatch(t, expected, got, "with invalid HTTP status code")
}
func TestSpanStatusFromHTTPStatusCode(t *testing.T) {
for code := 0; code < 1000; code++ {
expected := getExpectedGRPCCodeForHTTPCode(code)
got, _ := SpanStatusFromHTTPStatusCode(code)
assert.Equalf(t, expected, got, "%s vs %s", expected, got)
}
}
func getExpectedGRPCCodeForHTTPCode(code int) codes.Code {
if http.StatusText(code) == "" {
return codes.Unknown
}
switch code {
case http.StatusUnauthorized:
return codes.Unauthenticated
case http.StatusForbidden:
return codes.PermissionDenied
case http.StatusNotFound:
return codes.NotFound
case http.StatusTooManyRequests:
return codes.ResourceExhausted
case http.StatusNotImplemented:
return codes.Unimplemented
case http.StatusServiceUnavailable:
return codes.Unavailable
case http.StatusGatewayTimeout:
return codes.DeadlineExceeded
}
category := code / 100
if category < 4 {
return codes.OK
}
if category < 5 {
return codes.InvalidArgument
}
return codes.Internal
}
func assertElementsMatch(t *testing.T, expected, got []otelkv.KeyValue, format string, args ...interface{}) {
if !assert.ElementsMatchf(t, expected, got, format, args...) {
t.Log("expected:", kvStr(expected))
t.Log("got:", kvStr(got))
}
}
func testRequest(method, requestURI, proto, remoteAddr, host string, u *url.URL, header http.Header, tlsopt tlsOption) *http.Request {
major, minor := protoToInts(proto)
var tlsConn *tls.ConnectionState
switch tlsopt {
case noTLS:
case withTLS:
tlsConn = &tls.ConnectionState{}
}
return &http.Request{
Method: method,
URL: u,
Proto: proto,
ProtoMajor: major,
ProtoMinor: minor,
Header: header,
Host: host,
RemoteAddr: remoteAddr,
RequestURI: requestURI,
TLS: tlsConn,
}
}
func protoToInts(proto string) (int, int) {
switch proto {
case "HTTP/1.0":
return 1, 0
case "HTTP/1.1":
return 1, 1
case "HTTP/2.0":
return 2, 0
}
// invalid proto
return 13, 42
}
func kvStr(kvs []otelkv.KeyValue) string {
sb := strings.Builder{}
sb.WriteRune('[')
for idx, kv := range kvs {
if idx > 0 {
sb.WriteString(", ")
}
sb.WriteString((string)(kv.Key))
sb.WriteString(": ")
sb.WriteString(kv.Value.Emit())
}
sb.WriteRune(']')
return sb.String()
}

View File

@ -22,6 +22,7 @@ import (
"go.opentelemetry.io/otel/api/global"
"go.opentelemetry.io/otel/api/kv"
"go.opentelemetry.io/otel/api/propagation"
"go.opentelemetry.io/otel/api/standard"
"go.opentelemetry.io/otel/api/trace"
)
@ -34,10 +35,10 @@ var (
func Extract(ctx context.Context, req *http.Request) ([]kv.KeyValue, []kv.KeyValue, trace.SpanContext) {
ctx = propagation.ExtractHTTP(ctx, global.Propagators(), req.Header)
attrs := []kv.KeyValue{
URLKey.String(req.URL.String()),
// Etc.
}
attrs := append(
standard.HTTPServerAttributesFromHTTPRequest("", "", req),
standard.NetAttributesFromHTTPRequest("tcp", req)...,
)
var correlationCtxKVs []kv.KeyValue
correlation.MapFromContext(ctx).Foreach(func(kv kv.KeyValue) bool {

View File

@ -18,20 +18,10 @@ import (
"net/http"
"go.opentelemetry.io/otel/api/kv"
"go.opentelemetry.io/otel/api/trace"
)
// Attribute keys that can be added to a span.
const (
HostKey = kv.Key("http.host") // the HTTP host (http.Request.Host)
MethodKey = kv.Key("http.method") // the HTTP method (http.Request.Method)
PathKey = kv.Key("http.path") // the HTTP path (http.Request.URL.Path)
URLKey = kv.Key("http.url") // the HTTP URL (http.Request.URL.String())
UserAgentKey = kv.Key("http.user_agent") // the HTTP user agent (http.Request.UserAgent())
RouteKey = kv.Key("http.route") // the HTTP route (ex: /users/:id)
RemoteAddrKey = kv.Key("http.remote_addr") // the network address of the client that sent the HTTP request (http.Request.RemoteAddr)
StatusCodeKey = kv.Key("http.status_code") // if set, the HTTP status
ReadBytesKey = kv.Key("http.read_bytes") // if anything was read from the request body, the total number of bytes read
ReadErrorKey = kv.Key("http.read_error") // If an error occurred while reading a request, the string of the error (io.EOF is not recorded)
WroteBytesKey = kv.Key("http.wrote_bytes") // if anything was written to the response writer, the total number of bytes written
@ -41,15 +31,3 @@ const (
// Filter is a predicate used to determine whether a given http.request should
// be traced. A Filter must return true if the request should be traced.
type Filter func(*http.Request) bool
// Setup basic span attributes before so that they
// are available to be mutated if needed.
func setBasicAttributes(span trace.Span, r *http.Request) {
span.SetAttributes(
HostKey.String(r.Host),
MethodKey.String(r.Method),
PathKey.String(r.URL.Path),
URLKey.String(r.URL.String()),
UserAgentKey.String(r.UserAgent()),
)
}

View File

@ -21,6 +21,7 @@ import (
"go.opentelemetry.io/otel/api/global"
"go.opentelemetry.io/otel/api/kv"
"go.opentelemetry.io/otel/api/propagation"
"go.opentelemetry.io/otel/api/standard"
"go.opentelemetry.io/otel/api/trace"
)
@ -88,7 +89,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
opts := append([]trace.StartOption{}, h.spanStartOptions...) // start with the configured options
opts := append([]trace.StartOption{
trace.WithAttributes(standard.NetAttributesFromHTTPRequest("tcp", r)...),
trace.WithAttributes(standard.EndUserAttributesFromHTTPRequest(r)...),
trace.WithAttributes(standard.HTTPServerAttributesFromHTTPRequest(h.operation, "", r)...),
}, h.spanStartOptions...) // start with the configured options
ctx := propagation.ExtractHTTP(r.Context(), h.propagators, r.Header)
ctx, span := h.tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...)
@ -112,16 +117,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rww := &respWriterWrapper{ResponseWriter: w, record: writeRecordFunc, ctx: ctx, props: h.propagators}
setBasicAttributes(span, r)
span.SetAttributes(RemoteAddrKey.String(r.RemoteAddr))
h.handler.ServeHTTP(rww, r.WithContext(ctx))
setAfterServeAttributes(span, bw.read, rww.written, int64(rww.statusCode), bw.err, rww.err)
setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err)
}
func setAfterServeAttributes(span trace.Span, read, wrote, statusCode int64, rerr, werr error) {
kv := make([]kv.KeyValue, 0, 5)
func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, rerr, werr error) {
kv := []kv.KeyValue{}
// TODO: Consider adding an event after each read and write, possibly as an
// option (defaulting to off), so as to not create needlessly verbose spans.
if read > 0 {
@ -134,7 +137,7 @@ func setAfterServeAttributes(span trace.Span, read, wrote, statusCode int64, rer
kv = append(kv, WroteBytesKey.Int64(wrote))
}
if statusCode > 0 {
kv = append(kv, StatusCodeKey.Int64(statusCode))
kv = append(kv, standard.HTTPAttributesFromHTTPStatusCode(statusCode)...)
}
if werr != nil && werr != io.EOF {
kv = append(kv, WriteErrorKey.String(werr.Error()))
@ -147,7 +150,7 @@ func setAfterServeAttributes(span trace.Span, read, wrote, statusCode int64, rer
func WithRouteTag(route string, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
span := trace.SpanFromContext(r.Context())
span.SetAttributes(RouteKey.String(route))
span.SetAttributes(standard.HTTPRouteKey.String(route))
h.ServeHTTP(w, r)
})
}

View File

@ -21,6 +21,7 @@ import (
"go.opentelemetry.io/otel/api/global"
"go.opentelemetry.io/otel/api/propagation"
"go.opentelemetry.io/otel/api/standard"
"go.opentelemetry.io/otel/api/trace"
"google.golang.org/grpc/codes"
@ -88,7 +89,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
ctx, span := t.tracer.Start(r.Context(), t.spanNameFormatter("", r), opts...)
r = r.WithContext(ctx)
setBasicAttributes(span, r)
span.SetAttributes(standard.HTTPClientAttributesFromHTTPRequest(r)...)
propagation.InjectHTTP(ctx, t.propagators, r.Header)
res, err := t.rt.RoundTrip(r)
@ -98,7 +99,8 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
return res, err
}
span.SetAttributes(StatusCodeKey.Int(res.StatusCode))
span.SetAttributes(standard.HTTPAttributesFromHTTPStatusCode(res.StatusCode)...)
span.SetStatus(standard.SpanStatusFromHTTPStatusCode(res.StatusCode))
res.Body = &wrappedBody{ctx: ctx, span: span, body: res.Body}
return res, err