Files
go-omni/telemetry/telemetry_test.go

231 lines
8.1 KiB
Go

package telemetry_test
import (
"context"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/logx"
"git.yuntuo.hk/k2safe/go-omni/telemetry"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/otel/propagation"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHTTPTracePropagationMetricsAndRedisNil(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
reader := sdkmetric.NewManualReader()
runtime, err := telemetry.New(telemetry.Config{Service: "test", SampleRatio: 1, Exporter: exporter, Reader: reader})
if err != nil {
t.Fatal(err)
}
defer runtime.Close()
ctx := context.Background()
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.Header.Get("Traceparent"), "00-11111111111111111111111111111111-") {
t.Error("missing propagated trace")
}
io.WriteString(w, "ok")
}))
defer upstream.Close()
app := omni.New(omni.WithLogger(false))
app.Use(func(next httpx.Handler) httpx.Handler {
return func(c *httpx.Context) error {
err := next(c)
if logx.TraceID(c.Context()) != "11111111111111111111111111111111" {
t.Error("outer access log lost trace correlation")
}
return err
}
})
app.Use(runtime.Middleware())
app.GET("/users/:id", func(c *httpx.Context) error {
if logx.TraceID(c.Context()) != "11111111111111111111111111111111" {
t.Error("log correlation")
}
req, _ := http.NewRequestWithContext(c.Context(), "GET", upstream.URL+"?secret=hidden", nil)
resp, err := (telemetry.Transport{Runtime: runtime}).RoundTrip(req)
if err != nil {
return err
}
io.ReadAll(resp.Body)
resp.Body.Close()
if req.Header.Get("Traceparent") != "" {
t.Error("caller headers mutated")
}
return c.OK("ok")
})
req := httptest.NewRequest("GET", "/users/privateUser?secret=hidden", nil)
req.Header.Set("Traceparent", "00-11111111111111111111111111111111-2222222222222222-01")
rec := httptest.NewRecorder()
app.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Fatal(rec.Code)
}
hook := telemetry.RedisHook{Runtime: runtime}
if err = hook.ProcessHook(func(context.Context, redis.Cmder) error { return redis.Nil })(ctx, redis.NewStringCmd(ctx, "GET", "secret")); err != redis.Nil {
t.Fatal("cache miss was swallowed", err)
}
if err = runtime.Flush(ctx); err != nil {
t.Fatal(err)
}
spans := exporter.GetSpans()
if len(spans) != 3 {
t.Fatalf("got %d spans", len(spans))
}
var client, server bool
for _, s := range spans {
if strings.Contains(s.Name, "privateUser") || strings.Contains(s.Name, "secret") {
t.Fatal(s.Name)
}
if s.SpanKind == trace.SpanKindServer {
server = true
if s.Name != "GET /users/:id" || s.Parent.SpanID().String() != "2222222222222222" {
t.Fatal(s.Name, s.Parent)
}
}
if s.SpanKind == trace.SpanKindClient {
client = true
}
}
if !client || !server {
t.Fatal("missing client/server span")
}
var metrics metricdata.ResourceMetrics
if err = reader.Collect(ctx, &metrics); err != nil {
t.Fatal(err)
}
var count int64
for _, scope := range metrics.ScopeMetrics {
for _, m := range scope.Metrics {
if sum, ok := m.Data.(metricdata.Sum[int64]); ok {
for _, p := range sum.DataPoints {
count += p.Value
}
}
}
}
if count != 1 {
t.Fatal("request metric", count)
}
}
func TestConfiguredHeaderPropagation(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
propagator, err := telemetry.NewHeaderPropagator(telemetry.HeaderPropagationConfig{
TraceIDHeader: "X-Trace-Id", SpanIDHeader: "X-Span-Id", RequestIDHeader: "X-Request-Id",
})
if err != nil {
t.Fatal(err)
}
runtime, err := telemetry.New(telemetry.Config{Service: "header-test", SampleRatio: 1, Exporter: exporter, Propagator: propagator})
if err != nil {
t.Fatal(err)
}
defer runtime.Close()
const traceID = "11111111111111111111111111111111"
const parentSpanID = "2222222222222222"
const requestID = "gateway-request-1"
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Trace-Id") != traceID || r.Header.Get("X-Request-Id") != requestID {
t.Errorf("custom propagation = trace %q request %q", r.Header.Get("X-Trace-Id"), r.Header.Get("X-Request-Id"))
}
if r.Header.Get("X-Span-Id") == parentSpanID || len(r.Header.Get("X-Span-Id")) != 16 || !strings.HasPrefix(r.Header.Get("Traceparent"), "00-"+traceID+"-") {
t.Errorf("outbound parent propagation = %q", r.Header)
}
io.WriteString(w, "ok")
}))
defer upstream.Close()
app := omni.New(omni.WithLogger(false), omni.WithRequestIDHeader("X-Request-Id"))
defer app.Close()
app.Use(runtime.Middleware())
app.GET("/", func(c *httpx.Context) error {
if got := logx.TraceID(c.Context()); got != traceID {
t.Errorf("inbound trace correlation = %q", got)
}
if got := logx.RequestID(c.Context()); got != requestID {
t.Errorf("inbound request correlation = %q", got)
}
req, _ := http.NewRequestWithContext(c.Context(), http.MethodGet, upstream.URL, nil)
resp, err := (telemetry.Transport{Runtime: runtime}).RoundTrip(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.ReadAll(resp.Body)
return c.OK("ok")
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-Trace-Id", traceID)
req.Header.Set("X-Span-Id", parentSpanID)
req.Header.Set("X-Request-Id", requestID)
rec := httptest.NewRecorder()
app.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || rec.Header().Get("X-Request-Id") != requestID {
t.Fatalf("response status/request ID = %d/%q", rec.Code, rec.Header().Get("X-Request-Id"))
}
if err := runtime.Flush(context.Background()); err != nil {
t.Fatal(err)
}
spans := exporter.GetSpans()
var server, client *tracetest.SpanStub
for _, span := range spans {
switch span.SpanKind {
case trace.SpanKindServer:
span := span
server = &span
case trace.SpanKindClient:
span := span
client = &span
}
}
if server == nil || client == nil {
t.Fatalf("missing spans: %d", len(spans))
}
if server.SpanContext.TraceID().String() != traceID || server.Parent.TraceID().String() != traceID || server.Parent.SpanID().String() != parentSpanID || !server.Parent.IsRemote() {
t.Fatalf("server parent = %s/%s remote=%v", server.SpanContext.TraceID(), server.Parent.SpanID(), server.Parent.IsRemote())
}
if client.SpanContext.TraceID().String() != traceID || client.Parent.SpanID() != server.SpanContext.SpanID() {
t.Fatalf("client linkage = trace %s parent %s", client.SpanContext.TraceID(), client.Parent.SpanID())
}
}
func TestConfiguredHeaderPropagationPrefersW3CAndRejectsInvalidSetup(t *testing.T) {
propagator, err := telemetry.NewHeaderPropagator(telemetry.HeaderPropagationConfig{TraceIDHeader: "x-trace-id", SpanIDHeader: "x-span-id"})
if err != nil {
t.Fatal(err)
}
carrier := propagation.MapCarrier{
"traceparent": "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01",
"X-Trace-Id": "11111111111111111111111111111111",
"X-Span-Id": "2222222222222222",
}
ctx := propagator.Extract(context.Background(), carrier)
parent := trace.SpanContextFromContext(ctx)
if parent.TraceID().String() != "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" || parent.SpanID().String() != "bbbbbbbbbbbbbbbb" {
t.Fatalf("W3C parent was not preferred: %s/%s", parent.TraceID(), parent.SpanID())
}
for _, config := range []telemetry.HeaderPropagationConfig{
{TraceIDHeader: "", SpanIDHeader: "X-Span-Id"},
{TraceIDHeader: "X-Trace-Id", SpanIDHeader: ""},
{TraceIDHeader: "X-Trace-Id", SpanIDHeader: "X-Trace-Id"},
{TraceIDHeader: "traceparent", SpanIDHeader: "X-Span-Id"},
{TraceIDHeader: "X Trace", SpanIDHeader: "X-Span-Id"},
} {
if _, err := telemetry.NewHeaderPropagator(config); err == nil {
t.Fatalf("invalid propagation config accepted: %+v", config)
}
}
invalid := propagation.MapCarrier{"X-Trace-Id": "bad", "X-Span-Id": "bad"}
if got := trace.SpanContextFromContext(propagator.Extract(context.Background(), invalid)); got.IsValid() {
t.Fatal("invalid legacy IDs became a parent")
}
}