A community based topic aggregation platform built on atproto
at main 145 lines 4.0 kB view raw
1package observability 2 3import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "go.opentelemetry.io/otel" 9 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" 10 "go.opentelemetry.io/otel/sdk/resource" 11 "go.opentelemetry.io/otel/sdk/trace" 12 semconv "go.opentelemetry.io/otel/semconv/v1.37.0" 13) 14 15// Provider manages the OpenTelemetry TracerProvider lifecycle. 16type Provider struct { 17 enabled bool 18 tracerProvider *trace.TracerProvider 19} 20 21// NewProvider creates a new OpenTelemetry provider based on the given configuration. 22// If tracing is not enabled, it returns a Provider with enabled=false and no active tracer. 23// When enabled, it creates an OTLP HTTP exporter and configures the global tracer provider. 24func NewProvider(ctx context.Context, cfg Config) (*Provider, error) { 25 if err := cfg.Validate(); err != nil { 26 return nil, fmt.Errorf("invalid observability config: %w", err) 27 } 28 29 if !cfg.Enabled { 30 return &Provider{enabled: false}, nil 31 } 32 33 // Build exporter options for HTTP 34 opts := []otlptracehttp.Option{ 35 otlptracehttp.WithEndpoint(stripScheme(cfg.Endpoint)), 36 } 37 38 if cfg.Insecure { 39 opts = append(opts, otlptracehttp.WithInsecure()) 40 } 41 42 // Parse and add headers if provided 43 if cfg.Headers != "" { 44 headers := parseHeaders(cfg.Headers) 45 if len(headers) > 0 { 46 opts = append(opts, otlptracehttp.WithHeaders(headers)) 47 } 48 } 49 50 // Create the OTLP HTTP exporter 51 exporter, err := otlptracehttp.New(ctx, opts...) 52 if err != nil { 53 return nil, fmt.Errorf("failed to create OTLP trace exporter: %w", err) 54 } 55 56 // Create the resource with service information 57 res, err := resource.Merge( 58 resource.Default(), 59 resource.NewWithAttributes( 60 semconv.SchemaURL, 61 semconv.ServiceName(cfg.ServiceName), 62 ), 63 ) 64 if err != nil { 65 return nil, fmt.Errorf("failed to create resource: %w", err) 66 } 67 68 // Create the sampler based on the sample ratio 69 var sampler trace.Sampler 70 if cfg.SampleRatio >= 1.0 { 71 sampler = trace.AlwaysSample() 72 } else if cfg.SampleRatio <= 0.0 { 73 sampler = trace.NeverSample() 74 } else { 75 sampler = trace.TraceIDRatioBased(cfg.SampleRatio) 76 } 77 78 // Create the TracerProvider with the exporter and sampler 79 tp := trace.NewTracerProvider( 80 trace.WithBatcher(exporter), 81 trace.WithResource(res), 82 trace.WithSampler(sampler), 83 ) 84 85 // Register the global tracer provider 86 otel.SetTracerProvider(tp) 87 88 return &Provider{ 89 enabled: true, 90 tracerProvider: tp, 91 }, nil 92} 93 94// Shutdown flushes any remaining spans and shuts down the tracer provider. 95// It should be called when the application is shutting down to ensure 96// all traces are exported before the process exits. 97func (p *Provider) Shutdown(ctx context.Context) error { 98 if p == nil || !p.enabled || p.tracerProvider == nil { 99 return nil 100 } 101 102 if err := p.tracerProvider.Shutdown(ctx); err != nil { 103 return fmt.Errorf("failed to shutdown tracer provider: %w", err) 104 } 105 106 return nil 107} 108 109// Enabled returns whether tracing is enabled for this provider. 110func (p *Provider) Enabled() bool { 111 if p == nil { 112 return false 113 } 114 return p.enabled 115} 116 117// stripScheme removes the http:// or https:// prefix from an endpoint URL. 118// The HTTP exporter expects just host:port, not a full URL with scheme. 119func stripScheme(endpoint string) string { 120 endpoint = strings.TrimPrefix(endpoint, "http://") 121 endpoint = strings.TrimPrefix(endpoint, "https://") 122 return endpoint 123} 124 125// parseHeaders parses a comma-separated list of key=value pairs into a map. 126// Example: "key1=value1,key2=value2" -> map[string]string{"key1": "value1", "key2": "value2"} 127func parseHeaders(headers string) map[string]string { 128 result := make(map[string]string) 129 pairs := strings.Split(headers, ",") 130 for _, pair := range pairs { 131 pair = strings.TrimSpace(pair) 132 if pair == "" { 133 continue 134 } 135 parts := strings.SplitN(pair, "=", 2) 136 if len(parts) == 2 { 137 key := strings.TrimSpace(parts[0]) 138 value := strings.TrimSpace(parts[1]) 139 if key != "" { 140 result[key] = value 141 } 142 } 143 } 144 return result 145}