From e161e252b3c2ffe859e2d6af97a19403182924a6 Mon Sep 17 00:00:00 2001 From: Elizabeth Worstell Date: Fri, 15 May 2026 00:06:00 -0700 Subject: [PATCH] feat(tracing): add snapshot spans and propagator setup Adds two manual spans on the snapshot create/restore paths and registers a W3C TraceContext + Baggage propagator so trace IDs flow across HTTP boundaries (otelhttp uses the global propagator; without this, every service starts a new trace). New spans: - cachew.snapshot.create - cachew.snapshot.restore Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-019e28cc-12d9-766e-8a0d-7d42c46c824d --- internal/snapshot/snapshot.go | 42 +++++++++++++++++++++++++++++++++-- internal/tracing/tracing.go | 9 ++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/internal/snapshot/snapshot.go b/internal/snapshot/snapshot.go index 03ad579..d43cbe3 100644 --- a/internal/snapshot/snapshot.go +++ b/internal/snapshot/snapshot.go @@ -10,11 +10,18 @@ import ( "time" "github.com/alecthomas/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "github.com/block/cachew/client" "github.com/block/cachew/internal/cache" ) +//nolint:gochecknoglobals // OTel tracer instances are package-scoped by convention +var tracer = otel.Tracer("github.com/block/cachew/internal/snapshot") + // Create archives a directory using tar with zstd compression, then uploads to the cache. // // The archive preserves all file permissions, ownership, and symlinks. @@ -34,7 +41,24 @@ func Create(ctx context.Context, remote cache.Cache, key cache.Key, directory st // Each entry in includePaths is archived relative to baseDir and must exist. // Exclude patterns use tar's --exclude syntax. // threads controls zstd parallelism; 0 uses all available CPU cores. -func CreatePaths(ctx context.Context, remote cache.Cache, key cache.Key, baseDir, archiveName string, includePaths []string, ttl time.Duration, excludePatterns []string, threads int, extraHeaders ...http.Header) error { +func CreatePaths(ctx context.Context, remote cache.Cache, key cache.Key, baseDir, archiveName string, includePaths []string, ttl time.Duration, excludePatterns []string, threads int, extraHeaders ...http.Header) (returnErr error) { + ctx, span := tracer.Start(ctx, "cachew.snapshot.create_and_upload", + trace.WithAttributes( + attribute.String("cache.key", key.String()), + attribute.String("snapshot.archive_name", archiveName), + attribute.Int("snapshot.include_paths", len(includePaths)), + attribute.Int("snapshot.exclude_patterns", len(excludePatterns)), + attribute.Int("snapshot.threads", threads), + ), + ) + defer func() { + if returnErr != nil { + span.RecordError(returnErr) + span.SetStatus(codes.Error, returnErr.Error()) + } + span.End() + }() + headers := make(http.Header) headers.Set("Content-Type", "application/zstd") headers.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", archiveName+".tar.zst")) @@ -71,7 +95,21 @@ func StreamTo(ctx context.Context, w io.Writer, directory string, excludePattern // all file permissions, ownership, and symlinks. // The operation is fully streaming - no temporary files are created. // threads controls zstd parallelism; 0 uses all available CPU cores. -func Restore(ctx context.Context, remote cache.Cache, key cache.Key, directory string, threads int) error { +func Restore(ctx context.Context, remote cache.Cache, key cache.Key, directory string, threads int) (returnErr error) { + ctx, span := tracer.Start(ctx, "cachew.snapshot.restore", + trace.WithAttributes( + attribute.String("cache.key", key.String()), + attribute.Int("snapshot.threads", threads), + ), + ) + defer func() { + if returnErr != nil { + span.RecordError(returnErr) + span.SetStatus(codes.Error, returnErr.Error()) + } + span.End() + }() + rc, _, err := remote.Open(ctx, key) if err != nil { return errors.Wrap(err, "failed to open object") diff --git a/internal/tracing/tracing.go b/internal/tracing/tracing.go index b0a0c7e..2dccdcb 100644 --- a/internal/tracing/tracing.go +++ b/internal/tracing/tracing.go @@ -13,6 +13,7 @@ import ( "github.com/alecthomas/errors" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/trace" ) @@ -41,6 +42,14 @@ func New(ctx context.Context, cfg Config) (stop func(), err error) { provider := trace.NewTracerProvider(trace.WithBatcher(exporter)) otel.SetTracerProvider(provider) + // Register a W3C trace context + baggage propagator so trace IDs + // flow across HTTP boundaries (otelhttp/otelconnect use the global + // propagator). Without this, every service starts a new trace. + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + return func() { _ = provider.Shutdown(context.Background()) //nolint:errcheck // shutdown errors are not actionable }, nil