mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
172 lines
5.3 KiB
Go
172 lines
5.3 KiB
Go
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
|
|
}
|
|
|
|
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
|
|
attributes := make([]attribute.KeyValue, 0, len(attrs)+2)
|
|
attributes = append(attributes,
|
|
semconv.ServiceName("graphql-monitoring-proxy"),
|
|
semconv.ServiceVersion("1.0"),
|
|
)
|
|
|
|
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
|
|
}
|