Files
graphql-monitoring-proxy/tracing/tracing.go
T
lukaszraczylo c2c75d69c0 perf+coverage: optimisation pass + coverage push to ≥70%
Performance / resource usage:
- circuit_breaker_metrics: fix data race on failCounters map (RWMutex + double-checked locking)
- server.go: drop user_id and op_name metric labels (Prometheus cardinality bound); de-duplicate extractUserInfo
- graphql.go: gate runtime.ReadMemStats per-request behind ENABLE_ALLOCATION_TRACKING flag (default off)
- graphql.go: collapse two-pass AST scan into single pass; lower-case once
- sanitization.go: cache compiled redaction regexes per pattern via sync.Map; hoist inner constants to pkg vars
- proxy.go: hoist connection/timeout substrings to pkg vars; sentinel errors for static error paths; drop dead Headers map alloc
- metrics_aggregator.go: log-field allocation guarded by Logger.IsLevelEnabled
- logging/logger.go: add IsLevelEnabled helper
- lru_cache.go: 16-shard sharding, FNV-1a routing (concurrent throughput +22%)
- cache/memory/lru_memory_cache.go: gzip compress/decompress moved outside mu.Lock
- rps_tracker.go: RWMutex+uint64 -> atomic.Uint64
- retry_budget.go: drop unused mutex
- api.go: bannedUsersIDs map+RWMutex -> sync.Map (+ snapshot/replace helpers)
- tracing/tracing.go: pkg-level constSpanAttrs, copy-then-append in StartSpanWithAttributes
- admin_dashboard.go: handleStatsWebSocket reuses bytes.Buffer + json.Encoder per connection

Build / runtime:
- Makefile: -ldflags="-s -w" -trimpath, CGO_ENABLED=0 for build (=1 for test recipes)
- Dockerfile + Dockerfile.goreleaser: ENV GOMEMLIMIT=512MiB
- main.go: blank import go.uber.org/automaxprocs (cgroup-aware GOMAXPROCS)
- main.go: PPROF_PORT env var wires net/http/pprof on 127.0.0.1 only with full server timeouts
- README.md: env-var docs + metric-label docs updated; cardinality note

Test coverage push (per package):
- main 51.2% -> 74.7%
- cache 66.3% -> 93.7%
- cache/redis 45.5% -> 98.2%
- tracing 66.7% -> 72.9%
- (cache/memory 91.6%, logging 91.9%, monitoring 77.6%, pkg/pools 100% unchanged)

New test files: coverage_micro_test, coverage_extras_test, server_handlers_test,
api_health_test, admin_dashboard_cluster_test, metrics_aggregator_test, concerns_test,
cache/cache_coverage_test, cache/redis/redis_coverage_test, tracing/tracing_coverage_test.

Bug fix: connection_resilience_test.go TestIntegratedHealthManagement.health_manager_startup
was sync.Once-coupled to InitializeBackendHealth and panicked when another test (e.g. via
parseConfig) had already triggered Once. Use NewBackendHealthManager directly.
2026-04-19 19:49:24 +01:00

182 lines
5.8 KiB
Go

// Package tracing provides OpenTelemetry distributed tracing integration
// for the GraphQL proxy. Supports OTLP export to collectors like Jaeger,
// Zipkin, or any OTLP-compatible backend.
package tracing
import (
"context"
"encoding/json"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
)
type TracingSetup struct {
tracerProvider *sdktrace.TracerProvider
tracer trace.Tracer
}
// constSpanAttrs holds attributes that are identical for every span created
// by this package. Building the slice once at package init avoids two
// allocations per StartSpan / StartSpanWithAttributes call.
var constSpanAttrs = []attribute.KeyValue{
semconv.ServiceName("graphql-monitoring-proxy"),
semconv.ServiceVersion("1.0"),
}
type TraceSpanInfo struct {
TraceParent string `json:"traceparent"`
}
// NewTracing creates a new tracing setup with OTLP exporter
func NewTracing(ctx context.Context, endpoint string) (*TracingSetup, error) {
if ctx == nil {
return nil, fmt.Errorf("context cannot be nil")
}
if endpoint == "" {
return nil, fmt.Errorf("endpoint cannot be empty")
}
// Validate endpoint format
// A simple validation to check if the endpoint has a reasonable format
// We're looking for hostname:port where port is a valid port number (0-65535)
var host string
var port int
if n, err := fmt.Sscanf(endpoint, "%s:%d", &host, &port); err != nil || n != 2 {
return nil, fmt.Errorf("invalid endpoint format: must be 'hostname:port'")
}
if port < 0 || port > 65535 {
return nil, fmt.Errorf("invalid port number: must be between 0 and 65535")
}
// Create the exporter directly with the endpoint
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithTimeout(5*time.Second),
otlptracegrpc.WithDialOption(grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16*1024*1024))), // 16MB max message size
)
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}
// Create a resource with more detailed attributes
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("graphql-monitoring-proxy"),
semconv.ServiceVersion("1.0"),
semconv.DeploymentEnvironment("production"),
attribute.String("application.type", "proxy"),
),
resource.WithHost(), // Add host information
resource.WithOSType(), // Add OS information
resource.WithProcessPID(), // Add process information
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
// Create the tracer provider with improved configuration
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
// Configure batch processing
sdktrace.WithMaxExportBatchSize(512),
sdktrace.WithBatchTimeout(3*time.Second),
sdktrace.WithMaxQueueSize(2048),
),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)), // Sample 10% of traces
)
// Set the global tracer provider and propagator
otel.SetTracerProvider(tracerProvider)
otel.SetTextMapPropagator(propagation.TraceContext{})
// Create a tracer
tracer := tracerProvider.Tracer("graphql-monitoring-proxy")
return &TracingSetup{
tracerProvider: tracerProvider,
tracer: tracer,
}, nil
}
// ExtractSpanContext extracts span context from TraceSpanInfo
func (ts *TracingSetup) ExtractSpanContext(spanInfo *TraceSpanInfo) (trace.SpanContext, error) {
carrier := propagation.MapCarrier{
"traceparent": spanInfo.TraceParent,
}
ctx := context.Background()
ctx = otel.GetTextMapPropagator().Extract(ctx, carrier)
spanCtx := trace.SpanContextFromContext(ctx)
if !spanCtx.IsValid() {
return trace.SpanContext{}, fmt.Errorf("invalid span context")
}
return spanCtx, nil
}
// ParseTraceHeader parses X-Trace-Span header content
func ParseTraceHeader(headerContent string) (*TraceSpanInfo, error) {
var spanInfo TraceSpanInfo
if err := json.Unmarshal([]byte(headerContent), &spanInfo); err != nil {
return nil, fmt.Errorf("failed to parse trace header: %w", err)
}
return &spanInfo, nil
}
// Shutdown cleanly shuts down the tracer provider
func (ts *TracingSetup) Shutdown(ctx context.Context) error {
if ts.tracerProvider == nil {
return nil
}
return ts.tracerProvider.Shutdown(ctx)
}
// StartSpan starts a new span with the given name and parent context
func (ts *TracingSetup) StartSpan(ctx context.Context, name string) (trace.Span, context.Context) {
if ts == nil || ts.tracer == nil {
// Return a no-op span if tracing is not configured
return trace.SpanFromContext(ctx), ctx
}
// Add common attributes to all spans
opts := []trace.SpanStartOption{
trace.WithAttributes(
semconv.ServiceName("graphql-monitoring-proxy"),
semconv.ServiceVersion("1.0"),
),
}
ctx, span := ts.tracer.Start(ctx, name, opts...)
return span, ctx
}
// StartSpanWithAttributes starts a new span with custom attributes
func (ts *TracingSetup) StartSpanWithAttributes(ctx context.Context, name string, attrs map[string]string) (trace.Span, context.Context) {
if ts == nil || ts.tracer == nil {
return trace.SpanFromContext(ctx), ctx
}
// Convert string attributes to KeyValue pairs.
// Pre-size with constants + per-call attrs, copy constant block in one shot,
// then append the dynamic attributes.
attributes := make([]attribute.KeyValue, len(constSpanAttrs), len(constSpanAttrs)+len(attrs))
copy(attributes, constSpanAttrs)
for k, v := range attrs {
attributes = append(attributes, attribute.String(k, v))
}
ctx, span := ts.tracer.Start(ctx, name, trace.WithAttributes(attributes...))
return span, ctx
}