mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-21 06:37:21 +00:00
Add tracing and relevant tests (#21)
* Add tracing and relevant tests. * fixup! Add tracing and relevant tests. * gofmt the code 🤷 * fixup! gofmt the code 🤷
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"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"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
conn, err := grpc.DialContext(ctx, endpoint,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock(),
|
||||
grpc.WithReturnConnectionError(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err)
|
||||
}
|
||||
|
||||
exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
|
||||
}
|
||||
|
||||
res, err := resource.New(ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName("graphql-monitoring-proxy"),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create resource: %w", err)
|
||||
}
|
||||
|
||||
tracerProvider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(exporter),
|
||||
sdktrace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tracerProvider)
|
||||
otel.SetTextMapPropagator(propagation.TraceContext{})
|
||||
|
||||
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.tracer == nil {
|
||||
return trace.SpanFromContext(ctx), ctx
|
||||
}
|
||||
ctx, span := ts.tracer.Start(ctx, name)
|
||||
return span, ctx
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
func TestParseTraceHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
header string
|
||||
want *TraceSpanInfo
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid trace header",
|
||||
header: `{"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}`,
|
||||
want: &TraceSpanInfo{
|
||||
TraceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
header: `{"traceparent": invalid}`,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty header",
|
||||
header: "",
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseTraceHeader(tt.header)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseTraceHeader() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr {
|
||||
gotJSON, _ := json.Marshal(got)
|
||||
wantJSON, _ := json.Marshal(tt.want)
|
||||
if string(gotJSON) != string(wantJSON) {
|
||||
t.Errorf("ParseTraceHeader() = %v, want %v", string(gotJSON), string(wantJSON))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTracing(t *testing.T) {
|
||||
// Skip actual connection tests since they require a running collector
|
||||
t.Run("empty endpoint", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, err := NewTracing(ctx, "")
|
||||
assert.Error(t, err, "Expected error for empty endpoint")
|
||||
assert.Contains(t, err.Error(), "endpoint cannot be empty")
|
||||
})
|
||||
|
||||
t.Run("nil context", func(t *testing.T) {
|
||||
_, err := NewTracing(nil, "localhost:4317")
|
||||
assert.Error(t, err, "Expected error for nil context")
|
||||
assert.Contains(t, err.Error(), "context cannot be nil")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTracingSetup_ExtractSpanContext(t *testing.T) {
|
||||
ts := &TracingSetup{}
|
||||
spanInfo := &TraceSpanInfo{
|
||||
TraceParent: "invalid-traceparent",
|
||||
}
|
||||
|
||||
_, err := ts.ExtractSpanContext(spanInfo)
|
||||
assert.Error(t, err, "Expected error for invalid traceparent")
|
||||
assert.Contains(t, err.Error(), "invalid span context")
|
||||
}
|
||||
|
||||
func TestTracingSetup_StartSpan(t *testing.T) {
|
||||
ts := &TracingSetup{}
|
||||
ctx := context.Background()
|
||||
|
||||
span, newCtx := ts.StartSpan(ctx, "test-span")
|
||||
assert.NotNil(t, span, "Expected non-nil span even when tracer is nil")
|
||||
assert.NotNil(t, newCtx, "Expected non-nil context")
|
||||
assert.Equal(t, trace.SpanFromContext(ctx), span, "Expected span from context when tracer is nil")
|
||||
}
|
||||
|
||||
func TestTracingSetup_Shutdown(t *testing.T) {
|
||||
ts := &TracingSetup{}
|
||||
ctx := context.Background()
|
||||
|
||||
err := ts.Shutdown(ctx)
|
||||
assert.NoError(t, err, "Expected no error when shutting down nil tracer provider")
|
||||
}
|
||||
Reference in New Issue
Block a user