Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

otelhttptrace: add TLS info to the "http.tls" span #5563

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The `WithSchemaURL` option function in `go.opentelemetry.io/contrib/bridges/otelslog`.
This option function is used as a replacement of `WithInstrumentationScope` to specify the semantic convention schema URL for the logged records. (#5588)
- Add support for Cloud Run jobs in `go.opentelemetry.io/contrib/detectors/gcp`. (#5559)
- Add TLS information to otelhttptrace http.tls attributes, providing information on the cipher and protocol version used, whether the session was resumed, the SHA256 hash of the leaf certificate, "not before"/"not after" dates, and verified certificate chains.
dotwaffle marked this conversation as resolved.
Show resolved Hide resolved

### Changed

Expand Down
42 changes: 40 additions & 2 deletions instrumentation/net/http/httptrace/otelhttptrace/clienttrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ package otelhttptrace // import "go.opentelemetry.io/contrib/instrumentation/net

import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"fmt"
"net/http/httptrace"
"net/textproto"
"strings"
"sync"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
Expand All @@ -36,6 +40,19 @@ var (
HTTPDNSAddrs = attribute.Key("http.dns.addrs")
)

// TLS attributes.
//
// From https://opentelemetry.io/docs/specs/semconv/attributes-registry/tls/ but only present from semconv v1.24.
var (
TLSCipher = attribute.Key("tls.cipher")
TLSProtocolVersion = attribute.Key("tls.protocol.version")
TLSResumed = attribute.Key("tls.resumed")
TLSServerCertificateChain = attribute.Key("tls.server.certificate_chain")
TLSServerHashSha256 = attribute.Key("tls.server.hash.sha256")
TLSServerNotAfter = attribute.Key("tls.server.not_after")
TLSServerNotBefore = attribute.Key("tls.server.not_before")
dmathieu marked this conversation as resolved.
Show resolved Hide resolved
)

var hookMap = map[string]string{
"http.dns": "http.getconn",
"http.connect": "http.getconn",
Expand Down Expand Up @@ -316,8 +333,29 @@ func (ct *clientTracer) tlsHandshakeStart() {
ct.start("http.tls", "http.tls")
}

func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) {
ct.end("http.tls", err)
func (ct *clientTracer) tlsHandshakeDone(state tls.ConnectionState, err error) {
attrs := make([]attribute.KeyValue, 0, 7)
attrs = append(attrs,
TLSCipher.String(tls.CipherSuiteName(state.CipherSuite)),
TLSProtocolVersion.String(tls.VersionName(state.Version)),
TLSResumed.Bool(state.DidResume),
)

if len(state.PeerCertificates) > 0 {
certChain := make([]string, len(state.PeerCertificates))
for i, cert := range state.PeerCertificates {
certChain[i] = base64.StdEncoding.EncodeToString(cert.Raw)
}

leafCert := state.PeerCertificates[0]
attrs = append(attrs,
TLSServerCertificateChain.StringSlice(certChain),
TLSServerHashSha256.String(fmt.Sprintf("%X", sha256.Sum256(leafCert.Raw))),
TLSServerNotAfter.String(leafCert.NotAfter.UTC().Format(time.RFC3339)),
TLSServerNotBefore.String(leafCert.NotBefore.UTC().Format(time.RFC3339)),
)
}
ct.end("http.tls", err, attrs...)
}

func (ct *clientTracer) wroteHeaderField(k string, v []string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package otelhttptrace_test

import (
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
Expand All @@ -30,7 +31,7 @@ func TestRoundtrip(t *testing.T) {
props := otelhttptrace.WithPropagators(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

// Mock http server
ts := httptest.NewServer(
ts := httptest.NewTLSServer(
dotwaffle marked this conversation as resolved.
Show resolved Hide resolved
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attrs, corrs, span := otelhttptrace.Extract(r.Context(), r, props)

Expand Down Expand Up @@ -69,16 +70,16 @@ func TestRoundtrip(t *testing.T) {
defer ts.Close()

address := ts.Listener.Addr()
hp := strings.Split(address.String(), ":")
host, port, _ := net.SplitHostPort(address.String())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
host, port, _ := net.SplitHostPort(address.String())
host, port, err := net.SplitHostPort(address.String())
if err != nil {
t.Fatalf("Address splitting failed: %s", err.Error())
}

expectedAttrs = map[attribute.Key]string{
semconv.NetHostNameKey: hp[0],
semconv.NetHostPortKey: hp[1],
semconv.NetHostNameKey: host,
semconv.NetHostPortKey: port,
semconv.NetProtocolVersionKey: "1.1",
semconv.HTTPMethodKey: "GET",
semconv.HTTPSchemeKey: "http",
semconv.HTTPSchemeKey: "https",
semconv.HTTPTargetKey: "/",
semconv.HTTPRequestContentLengthKey: "3",
semconv.NetSockPeerAddrKey: hp[0],
semconv.NetSockPeerAddrKey: host,
semconv.NetTransportKey: "ip_tcp",
semconv.UserAgentOriginalKey: "Go-http-client/1.1",
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ package test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"slices"
"testing"
"time"

Expand Down Expand Up @@ -48,7 +52,7 @@ func TestHTTPRequestWithClientTrace(t *testing.T) {
tr := tp.Tracer("httptrace/client")

// Mock http server
ts := httptest.NewServer(
ts := httptest.NewTLSServer(
dotwaffle marked this conversation as resolved.
Show resolved Hide resolved
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
}),
)
Expand Down Expand Up @@ -79,6 +83,21 @@ func TestHTTPRequestWithClientTrace(t *testing.T) {
attributes []attribute.KeyValue
parent string
}{
{
name: "http.tls",
attributes: []attribute.KeyValue{
attribute.Key("tls.server.certificate_chain").StringSlice(
[]string{base64.StdEncoding.EncodeToString(ts.Certificate().Raw)},
),
attribute.Key("tls.server.hash.sha256").
String(fmt.Sprintf("%X", sha256.Sum256(ts.Certificate().Raw))),
attribute.Key("tls.server.not_after").
String(ts.Certificate().NotAfter.UTC().Format(time.RFC3339)),
attribute.Key("tls.server.not_before").
String(ts.Certificate().NotBefore.UTC().Format(time.RFC3339)),
},
parent: "http.getconn",
},
{
name: "http.connect",
attributes: []attribute.KeyValue{
Expand Down Expand Up @@ -115,7 +134,7 @@ func TestHTTPRequestWithClientTrace(t *testing.T) {
name: "test",
},
}
for _, tl := range testLen {
for i, tl := range testLen {
span, ok := getSpanFromRecorder(sr, tl.name)
if !assert.True(t, ok) {
continue
Expand All @@ -142,6 +161,20 @@ func TestHTTPRequestWithClientTrace(t *testing.T) {
}
assert.True(t, contains, "missing http.local attribute")
}
if tl.name == "http.tls" {
if i == 0 {
tl.attributes = append(tl.attributes, attribute.Key("tls.resumed").Bool(false))
} else {
tl.attributes = append(tl.attributes, attribute.Key("tls.resumed").Bool(true))
}
attrs = slices.DeleteFunc(attrs, func(a attribute.KeyValue) bool {
// Skip keys that are unable to be detected beforehand.
if a.Key == otelhttptrace.TLSCipher || a.Key == otelhttptrace.TLSProtocolVersion {
return true
}
return false
})
}
assert.ElementsMatch(t, tl.attributes, attrs)
}
}
Expand Down