782 lines
22 KiB
Go
782 lines
22 KiB
Go
package otelchi_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/propagation"
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"toastielab.dev/toastie-stuff/otelchi"
|
|
)
|
|
|
|
func TestSDKIntegration(t *testing.T) {
|
|
// prepare router and span recorder
|
|
router, sr := newSDKTestRouter("foobar", false)
|
|
|
|
// define routes
|
|
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
|
router.HandleFunc("/book/{title}", ok)
|
|
|
|
// execute requests
|
|
reqs := []*http.Request{
|
|
httptest.NewRequest("GET", "/user/123", nil),
|
|
httptest.NewRequest("GET", "/book/foo", nil),
|
|
}
|
|
executeRequests(router, reqs)
|
|
|
|
// get recorded spans
|
|
recordedSpans := sr.Ended()
|
|
|
|
// ensure that we have 2 recorded spans
|
|
require.Len(t, recordedSpans, len(reqs))
|
|
|
|
// ensure span values
|
|
checkSpans(t, recordedSpans, []spanValueCheck{
|
|
{
|
|
Name: "/user/{id:[0-9]+}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/user/{id:[0-9]+}",
|
|
),
|
|
},
|
|
{
|
|
Name: "/book/{title}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/book/{title}",
|
|
),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSDKIntegrationWithFilter(t *testing.T) {
|
|
// prepare test cases
|
|
serviceName := "foobar"
|
|
testCases := []struct {
|
|
Name string
|
|
FilterFn []otelchi.Filter
|
|
LenSpans int
|
|
ExpectedRouteNames []string
|
|
}{
|
|
{
|
|
Name: "One WithFilter",
|
|
FilterFn: []otelchi.Filter{
|
|
func(r *http.Request) bool {
|
|
return r.URL.Path != "/live" && r.URL.Path != "/ready"
|
|
},
|
|
},
|
|
LenSpans: 2,
|
|
ExpectedRouteNames: []string{"/user/{id:[0-9]+}", "/book/{title}"},
|
|
},
|
|
{
|
|
Name: "Multiple WithFilter",
|
|
FilterFn: []otelchi.Filter{
|
|
func(r *http.Request) bool {
|
|
return r.URL.Path != "/ready"
|
|
},
|
|
func(r *http.Request) bool {
|
|
return r.URL.Path != "/live"
|
|
},
|
|
},
|
|
LenSpans: 2,
|
|
ExpectedRouteNames: []string{"/user/{id:[0-9]+}", "/book/{title}"},
|
|
},
|
|
{
|
|
Name: "All Routes are traced",
|
|
FilterFn: []otelchi.Filter{
|
|
func(r *http.Request) bool {
|
|
return true
|
|
},
|
|
},
|
|
LenSpans: 4,
|
|
ExpectedRouteNames: []string{"/user/{id:[0-9]+}", "/book/{title}", "/live", "/ready"},
|
|
},
|
|
{
|
|
Name: "All Routes are not traced",
|
|
FilterFn: []otelchi.Filter{
|
|
func(r *http.Request) bool {
|
|
return false
|
|
},
|
|
},
|
|
LenSpans: 0,
|
|
ExpectedRouteNames: []string{},
|
|
},
|
|
}
|
|
|
|
// execute test cases
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
// prepare router and span recorder
|
|
filters := []otelchi.Option{}
|
|
for _, filter := range testCase.FilterFn {
|
|
filters = append(filters, otelchi.WithFilter(filter))
|
|
}
|
|
router, sr := newSDKTestRouter(serviceName, false, filters...)
|
|
|
|
// define router
|
|
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
|
router.HandleFunc("/book/{title}", ok)
|
|
router.HandleFunc("/health", ok)
|
|
router.HandleFunc("/live", ok)
|
|
router.HandleFunc("/ready", ok)
|
|
|
|
// execute requests
|
|
executeRequests(router, []*http.Request{
|
|
httptest.NewRequest("GET", "/user/123", nil),
|
|
httptest.NewRequest("GET", "/book/foo", nil),
|
|
httptest.NewRequest("GET", "/live", nil),
|
|
httptest.NewRequest("GET", "/ready", nil),
|
|
})
|
|
|
|
// check recorded spans
|
|
recordedSpans := sr.Ended()
|
|
require.Len(t, recordedSpans, testCase.LenSpans)
|
|
|
|
// ensure span values
|
|
spanValues := []spanValueCheck{}
|
|
for _, routeName := range testCase.ExpectedRouteNames {
|
|
spanValues = append(spanValues, spanValueCheck{
|
|
Name: routeName,
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
serviceName,
|
|
http.StatusOK,
|
|
"GET",
|
|
routeName,
|
|
),
|
|
})
|
|
}
|
|
checkSpans(t, recordedSpans, spanValues)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestSDKIntegrationWithChiRoutes(t *testing.T) {
|
|
// define router & span recorder
|
|
router, sr := newSDKTestRouter("foobar", true)
|
|
|
|
// define route
|
|
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
|
router.HandleFunc("/book/{title}", ok)
|
|
|
|
// execute requests
|
|
reqs := []*http.Request{
|
|
httptest.NewRequest("GET", "/user/123", nil),
|
|
httptest.NewRequest("GET", "/book/foo", nil),
|
|
}
|
|
executeRequests(router, reqs)
|
|
|
|
// get recorded spans
|
|
recordedSpans := sr.Ended()
|
|
|
|
// ensure that we have 2 recorded spans
|
|
require.Len(t, recordedSpans, len(reqs))
|
|
|
|
// ensure span values
|
|
checkSpans(t, recordedSpans, []spanValueCheck{
|
|
{
|
|
Name: "/user/{id:[0-9]+}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/user/{id:[0-9]+}",
|
|
),
|
|
},
|
|
{
|
|
Name: "/book/{title}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/book/{title}",
|
|
),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSDKIntegrationOverrideSpanName(t *testing.T) {
|
|
// prepare test router and span recorder
|
|
router, sr := newSDKTestRouter("foobar", true)
|
|
|
|
// define route
|
|
router.HandleFunc("/user/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
span.SetName("overriden span name")
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
router.HandleFunc("/book/{title}", ok)
|
|
|
|
// execute requests
|
|
reqs := []*http.Request{
|
|
httptest.NewRequest("GET", "/user/123", nil),
|
|
httptest.NewRequest("GET", "/book/foo", nil),
|
|
}
|
|
executeRequests(router, reqs)
|
|
|
|
// get recorded spans
|
|
recordedSpans := sr.Ended()
|
|
|
|
// ensure the number of spans is correct
|
|
require.Len(t, sr.Ended(), len(reqs))
|
|
|
|
// check span values
|
|
checkSpans(t, recordedSpans, []spanValueCheck{
|
|
{
|
|
Name: "overriden span name",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/user/{id:[0-9]+}",
|
|
),
|
|
},
|
|
{
|
|
Name: "/book/{title}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/book/{title}",
|
|
),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSDKIntegrationWithRequestMethodInSpanName(t *testing.T) {
|
|
// prepare router & span recorder
|
|
router, sr := newSDKTestRouter("foobar", true, otelchi.WithRequestMethodInSpanName(true))
|
|
|
|
// define handler
|
|
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
|
router.HandleFunc("/book/{title}", ok)
|
|
|
|
// execute requests
|
|
reqs := []*http.Request{
|
|
httptest.NewRequest("GET", "/user/123", nil),
|
|
httptest.NewRequest("GET", "/book/foo", nil),
|
|
}
|
|
executeRequests(router, reqs)
|
|
|
|
// get recorded spans & ensure the number is correct
|
|
recordedSpans := sr.Ended()
|
|
require.Len(t, sr.Ended(), len(reqs))
|
|
|
|
// check span values
|
|
checkSpans(t, recordedSpans, []spanValueCheck{
|
|
{
|
|
Name: "GET /user/{id:[0-9]+}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/user/{id:[0-9]+}",
|
|
),
|
|
},
|
|
{
|
|
Name: "GET /book/{title}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/book/{title}",
|
|
),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSDKIntegrationEmptyHandlerDefaultStatusCode(t *testing.T) {
|
|
// prepare router and span recorder
|
|
router, sr := newSDKTestRouter("foobar", false)
|
|
|
|
// define routes
|
|
router.HandleFunc("/user/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {})
|
|
router.HandleFunc("/book/{title}", func(w http.ResponseWriter, r *http.Request) {})
|
|
router.HandleFunc("/not-found", http.NotFound)
|
|
|
|
// execute requests
|
|
reqs := []*http.Request{
|
|
httptest.NewRequest("GET", "/user/123", nil),
|
|
httptest.NewRequest("GET", "/book/foo", nil),
|
|
httptest.NewRequest("GET", "/not-found", nil),
|
|
}
|
|
executeRequests(router, reqs)
|
|
|
|
// get recorded spans
|
|
recordedSpans := sr.Ended()
|
|
|
|
// ensure that we have 3 recorded spans
|
|
require.Len(t, recordedSpans, len(reqs))
|
|
|
|
// ensure span values
|
|
checkSpans(t, recordedSpans, []spanValueCheck{
|
|
{
|
|
Name: "/user/{id:[0-9]+}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/user/{id:[0-9]+}",
|
|
),
|
|
},
|
|
{
|
|
Name: "/book/{title}",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/book/{title}",
|
|
),
|
|
},
|
|
{
|
|
Name: "/not-found",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusNotFound,
|
|
"GET",
|
|
"/not-found",
|
|
),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSDKIntegrationRootHandler(t *testing.T) {
|
|
// prepare router and span recorder
|
|
router, sr := newSDKTestRouter("foobar", true)
|
|
|
|
// define routes
|
|
router.HandleFunc("/", ok)
|
|
|
|
// execute requests
|
|
reqs := []*http.Request{
|
|
httptest.NewRequest("GET", "/", nil),
|
|
}
|
|
executeRequests(router, reqs)
|
|
|
|
// get recorded spans
|
|
recordedSpans := sr.Ended()
|
|
|
|
// ensure that we have 1 recorded span
|
|
require.Len(t, recordedSpans, 1)
|
|
|
|
// ensure span values
|
|
checkSpans(t, recordedSpans, []spanValueCheck{
|
|
{
|
|
Name: "/",
|
|
Kind: trace.SpanKindServer,
|
|
Attributes: getSemanticAttributes(
|
|
"foobar",
|
|
http.StatusOK,
|
|
"GET",
|
|
"/",
|
|
),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSDKIntegrationWithTraceIDResponseHeader(t *testing.T) {
|
|
// prepare both sampled & non-sampled span context
|
|
spanCtxSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
|
TraceID: [16]byte{1},
|
|
SpanID: [8]byte{1},
|
|
Remote: true,
|
|
TraceFlags: trace.FlagsSampled,
|
|
})
|
|
spanCtxNotSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
|
TraceID: [16]byte{2},
|
|
SpanID: [8]byte{2},
|
|
Remote: true,
|
|
TraceFlags: 0,
|
|
})
|
|
|
|
// define custom header key function
|
|
customHeaderKeyFunc := func() string {
|
|
return "X-Custom-Trace-ID"
|
|
}
|
|
|
|
// define test cases
|
|
testCases := []struct {
|
|
Name string
|
|
HeaderKeyFunc func() string
|
|
SpanContext trace.SpanContext
|
|
ExpTraceResponseIDKey string
|
|
ExpTraceResponseSampledKeyVal bool
|
|
}{
|
|
{
|
|
Name: "Default Header Key, Trace Sampled",
|
|
HeaderKeyFunc: nil,
|
|
SpanContext: spanCtxSampled,
|
|
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
|
ExpTraceResponseSampledKeyVal: true,
|
|
},
|
|
{
|
|
Name: "Default Header Key, Trace Not Sampled",
|
|
HeaderKeyFunc: nil,
|
|
SpanContext: spanCtxNotSampled,
|
|
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
|
ExpTraceResponseSampledKeyVal: false,
|
|
},
|
|
{
|
|
Name: "Custom Header Key, Trace Sampled",
|
|
HeaderKeyFunc: customHeaderKeyFunc,
|
|
SpanContext: spanCtxSampled,
|
|
ExpTraceResponseIDKey: customHeaderKeyFunc(),
|
|
ExpTraceResponseSampledKeyVal: true,
|
|
},
|
|
{
|
|
Name: "Custom Header Key, Trace Not Sampled",
|
|
HeaderKeyFunc: customHeaderKeyFunc,
|
|
SpanContext: spanCtxNotSampled,
|
|
ExpTraceResponseIDKey: customHeaderKeyFunc(),
|
|
ExpTraceResponseSampledKeyVal: false,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
// configure router
|
|
router := chi.NewRouter()
|
|
router.Use(
|
|
otelchi.Middleware(
|
|
"foobar",
|
|
otelchi.WithChiRoutes(router),
|
|
otelchi.WithTraceIDResponseHeader(testCase.HeaderKeyFunc),
|
|
),
|
|
)
|
|
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
|
router.HandleFunc("/book/{title}", ok)
|
|
|
|
// execute requests
|
|
r0 := httptest.NewRequest("GET", "/user/123", nil)
|
|
r0 = r0.WithContext(trace.ContextWithRemoteSpanContext(context.Background(), testCase.SpanContext))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, r0)
|
|
|
|
// check response headers
|
|
require.Equal(t, testCase.SpanContext.TraceID().String(), w.Header().Get(testCase.ExpTraceResponseIDKey))
|
|
require.Equal(t, fmt.Sprintf("%v", testCase.ExpTraceResponseSampledKeyVal), w.Header().Get(otelchi.DefaultTraceSampledResponseHeaderKey))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSDKIntegrationWithTraceResponseHeaders(t *testing.T) {
|
|
// prepare both sampled & non-sampled span context
|
|
spanCtxSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
|
TraceID: [16]byte{1},
|
|
SpanID: [8]byte{1},
|
|
Remote: true,
|
|
TraceFlags: trace.FlagsSampled,
|
|
})
|
|
spanCtxNotSampled := trace.NewSpanContext(trace.SpanContextConfig{
|
|
TraceID: [16]byte{2},
|
|
SpanID: [8]byte{2},
|
|
Remote: true,
|
|
TraceFlags: 0,
|
|
})
|
|
|
|
// define test cases
|
|
testCases := []struct {
|
|
Name string
|
|
TraceHeaderConfig otelchi.TraceHeaderConfig
|
|
SpanContext trace.SpanContext
|
|
ExpTraceResponseIDKey string
|
|
ExpTraceResponseSampledKey string
|
|
ExpTraceResponseSampledKeyVal bool
|
|
}{
|
|
{
|
|
Name: "Default Trace Config, Trace Sampled",
|
|
TraceHeaderConfig: otelchi.TraceHeaderConfig{},
|
|
SpanContext: spanCtxSampled,
|
|
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
|
ExpTraceResponseSampledKey: otelchi.DefaultTraceSampledResponseHeaderKey,
|
|
ExpTraceResponseSampledKeyVal: true,
|
|
},
|
|
{
|
|
Name: "Default Trace Config, Trace Not Sampled",
|
|
TraceHeaderConfig: otelchi.TraceHeaderConfig{},
|
|
SpanContext: spanCtxNotSampled,
|
|
ExpTraceResponseIDKey: otelchi.DefaultTraceIDResponseHeaderKey,
|
|
ExpTraceResponseSampledKey: otelchi.DefaultTraceSampledResponseHeaderKey,
|
|
ExpTraceResponseSampledKeyVal: false,
|
|
},
|
|
{
|
|
Name: "Custom Trace Config, Trace Sampled",
|
|
TraceHeaderConfig: otelchi.TraceHeaderConfig{
|
|
TraceIDHeader: "X-Custom-Trace-ID",
|
|
TraceSampledHeader: "X-Custom-Trace-Sampled",
|
|
},
|
|
SpanContext: spanCtxSampled,
|
|
ExpTraceResponseIDKey: "X-Custom-Trace-ID",
|
|
ExpTraceResponseSampledKey: "X-Custom-Trace-Sampled",
|
|
ExpTraceResponseSampledKeyVal: true,
|
|
},
|
|
{
|
|
Name: "Custom Trace Config, Trace Not Sampled",
|
|
TraceHeaderConfig: otelchi.TraceHeaderConfig{
|
|
TraceIDHeader: "X-Custom-Trace-ID",
|
|
TraceSampledHeader: "X-Custom-Trace-Sampled",
|
|
},
|
|
SpanContext: spanCtxNotSampled,
|
|
ExpTraceResponseIDKey: "X-Custom-Trace-ID",
|
|
ExpTraceResponseSampledKey: "X-Custom-Trace-Sampled",
|
|
ExpTraceResponseSampledKeyVal: false,
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
// configure router
|
|
router := chi.NewRouter()
|
|
router.Use(
|
|
otelchi.Middleware(
|
|
"foobar",
|
|
otelchi.WithChiRoutes(router),
|
|
otelchi.WithTraceResponseHeaders(testCase.TraceHeaderConfig),
|
|
),
|
|
)
|
|
router.HandleFunc("/user/{id:[0-9]+}", ok)
|
|
router.HandleFunc("/book/{title}", ok)
|
|
|
|
// execute requests
|
|
r0 := httptest.NewRequest("GET", "/user/123", nil)
|
|
r0 = r0.WithContext(trace.ContextWithRemoteSpanContext(context.Background(), testCase.SpanContext))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, r0)
|
|
|
|
// check response headers
|
|
require.Equal(t, testCase.SpanContext.TraceID().String(), w.Header().Get(testCase.ExpTraceResponseIDKey))
|
|
require.Equal(t, fmt.Sprintf("%v", testCase.ExpTraceResponseSampledKeyVal), w.Header().Get(testCase.ExpTraceResponseSampledKey))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWithPublicEndpoint(t *testing.T) {
|
|
// prepare router and span recorder
|
|
router, spanRecorder := newSDKTestRouter("foobar", true, otelchi.WithPublicEndpoint())
|
|
|
|
// prepare remote span context
|
|
remoteSpanCtx := trace.NewSpanContext(trace.SpanContextConfig{
|
|
TraceID: trace.TraceID{0x01},
|
|
SpanID: trace.SpanID{0x01},
|
|
Remote: true,
|
|
})
|
|
|
|
// prepare http request & inject remote span context into it
|
|
endpointURL := "/with/public/endpoint"
|
|
req := httptest.NewRequest(http.MethodGet, endpointURL, nil)
|
|
ctx := trace.ContextWithSpanContext(context.Background(), remoteSpanCtx)
|
|
(propagation.TraceContext{}).Inject(ctx, propagation.HeaderCarrier(req.Header))
|
|
|
|
// configure router handler
|
|
router.HandleFunc(endpointURL, func(w http.ResponseWriter, r *http.Request) {
|
|
// get span from request context
|
|
span := trace.SpanFromContext(r.Context())
|
|
spanCtx := span.SpanContext()
|
|
|
|
// ensure it is not equal to the remote span context
|
|
require.False(t, spanCtx.Equal(remoteSpanCtx))
|
|
require.True(t, spanCtx.IsValid())
|
|
require.False(t, spanCtx.IsRemote())
|
|
})
|
|
|
|
// execute http request
|
|
executeRequests(router, []*http.Request{req})
|
|
|
|
// get recorded spans
|
|
recordedSpans := spanRecorder.Ended()
|
|
require.Len(t, recordedSpans, 1)
|
|
|
|
links := recordedSpans[0].Links()
|
|
require.Len(t, links, 1)
|
|
require.True(t, remoteSpanCtx.Equal(links[0].SpanContext))
|
|
}
|
|
|
|
func TestWithPublicEndpointFn(t *testing.T) {
|
|
// prepare remote span context
|
|
remoteSpanCtx := trace.NewSpanContext(trace.SpanContextConfig{
|
|
TraceID: trace.TraceID{0x01},
|
|
SpanID: trace.SpanID{0x01},
|
|
Remote: true,
|
|
})
|
|
|
|
// prepare test cases
|
|
testCases := []struct {
|
|
Name string
|
|
Fn func(r *http.Request) bool
|
|
HandlerAssert func(t *testing.T, spanCtx trace.SpanContext)
|
|
SpansAssert func(t *testing.T, spanCtx trace.SpanContext, spans []sdktrace.ReadOnlySpan)
|
|
}{
|
|
{
|
|
Name: "Function Always Return True",
|
|
Fn: func(r *http.Request) bool { return true },
|
|
HandlerAssert: func(t *testing.T, spanCtx trace.SpanContext) {
|
|
// ensure it is not equal to the remote span context
|
|
require.False(t, spanCtx.Equal(remoteSpanCtx))
|
|
require.True(t, spanCtx.IsValid())
|
|
|
|
// ensure it is not remote span
|
|
require.False(t, spanCtx.IsRemote())
|
|
},
|
|
SpansAssert: func(t *testing.T, spanCtx trace.SpanContext, spans []sdktrace.ReadOnlySpan) {
|
|
// ensure spans length
|
|
require.Len(t, spans, 1)
|
|
|
|
// ensure the span has been linked
|
|
links := spans[0].Links()
|
|
require.Len(t, links, 1)
|
|
require.True(t, remoteSpanCtx.Equal(links[0].SpanContext))
|
|
},
|
|
},
|
|
{
|
|
Name: "Function Always Return False",
|
|
Fn: func(r *http.Request) bool { return false },
|
|
HandlerAssert: func(t *testing.T, spanCtx trace.SpanContext) {
|
|
// ensure the span is child of the remote span
|
|
require.Equal(t, remoteSpanCtx.TraceID(), spanCtx.TraceID())
|
|
require.True(t, spanCtx.IsValid())
|
|
|
|
// ensure it is not remote span
|
|
require.False(t, spanCtx.IsRemote())
|
|
},
|
|
SpansAssert: func(t *testing.T, spanCtx trace.SpanContext, spans []sdktrace.ReadOnlySpan) {
|
|
// ensure spans length
|
|
require.Len(t, spans, 1, "unexpected span length")
|
|
|
|
// ensure the span has no links
|
|
links := spans[0].Links()
|
|
require.Len(t, links, 0)
|
|
},
|
|
},
|
|
}
|
|
|
|
// execute test cases
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.Name, func(t *testing.T) {
|
|
|
|
// prepare router and span recorder
|
|
router, spanRecorder := newSDKTestRouter(
|
|
"foobar",
|
|
true,
|
|
otelchi.WithPublicEndpointFn(testCase.Fn),
|
|
)
|
|
|
|
// prepare http request & inject remote span context into it
|
|
endpointURL := "/with/public/endpoint"
|
|
req := httptest.NewRequest(http.MethodGet, endpointURL, nil)
|
|
ctx := trace.ContextWithSpanContext(context.Background(), remoteSpanCtx)
|
|
(propagation.TraceContext{}).Inject(ctx, propagation.HeaderCarrier(req.Header))
|
|
|
|
// configure router handler
|
|
router.HandleFunc(endpointURL, func(w http.ResponseWriter, r *http.Request) {
|
|
// assert handler
|
|
span := trace.SpanFromContext(r.Context())
|
|
testCase.HandlerAssert(t, span.SpanContext())
|
|
})
|
|
|
|
// execute http request
|
|
executeRequests(router, []*http.Request{req})
|
|
|
|
// assert recorded spans
|
|
testCase.SpansAssert(t, remoteSpanCtx, spanRecorder.Ended())
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertSpan(t *testing.T, span sdktrace.ReadOnlySpan, name string, kind trace.SpanKind, attrs ...attribute.KeyValue) {
|
|
t.Helper()
|
|
|
|
assert.Equal(t, name, span.Name())
|
|
assert.Equal(t, kind, span.SpanKind())
|
|
|
|
got := make(map[attribute.Key]attribute.Value, len(span.Attributes()))
|
|
for _, a := range span.Attributes() {
|
|
got[a.Key] = a.Value
|
|
}
|
|
for _, want := range attrs {
|
|
if !assert.Contains(t, got, want.Key) {
|
|
continue
|
|
}
|
|
assert.Equal(t, want.Value, got[want.Key])
|
|
}
|
|
}
|
|
|
|
func ok(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func newSDKTestRouter(serverName string, withChiRoutes bool, opts ...otelchi.Option) (*chi.Mux, *tracetest.SpanRecorder) {
|
|
spanRecorder := tracetest.NewSpanRecorder()
|
|
tracerProvider := sdktrace.NewTracerProvider(
|
|
// set the tracer provider to always sample trace, this is important
|
|
// because if we don't set this, sometimes there are traces that
|
|
// won't be sampled (recorded), so we need to set this option
|
|
// to ensure every trace in this test is recorded.
|
|
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
|
)
|
|
tracerProvider.RegisterSpanProcessor(spanRecorder)
|
|
|
|
opts = append(opts, otelchi.WithTracerProvider(tracerProvider))
|
|
|
|
router := chi.NewRouter()
|
|
if withChiRoutes {
|
|
opts = append(opts, otelchi.WithChiRoutes(router))
|
|
}
|
|
router.Use(otelchi.Middleware(serverName, opts...))
|
|
|
|
return router, spanRecorder
|
|
}
|
|
|
|
type spanValueCheck struct {
|
|
Name string
|
|
Kind trace.SpanKind
|
|
Attributes []attribute.KeyValue
|
|
}
|
|
|
|
func getSemanticAttributes(serverName string, httpStatusCode int, httpMethod, httpRoute string) []attribute.KeyValue {
|
|
return []attribute.KeyValue{
|
|
attribute.String("net.host.name", serverName),
|
|
attribute.Int("http.status_code", httpStatusCode),
|
|
attribute.String("http.method", httpMethod),
|
|
attribute.String("http.route", httpRoute),
|
|
}
|
|
}
|
|
|
|
func checkSpans(t *testing.T, spans []sdktrace.ReadOnlySpan, valueChecks []spanValueCheck) {
|
|
t.Helper()
|
|
|
|
for i := 0; i < len(spans); i++ {
|
|
span := spans[i]
|
|
valueCheck := valueChecks[i]
|
|
assertSpan(t, span, valueCheck.Name, valueCheck.Kind, valueCheck.Attributes...)
|
|
}
|
|
}
|
|
|
|
func executeRequests(router *chi.Mux, reqs []*http.Request) {
|
|
w := httptest.NewRecorder()
|
|
for _, r := range reqs {
|
|
router.ServeHTTP(w, r)
|
|
}
|
|
}
|