From 1d6cd95909c70a62073c70bf199acaf516d8df22 Mon Sep 17 00:00:00 2001 From: Aleksey Shakhmatov Date: Mon, 23 Mar 2026 17:22:32 +0300 Subject: [PATCH] Add full-observability example: metrics and tracing together Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/full-observability/main.go | 107 ++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 examples/full-observability/main.go diff --git a/examples/full-observability/main.go b/examples/full-observability/main.go new file mode 100644 index 0000000..84bc8dc --- /dev/null +++ b/examples/full-observability/main.go @@ -0,0 +1,107 @@ +// Full observability: Prometheus metrics and OpenTelemetry tracing together. +package main + +import ( + "context" + "fmt" + "log" + "math/rand/v2" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + "git.codelab.vc/pkg/obsx" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + // --- Tracing --- + shutdown, err := obsx.SetupTracer(ctx, obsx.TracerConfig{ + ServiceName: "full-observability", + ServiceVersion: "0.1.0", + Endpoint: "localhost:4317", + }) + if err != nil { + log.Fatal(err) + } + defer func() { + if err := shutdown(context.Background()); err != nil { + log.Printf("tracer shutdown: %v", err) + } + }() + + // --- Metrics --- + metrics := obsx.NewMetrics(obsx.MetricsConfig{ + Namespace: "myapp", + Subsystem: "orders", + }) + + requests := metrics.Counter("requests_total", "Total requests", "method", "status") + duration := metrics.Histogram("request_duration_seconds", "Latency", nil, "method") + inflight := metrics.Gauge("inflight_requests", "In-flight requests") + + // --- Routes --- + mux := http.NewServeMux() + + mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) { + inflight.WithLabelValues().Inc() + defer inflight.WithLabelValues().Dec() + start := time.Now() + + ctx, span := obsx.StartSpan(r.Context(), "HandleCreateOrder") + defer span.End() + + orderID, err := createOrder(ctx) + + status := http.StatusCreated + if err != nil { + status = http.StatusInternalServerError + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + http.Error(w, err.Error(), status) + } else { + span.SetStatus(codes.Ok, "") + w.WriteHeader(status) + fmt.Fprintf(w, `{"order_id": %q}`+"\n", orderID) + } + + requests.WithLabelValues("POST", strconv.Itoa(status)).Inc() + duration.WithLabelValues("POST").Observe(time.Since(start).Seconds()) + }) + + mux.Handle("GET /metrics", metrics.Handler()) + + // --- Server --- + srv := &http.Server{Addr: ":8080", Handler: mux} + + go func() { + <-ctx.Done() + _ = srv.Shutdown(context.Background()) + }() + + log.Println("listening on :8080 (try POST /orders, GET /metrics)") + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatal(err) + } +} + +// createOrder simulates order creation with a child span. +func createOrder(ctx context.Context) (string, error) { + _, span := obsx.StartSpan(ctx, "db.InsertOrder") + defer span.End() + + span.SetAttributes(attribute.String("db.statement", "INSERT INTO orders ...")) + + // Simulate DB latency. + time.Sleep(time.Duration(rand.IntN(80)) * time.Millisecond) + + orderID := fmt.Sprintf("ord-%d", rand.IntN(100000)) + span.SetAttributes(attribute.String("order.id", orderID)) + return orderID, nil +}