Compare commits

3 Commits
main ... v0.1.0

Author SHA1 Message Date
1d6cd95909 Add full-observability example: metrics and tracing together
All checks were successful
Publish / publish (push) Successful in 19s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:22:32 +03:00
899a1da701 Add basic-tracing example: nested spans and graceful shutdown
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:22:28 +03:00
d85094b86d Add basic-metrics example: counter, histogram, gauge, and /metrics endpoint
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:22:23 +03:00
3 changed files with 236 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
// Prometheus metrics: counter, histogram, gauge, and /metrics endpoint.
package main
import (
"fmt"
"log"
"math/rand/v2"
"net/http"
"time"
"git.codelab.vc/pkg/obsx"
)
func main() {
metrics := obsx.NewMetrics(obsx.MetricsConfig{
Namespace: "myapp",
Subsystem: "api",
})
requests := metrics.Counter("requests_total", "Total HTTP requests", "method", "status")
duration := metrics.Histogram("request_duration_seconds", "Request latency", nil, "method")
inflight := metrics.Gauge("inflight_requests", "Current in-flight requests")
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
inflight.WithLabelValues().Inc()
defer inflight.WithLabelValues().Dec()
start := time.Now()
// Simulate work.
time.Sleep(time.Duration(rand.IntN(100)) * time.Millisecond)
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "hello, world")
requests.WithLabelValues("GET", "200").Inc()
duration.WithLabelValues("GET").Observe(time.Since(start).Seconds())
})
mux.Handle("GET /metrics", metrics.Handler())
log.Println("listening on :8080 (try GET /hello, GET /metrics)")
log.Fatal(http.ListenAndServe(":8080", mux))
}

View File

@@ -0,0 +1,83 @@
// OpenTelemetry tracing with nested spans and graceful shutdown.
package main
import (
"context"
"fmt"
"log"
"math/rand/v2"
"net/http"
"os"
"os/signal"
"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()
shutdown, err := obsx.SetupTracer(ctx, obsx.TracerConfig{
ServiceName: "basic-tracing",
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)
}
}()
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
ctx, span := obsx.StartSpan(r.Context(), "HandleGetUser")
defer span.End()
userID := r.PathValue("id")
span.SetAttributes(attribute.String("user.id", userID))
user, err := fetchUser(ctx, userID)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
span.SetStatus(codes.Ok, "")
fmt.Fprintln(w, user)
})
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
<-ctx.Done()
_ = srv.Shutdown(context.Background())
}()
log.Println("listening on :8080 (try GET /users/42)")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
// fetchUser simulates a database query with its own child span.
func fetchUser(ctx context.Context, id string) (string, error) {
ctx, span := obsx.StartSpan(ctx, "db.QueryUser")
defer span.End()
span.SetAttributes(attribute.String("db.statement", "SELECT * FROM users WHERE id = $1"))
// Simulate DB latency.
time.Sleep(time.Duration(rand.IntN(50)) * time.Millisecond)
_ = ctx // context carries the span for further nesting if needed
return fmt.Sprintf(`{"id": %q, "name": "Alice"}`, id), nil
}

View File

@@ -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
}