mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
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.
182 lines
5.8 KiB
Go
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
|
|
}
|