diff --git a/tracing/tracing.go b/tracing/tracing.go new file mode 100644 index 0000000..466df0f --- /dev/null +++ b/tracing/tracing.go @@ -0,0 +1,98 @@ +package libpack_trace + +import ( + "context" + "fmt" + "time" + + libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" + libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + oteltrace "go.opentelemetry.io/otel/trace" + + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +func NewClient(log *libpack_logging.Logger, otelGRPCCollector string, attr ...attribute.KeyValue) (func(), error) { + attr = append(attr, semconv.ServiceNameKey.String(libpack_config.PKG_NAME)) + fmt.Printf("Starting OpenTelemetry tracer: otlp, configured with endpoint: %s\n", otelGRPCCollector) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client := otlptracegrpc.NewClient( + otlptracegrpc.WithInsecure(), + otlptracegrpc.WithEndpoint(otelGRPCCollector), + ) + + exporter, err := otlptrace.New(ctx, client) + if err != nil { + log.Error(&libpack_logging.LogMessage{ + Message: "Failed to create exporter", + Pairs: map[string]interface{}{"error": err}, + }) + return nil, err + } + + tp := trace.NewTracerProvider( + trace.WithSampler(trace.AlwaysSample()), + trace.WithBatcher(exporter, trace.WithMaxExportBatchSize(1), trace.WithBatchTimeout(30*time.Second)), + trace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, attr...)), + ) + + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + shutdownFunc := func() { + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + log.Info(&libpack_logging.LogMessage{ + Message: "Shutting down tracer", + Pairs: nil, + }) + if err := tp.Shutdown(shutdownCtx); err != nil { + log.Warning(&libpack_logging.LogMessage{ + Message: "Failed to shutdown tracer provider", + Pairs: map[string]interface{}{"error": err}, + }) + } + } + + return shutdownFunc, nil +} + +func TraceContextInject(ctx context.Context) map[string]string { + carrier := propagation.MapCarrier{} + propagator := otel.GetTextMapPropagator() + propagator.Inject(ctx, carrier) + return map[string]string(carrier) +} + +func TraceContextExtract(ctx context.Context, traceContext map[string]string) context.Context { + carrier := propagation.MapCarrier(traceContext) + propagator := otel.GetTextMapPropagator() + return propagator.Extract(ctx, carrier) +} + +func StartSpanFromContext(ctx context.Context, operationName string) (context.Context, oteltrace.Span) { + tr := otel.GetTracerProvider().Tracer("") + return tr.Start(ctx, operationName, oteltrace.WithSpanKind(oteltrace.SpanKindServer)) +} + +func ContinueSpanFromContext(ctx context.Context, operationName string) (context.Context, oteltrace.Span) { + tr := otel.GetTracerProvider().Tracer("") + options := []oteltrace.SpanStartOption{ + oteltrace.WithSpanKind(oteltrace.SpanKindInternal), + oteltrace.WithAttributes(attribute.String("cont", "true")), + } + return tr.Start(ctx, operationName, options...) +} + +func AddAttributesToSpan(span oteltrace.Span, attributes ...attribute.KeyValue) { + span.SetAttributes(attributes...) +} diff --git a/tracing/tracing_test.go b/tracing/tracing_test.go new file mode 100644 index 0000000..15c146a --- /dev/null +++ b/tracing/tracing_test.go @@ -0,0 +1,109 @@ +package libpack_trace + +import ( + "testing" + + libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type TraceTestSuite struct { + suite.Suite + logger *libpack_logging.Logger +} + +func (suite *TraceTestSuite) SetupTest() { + suite.logger = libpack_logging.New() +} + +func (suite *TraceTestSuite) TearDownTest() { + // Any cleanup logic can be added here +} + +func TestTraceTestSuite(t *testing.T) { + suite.Run(t, new(TraceTestSuite)) +} + +func (suite *TraceTestSuite) Test_NewClient() { + shutdownFunc, err := NewClient(suite.logger, "localhost:4317") + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), shutdownFunc) + + shutdownFunc() +} + +// func (suite *TraceTestSuite) Test_TraceContextInjectExtract() { +// ctx := context.Background() +// traceContext := TraceContextInject(ctx) +// assert.NotEmpty(suite.T(), traceContext) + +// extractedCtx := TraceContextExtract(ctx, traceContext) +// assert.NotNil(suite.T(), extractedCtx) +// } + +// func (suite *TraceTestSuite) Test_StartSpanFromContext() { +// ctx := context.Background() +// ctx, span := StartSpanFromContext(ctx, "operation") +// assert.NotNil(suite.T(), ctx) +// assert.NotNil(suite.T(), span) +// span.End() +// } + +// func (suite *TraceTestSuite) Test_ContinueSpanFromContext() { +// ctx := context.Background() +// ctx, span := ContinueSpanFromContext(ctx, "operation") +// assert.NotNil(suite.T(), ctx) +// assert.NotNil(suite.T(), span) +// span.End() +// } + +// func (suite *TraceTestSuite) Test_AddAttributesToSpan() { +// ctx := context.Background() +// _, span := StartSpanFromContext(ctx, "operation") + +// attributes := []attribute.KeyValue{ +// attribute.String("key1", "value1"), +// attribute.Int("key2", 2), +// } +// AddAttributesToSpan(span, attributes...) +// span.End() + +// // Create an in-memory span exporter +// exporter := tracetest.NewSpanRecorder() +// tracerProvider := trace.NewTracerProvider(trace.WithSpanProcessor(exporter)) +// otel.SetTracerProvider(tracerProvider) + +// // Verify the span attributes +// spans := exporter.Ended() +// assert.Len(suite.T(), spans, 1) +// exportedSpan := spans[0] + +// for _, attr := range attributes { +// assert.Contains(suite.T(), exportedSpan.Attributes(), attr) +// } +// } + +// func (suite *TraceTestSuite) Test_Shutdown() { +// shutdownFunc, err := NewClient(suite.logger, "localhost:4317") +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), shutdownFunc) + +// shutdownFunc() +// logOutput := captureStdOut(func() { suite.logger.Info(&libpack_logging.LogMessage{Message: "Shutting down tracer"}) }) +// assert.Contains(suite.T(), logOutput, "Shutting down tracer") +// } + +// // Helper function to capture standard output for testing logs +// func captureStdOut(f func()) string { +// originalStdout := os.Stdout +// r, w, _ := os.Pipe() +// os.Stdout = w +// f() +// w.Close() +// var buf bytes.Buffer +// buf.ReadFrom(r) +// os.Stdout = originalStdout +// return buf.String() +// }