diff --git a/cmd/go-cache-plugin/commands.go b/cmd/go-cache-plugin/commands.go index 3e419bd..57dce8f 100644 --- a/cmd/go-cache-plugin/commands.go +++ b/cmd/go-cache-plugin/commands.go @@ -18,25 +18,38 @@ import ( "syscall" "time" + "github.com/grafana/go-cache-plugin/lib/otel" + "github.com/creachadair/command" "github.com/creachadair/gocache" "github.com/creachadair/taskgroup" ) var flags struct { - CacheDir string `flag:"cache-dir,default=$GOCACHE_DIR,Local cache directory (required)"` - S3Bucket string `flag:"bucket,default=$GOCACHE_S3_BUCKET,S3 bucket name (required)"` - S3Region string `flag:"region,default=$GOCACHE_S3_REGION,S3 region"` - S3Endpoint string `flag:"s3-endpoint-url,default=$GOCACHE_S3_ENDPOINT_URL,S3 custom endpoint URL (if unset, use AWS default)"` - S3PathStyle bool `flag:"s3-path-style,default=$GOCACHE_S3_PATH_STYLE,S3 path-style URLs (optional)"` - KeyPrefix string `flag:"prefix,default=$GOCACHE_KEY_PREFIX,S3 key prefix (optional)"` - MinUploadSize int64 `flag:"min-upload-size,default=$GOCACHE_MIN_SIZE,Minimum object size to upload to S3 (in bytes)"` - Concurrency int `flag:"c,default=$GOCACHE_CONCURRENCY,Maximum number of concurrent requests"` - S3Concurrency int `flag:"u,default=$GOCACHE_S3_CONCURRENCY,Maximum concurrency for upload to S3"` - PrintMetrics bool `flag:"metrics,default=$GOCACHE_METRICS,Print summary metrics to stderr at exit"` - Expiration time.Duration `flag:"expiry,default=$GOCACHE_EXPIRY,Cache expiration period (optional)"` - Verbose bool `flag:"v,default=$GOCACHE_VERBOSE,Enable verbose logging"` - DebugLog int `flag:"debug,default=$GOCACHE_DEBUG,Enable detailed per-request debug logging (noisy)"` + CacheDir string `flag:"cache-dir,default=$GOCACHE_DIR,Local cache directory (required)"` + S3Bucket string `flag:"bucket,default=$GOCACHE_S3_BUCKET,S3 bucket name (required if no --local flag provided)"` + S3Region string `flag:"region,default=$GOCACHE_S3_REGION,S3 region"` + S3Endpoint string `flag:"s3-endpoint-url,default=$GOCACHE_S3_ENDPOINT_URL,S3 custom endpoint URL (if unset, use AWS default)"` + S3PathStyle bool `flag:"s3-path-style,default=$GOCACHE_S3_PATH_STYLE,S3 path-style URLs (optional)"` + LocalOnlyCache bool `flag:"local-only,default=$GOCACHE_LOCAL_ONLY,Runs in no cache mode (no S3)"` + KeyPrefix string `flag:"prefix,default=$GOCACHE_KEY_PREFIX,S3 key prefix (optional)"` + MinUploadSize int64 `flag:"min-upload-size,default=$GOCACHE_MIN_SIZE,Minimum object size to upload to S3 (in bytes)"` + Concurrency int `flag:"c,default=$GOCACHE_CONCURRENCY,Maximum number of concurrent requests"` + S3Concurrency int `flag:"u,default=$GOCACHE_S3_CONCURRENCY,Maximum concurrency for upload to S3"` + PrintMetrics bool `flag:"metrics,default=$GOCACHE_METRICS,Print summary metrics to stderr at exit"` + Expiration time.Duration `flag:"expiry,default=$GOCACHE_EXPIRY,Cache expiration period (optional)"` + Verbose bool `flag:"v,default=$GOCACHE_VERBOSE,Enable verbose logging"` + DebugLog int `flag:"debug,default=$GOCACHE_DEBUG,Enable detailed per-request debug logging (noisy)"` + TracingEnabled bool `flag:"tracing,default=$GOCACHE_ENABLE_TRACING,Enable tracing (optional)"` + OtelCollectorAddress string `flag:"otel-collector,default=$GOCACHE_TRACING_OTEL_COLLECTOR,OTEL collector address (optional)"` + TracesLogFile string `flag:"traces-log-file,default=$GOCACHE_TRACING_TRACE_FILE,File used to write traces"` + TraceId string `flag:"traceId,default=$GOCAHE_TRACING_TRACE_ID,Trace Id (optional)"` + ParentSpanId string `flag:"parentSpanId,default=$GOCACHE_TRACING_PARENT_SPAN_ID,Parent Span Id (optional)"` + RunId string `flag:"runId,default=$RUN_ID,Run ID (optional)"` + RunAttempt string `flag:"runAttempt,default=$RUN_ATTEMPT,Run attempt (optional)"` + JobName string `flag:"jobName,default=$JOB_NAME,Job name (optional)"` + StepName string `flag:"stepName,default=$STEP_NAME,Step name (optional)"` + StepNumber string `flag:"stepNumber,default=$STEP_NUMBER,Step number (optional)"` } const ( @@ -62,11 +75,12 @@ func runDirect(env *command.Env) error { } var serveFlags struct { - Plugin string `flag:"plugin,default=$GOCACHE_PLUGIN,Plugin service addr (or port) (required)"` - HTTP string `flag:"http,default=$GOCACHE_HTTP,HTTP service address ([host]:port)"` - ModProxy bool `flag:"modproxy,default=$GOCACHE_MODPROXY,Enable a Go module proxy (requires --http)"` - RevProxy string `flag:"revproxy,default=$GOCACHE_REVPROXY,Reverse proxy these hosts (comma-separated; requires --http)"` - SumDB string `flag:"sumdb,default=$GOCACHE_SUMDB,SumDB servers to proxy for (comma-separated)"` + Plugin string `flag:"plugin,default=$GOCACHE_PLUGIN,Plugin service addr (or port) (required)"` + HTTP string `flag:"http,default=$GOCACHE_HTTP,HTTP service address ([host]:port)"` + ModProxy bool `flag:"modproxy,default=$GOCACHE_MODPROXY,Enable a Go module proxy (requires --http)"` + ModNoCache bool `flag:"modproxy-nocache,default=$GOCACHE_MODPROXY_NOCACHE,Disable the module cache (requires --modproxy)"` + RevProxy string `flag:"revproxy,default=$GOCACHE_REVPROXY,Reverse proxy these hosts (comma-separated; requires --http)"` + SumDB string `flag:"sumdb,default=$GOCACHE_SUMDB,SumDB servers to proxy for (comma-separated)"` } func noopClose(context.Context) error { return nil } @@ -126,14 +140,20 @@ func runServe(env *command.Env) error { // If an HTTP server is enabled, start it up with debug routes // and whatever other services were requested. if serveFlags.HTTP != "" { + otelCleanup, tracingContext, err := initModTracing(ctx, "gobuild-modcache") + if err != nil { + return fmt.Errorf("tracing: %w", err) + } + srv := &http.Server{ Addr: serveFlags.HTTP, - Handler: makeHandler(modProxy, revProxy), + Handler: makeHandler(modProxy, revProxy, tracingContext), } g.Go(srv.ListenAndServe) vprintf("HTTP server listening at %q", serveFlags.HTTP) g.Run(func() { <-ctx.Done() + otelCleanup(ctx) vprintf("stopping HTTP service") srv.Shutdown(context.Background()) }) @@ -169,8 +189,13 @@ func runServe(env *command.Env) error { // runConnect implements a direct cache proxy by connecting to a remote server. func runConnect(env *command.Env, plugin string) error { - addr := plugin + ctx := env.Context() + shutdownTracer, reportSpan, err := initTracing(ctx, "gobuild-gocacheprog-connect") + if err != nil { + return err + } + addr := plugin // If the caller has not specified a host/port, then likely this is an older usage which only specifies port if !strings.Contains(plugin, ":") { port, err := strconv.Atoi(plugin) @@ -190,23 +215,90 @@ func runConnect(env *command.Env, plugin string) error { out := taskgroup.Go(func() error { defer conn.(*net.TCPConn).CloseWrite() // let the server finish - return copy(conn, os.Stdin) + return copy(conn, os.Stdin, reportSpan) }) - if rerr := copy(os.Stdout, conn); rerr != nil { + if rerr := copy(os.Stdout, conn, reportSpan); rerr != nil { vprintf("read responses: %v", err) } out.Wait() conn.Close() + + shutdownTracer(context.Background()) vprintf("connection closed (%v elapsed)", time.Since(start)) return nil } +func initTracing(ctx context.Context, service string) (func(context.Context) error, func([]byte), error) { + if !flags.TracingEnabled { + return func(context.Context) error { return nil }, func([]byte) {}, nil + } + + tracingContext, err := initTracingContext() + if err != nil { + return nil, nil, err + } + + shutdown, err := initTracingProvider(ctx, service) + if err != nil { + return nil, nil, err + } + + spanner := otel.NewGoCacheSpanner(tracingContext) + + spanReporter := func(buffer []byte) { + _ = spanner.ProcessCacheRequest(ctx, buffer) + } + + return shutdown, spanReporter, err +} + +func initModTracing(ctx context.Context, service string) (func(context.Context) error, *otel.TracingContext, error) { + if !flags.TracingEnabled { + return func(context.Context) error { return nil }, nil, nil + } + + tracingContext, err := initTracingContext() + if err != nil { + return nil, nil, err + } + + shutdown, err := initTracingProvider(ctx, service) + if err != nil { + return nil, nil, err + } + + return shutdown, tracingContext, err +} + +func initTracingProvider(ctx context.Context, service string) (func(context.Context) error, error) { + var shutdown func(context.Context) error + var err error + if flags.OtelCollectorAddress != "" { + shutdown, err = otel.SetupOtelTraceProvider(ctx, service, flags.OtelCollectorAddress) + } else if flags.TracesLogFile != "" { + log.Printf("Otel Collector address not specified, starting with the logging reporter, log file: %s", flags.TracesLogFile) + shutdown, err = otel.SetupLoggingProvider(ctx, service, flags.TracesLogFile) + } else { + log.Printf("please specify either --otel-collector or --log-file to setup tracing or disable tracing") + return nil, errors.New("otel exporter not initialized") + } + return shutdown, err +} + +func initTracingContext() (*otel.TracingContext, error) { + if flags.TraceId != "" && flags.ParentSpanId != "" { + return otel.NewTracingContext(flags.TraceId, flags.ParentSpanId) + } else { + return otel.NewTracingContextFromRunData(flags.RunId, flags.RunAttempt, flags.JobName, flags.StepName, flags.StepNumber), nil + } +} + // copy emulates the base case of io.Copy, but does not attempt to use the // io.ReaderFrom or io.WriterTo implementations. // // TODO(creachadair): For some reason io.Copy does not work correctly when r is // a pipe (e.g., stdin) and w is a TCP socket. Figure out why. -func copy(w io.Writer, r io.Reader) error { +func copy(w io.Writer, r io.Reader, reportTrace func([]byte)) error { var buf [4096]byte for { nr, err := r.Read(buf[:]) @@ -216,6 +308,8 @@ func copy(w io.Writer, r io.Reader) error { } else if nw < nr { return fmt.Errorf("wrote %d < %d bytes: %w", nw, nr, io.ErrShortWrite) } + + reportTrace(buf[:]) } if err == io.EOF { return nil diff --git a/cmd/go-cache-plugin/go-cache-plugin.go b/cmd/go-cache-plugin/go-cache-plugin.go index d3cd111..2ab7ed5 100644 --- a/cmd/go-cache-plugin/go-cache-plugin.go +++ b/cmd/go-cache-plugin/go-cache-plugin.go @@ -72,7 +72,6 @@ When --http is enabled, the following options are available: This mode bridges stdin/stdout to a cache server (see the "serve" command) listening on the specified port.`, - Run: command.Adapt(runConnect), }, command.HelpCommand(helpTopics), diff --git a/cmd/go-cache-plugin/setup.go b/cmd/go-cache-plugin/setup.go index beee4e4..1395894 100644 --- a/cmd/go-cache-plugin/setup.go +++ b/cmd/go-cache-plugin/setup.go @@ -18,6 +18,8 @@ import ( "strings" "time" + "github.com/grafana/go-cache-plugin/lib/otel" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -39,8 +41,34 @@ func initCacheServer(env *command.Env) (*gocache.Server, *s3util.Client, error) switch { case flags.CacheDir == "": return nil, nil, env.Usagef("you must provide a --cache-dir") - case flags.S3Bucket == "": - return nil, nil, env.Usagef("you must provide an S3 --bucket name") + case flags.LocalOnlyCache: + dirCache, err := cachedir.New(flags.CacheDir) + if err != nil { + return nil, nil, fmt.Errorf("create local cache: %w", err) + } + + cacheClose := func(context.Context) error { return nil } + if flags.Expiration > 0 { + dirClose := dirCache.Cleanup(flags.Expiration) + cacheClose = func(ctx context.Context) error { + return errors.Join(dirClose(ctx)) + } + } + + setMetrics := func(ctx context.Context, m *expvar.Map) {} + s := &gocache.Server{ + Get: dirCache.Get, + Put: dirCache.Put, + Close: cacheClose, + SetMetrics: setMetrics, + MaxRequests: flags.Concurrency, + Logf: vprintf, + LogRequests: flags.DebugLog&debugBuildCache != 0, + } + return s, nil, nil + + case flags.S3Bucket == "" && !flags.LocalOnlyCache: + return nil, nil, env.Usagef("you must provide an S3 --bucket name or run with --no-cache") } region, err := getBucketRegion(env.Context(), flags.S3Bucket) if err != nil { @@ -112,19 +140,30 @@ func initModProxy(env *command.Env, s3c *s3util.Client) (_ http.Handler, cleanup return nil, nil, env.Usagef("you must set --http to enable --modproxy") } - modCachePath := filepath.Join(flags.CacheDir, "module") - if err := os.MkdirAll(modCachePath, 0755); err != nil { - return nil, nil, fmt.Errorf("create module cache: %w", err) + if s3c == nil && !serveFlags.ModNoCache { + return nil, nil, errors.New("s3 client not configured") } - cacher := &modproxy.S3Cacher{ - Local: modCachePath, - S3Client: s3c, - KeyPrefix: path.Join(flags.KeyPrefix, "module"), - MaxTasks: flags.S3Concurrency, - Logf: vprintf, - LogRequests: flags.DebugLog&debugModProxy != 0, + + var cacher goproxy.Cacher = nil + var metrics = func() *expvar.Map { return &expvar.Map{} } + if s3c != nil { + modCachePath := filepath.Join(flags.CacheDir, "module") + if err := os.MkdirAll(modCachePath, 0755); err != nil { + return nil, nil, fmt.Errorf("create module cache: %w", err) + } + + cacher := &modproxy.S3Cacher{ + Local: modCachePath, + S3Client: s3c, + KeyPrefix: path.Join(flags.KeyPrefix, "module"), + MaxTasks: flags.S3Concurrency, + Logf: vprintf, + LogRequests: flags.DebugLog&debugModProxy != 0, + } + cleanup = func() { vprintf("close cacher (err=%v)", cacher.Close()) } + metrics = cacher.Metrics } - cleanup = func() { vprintf("close cacher (err=%v)", cacher.Close()) } + proxy := &goproxy.Goproxy{ Fetcher: &goproxy.GoFetcher{ // As configured, the fetcher should never shell out to the go @@ -142,7 +181,7 @@ func initModProxy(env *command.Env, s3c *s3util.Client) (_ http.Handler, cleanup proxy.ProxiedSumDBs = strings.Split(serveFlags.SumDB, ",") vprintf("enabling sum DB proxy for %s", strings.Join(proxy.ProxiedSumDBs, ", ")) } - expvar.Publish("modcache", cacher.Metrics()) + expvar.Publish("modcache", metrics()) return http.StripPrefix("/mod", proxy), cleanup, nil } @@ -268,7 +307,7 @@ func initServerCert(env *command.Env, hosts []string) (tls.Certificate, error) { // makeHandler returns an HTTP handler that dispatches requests to debug // handlers or to the specified proxies, if they are defined. -func makeHandler(modProxy, revProxy http.Handler) http.HandlerFunc { +func makeHandler(modProxy, revProxy http.Handler, tracingContext *otel.TracingContext) http.HandlerFunc { mux := http.NewServeMux() tsweb.Debugger(mux) return func(w http.ResponseWriter, r *http.Request) { @@ -289,6 +328,11 @@ func makeHandler(modProxy, revProxy http.Handler) http.HandlerFunc { return } if modProxy != nil && r.Method == http.MethodGet && strings.HasPrefix(path, "/mod/") { + if tracingContext != nil { + _, span := tracingContext.SpanWithContext(r.Context(), strings.TrimPrefix(path, "/mod/")) + defer span.End() + } + modProxy.ServeHTTP(w, r) return } diff --git a/go.mod b/go.mod index b557355..492f0a6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 toolchain go1.24.2 require ( + github.com/aws/aws-sdk-go-v2 v1.36.0 github.com/aws/aws-sdk-go-v2/config v1.29.5 github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 github.com/creachadair/atomicfile v0.3.7 @@ -17,15 +18,21 @@ require ( github.com/creachadair/taskgroup v0.13.2 github.com/creachadair/tlsutil v0.0.0-20241111194928-a9f540254538 github.com/goproxy/goproxy v0.18.0 - golang.org/x/sync v0.13.0 - golang.org/x/sys v0.32.0 + go.opentelemetry.io/auto/sdk v1.1.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + golang.org/x/net v0.43.0 + golang.org/x/sync v0.16.0 + golang.org/x/sys v0.35.0 honnef.co/go/tools v0.6.1 tailscale.com v1.82.5 ) require ( github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect - github.com/aws/aws-sdk-go-v2 v1.36.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect @@ -41,15 +48,28 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect github.com/aws/smithy-go v1.22.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/creachadair/msync v0.4.0 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.35.0 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.36.0 // indirect - golang.org/x/tools v0.30.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) retract ( diff --git a/go.sum b/go.sum index c590d3d..4d7153a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5 github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/creachadair/atomicfile v0.3.7 h1:wdg8+Isz07NDMi2yZQAoI1EKB9SxuDhvo5MUii/ZqlM= github.com/creachadair/atomicfile v0.3.7/go.mod h1:lUrZrE/XjMA7rJY/n8dF7/sSpy6KjtPaxPbrDambthA= github.com/creachadair/command v0.1.20 h1:t19yejpScyH37RrRdDRahqWwUOG606sPwuBPSsFgZoQ= @@ -56,42 +58,97 @@ github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoi github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g= github.com/creachadair/tlsutil v0.0.0-20241111194928-a9f540254538 h1:a7Fm+PrmryX8BEDZ/ACyJfNwsRN9+helUaHmKrwZRww= github.com/creachadair/tlsutil v0.0.0-20241111194928-a9f540254538/go.mod h1:yr2fVialCe/CT6ORx9Vpb7MVKo+SlcZ9Q9yNFcNvCXw= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/goproxy/goproxy v0.18.0 h1:Wc6nBKQbiFvzRdPmMPPQUnMJJc8Gl/0TJhqUsm4kWJk= github.com/goproxy/goproxy v0.18.0/go.mod h1:swiTJu+YoEN4We14bsBhRG2q3ReI3Xl9fvdXjNPknQI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek= go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8= golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= -golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= tailscale.com v1.82.5 h1:p5owmyPoPM1tFVHR3LjquFuLfpZLzafvhe5kjVavHtE= diff --git a/lib/otel/collectors.go b/lib/otel/collectors.go new file mode 100644 index 0000000..347defe --- /dev/null +++ b/lib/otel/collectors.go @@ -0,0 +1,60 @@ +package otel + +import ( + "context" + "os" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" +) + +var tp *trace.TracerProvider + +func SetupLoggingProvider(ctx context.Context, service, file string) (func(context.Context) error, error) { + f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return nil, err + } + exporter, err := stdouttrace.New(stdouttrace.WithWriter(f)) + if err != nil { + return nil, err + } + shutdownHook := setupTraceProvider(service, exporter) + return shutdownHook, nil +} + +func SetupOtelTraceProvider(ctx context.Context, service, address string) (func(context.Context) error, error) { + exporter, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(address), + otlptracegrpc.WithInsecure(), + ) + + if err != nil { + return nil, err + } + + shutdownHook := setupTraceProvider(service, exporter) + + return shutdownHook, nil +} + +func setupTraceProvider(service string, exporter trace.SpanExporter) func(ctx context.Context) error { + res := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(service), + attribute.String("env", "ci"), + ) + + tp = trace.NewTracerProvider( + trace.WithBatcher(exporter), + trace.WithResource(res), + ) + + otel.SetTracerProvider(tp) + return tp.Shutdown +} diff --git a/lib/otel/gocache_spanner.go b/lib/otel/gocache_spanner.go new file mode 100644 index 0000000..927811e --- /dev/null +++ b/lib/otel/gocache_spanner.go @@ -0,0 +1,123 @@ +package otel + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "sync" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +// GoCacheSpanner The sole purpose of GoCacheSpanner is to intercept GOCACHEPROG commands and report traces +type GoCacheSpanner struct { + tracingContext *TracingContext + mutex sync.Mutex + data map[string]trace.Span +} + +func NewGoCacheSpanner(context *TracingContext) *GoCacheSpanner { + return &GoCacheSpanner{tracingContext: context, data: make(map[string]trace.Span)} +} + +type CacheRequest struct { + Id string `json:"ID"` + Miss bool `json:"Miss"` + ActionId string `json:"ActionID"` + Command string `json:"Command"` +} + +func (m *GoCacheSpanner) ProcessCacheRequest(ctx context.Context, data []byte) error { + request, err := parseCacheRequest(data) + if err != nil { + return err + } + + m.ProcessId(ctx, request) + return nil +} + +func (m *GoCacheSpanner) ProcessId(ctx context.Context, request CacheRequest) { + m.mutex.Lock() + defer m.mutex.Unlock() + + span, ok := m.data[request.Id] + if ok { + if request.Miss { + span.SetStatus(codes.Error, "Cache Miss") + } else { + span.SetStatus(codes.Ok, "Cache Hit") + } + + span.End() + delete(m.data, request.Id) + return + } + + _, span = m.tracingContext.SpanWithContext( + ctx, + fmt.Sprintf("Cache-%s", request.ActionId), + attribute.KeyValue{Key: "command", Value: attribute.StringValue(request.Command)}, + ) + m.data[request.Id] = span +} + +func parseCacheRequest(buf []byte) (CacheRequest, error) { + var data map[string]any + + err := json.Unmarshal(buf, &data) + if err != nil { + slice, _, found := bytes.Cut(buf, []byte{'\n'}) + if found { + err2 := json.Unmarshal(slice, &data) + if err2 != nil { + return CacheRequest{}, err + } + } + } + + var id string + var miss bool + var command string + var actionId string + + id1, ok := data["ID"] + if !ok { + return CacheRequest{}, errors.New("id field not found in the request") + } else { + id = fmt.Sprint(id1) + } + + command1, ok := data["Command"] + if !ok { + command = "" + } else { + command = fmt.Sprint(command1) + } + + _, ok = data["Miss"] + if !ok { + miss = false + } else { + miss = true + } + + actionId1, ok := data["ActionID"] + if !ok { + actionId = "" + } else { + actionId = fmt.Sprint(actionId1) + } + + data2 := CacheRequest{ + Id: id, + ActionId: actionId, + Miss: miss, + Command: command, + } + return data2, nil +} diff --git a/lib/otel/traces.go b/lib/otel/traces.go new file mode 100644 index 0000000..8effb86 --- /dev/null +++ b/lib/otel/traces.go @@ -0,0 +1,92 @@ +package otel + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type TracingContext struct { + TraceID trace.TraceID + ParentSpanID trace.SpanID +} + +func NewTracingContext(traceId, parentSpanId string) (*TracingContext, error) { + traceIDFromHex, err := trace.TraceIDFromHex(traceId) + if err != nil { + return nil, err + } + spanIDFromHex, err := trace.SpanIDFromHex(parentSpanId) + if err != nil { + return nil, err + } + + return &TracingContext{ + TraceID: traceIDFromHex, + ParentSpanID: spanIDFromHex, + }, nil +} + +func NewTracingContextFromRunData(runId, runAttempt, jobName, stepName, stepNumber string) *TracingContext { + traceId, _ := trace.TraceIDFromHex(GenerateTraceID(runId, runAttempt)) + spanId, _ := trace.SpanIDFromHex(GenerateStepSpanID_Number(runId, runAttempt, jobName, stepNumber)) + + return &TracingContext{ + TraceID: traceId, + ParentSpanID: spanId, + } +} + +func (t *TracingContext) SpanWithContext(context context.Context, name string, attributes ...attribute.KeyValue) (context.Context, trace.Span) { + + spanContext := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: t.TraceID, + SpanID: t.ParentSpanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }) + + ctx := trace.ContextWithSpanContext(context, spanContext) + tracer := otel.Tracer("github.com/grafana/go-cache-plugin") + + start, span := tracer.Start(ctx, name) + if len(attributes) > 0 { + span.SetAttributes(attributes...) + } + + return start, span +} + +func GenerateTraceID(runID, runAttempt string) string { + input := fmt.Sprintf("%s%st", runID, runAttempt) + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:])[:32] +} + +func GenerateParentSpanID(runID, runAttempt string) string { + input := fmt.Sprintf("%s%ss", runID, runAttempt) + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:])[16:32] +} + +func GenerateJobSpanID(runID, runAttempt, jobName string) string { + input := fmt.Sprintf("%s%s%s", runID, runAttempt, jobName) + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:])[16:32] +} + +func GenerateStepSpanID(runID, runAttempt, jobName, stepName string) string { + input := fmt.Sprintf("%s%s%s%s", runID, runAttempt, jobName, stepName) + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:])[16:32] +} + +func GenerateStepSpanID_Number(runID, runAttempt, jobName, stepNumber string) string { + input := fmt.Sprintf("%s%s%s%s", runID, runAttempt, jobName, stepNumber) + hash := sha256.Sum256([]byte(input)) + return hex.EncodeToString(hash[:])[16:32] +} diff --git a/lib/otel/traces_test.go b/lib/otel/traces_test.go new file mode 100644 index 0000000..02ac265 --- /dev/null +++ b/lib/otel/traces_test.go @@ -0,0 +1,78 @@ +package otel + +import ( + "testing" +) + +var ( + runId = "20137834310" + runAttempt = "1" + jobName = "build" + //jobNumber = "57796136451" + stepName = "Build Grafana" + stepNumber = "8" +) + +func TestGenerateTraceID(t *testing.T) { + expected := "cfe93a0cde6f53f539e0eaff28e05efc" + actual := GenerateTraceID(runId, runAttempt) + + if actual != expected { + t.Errorf("GenerateTraceID(%s, %s) = %s; want %s", runId, runAttempt, actual, expected) + } +} + +func TestGenerateRootSpanID(t *testing.T) { + expected := "7e173813d01cc668" + actual := GenerateParentSpanID(runId, runAttempt) + + if actual != expected { + t.Errorf("GenerateParentSpanID(%s, %s) = %s; want %s", runId, runAttempt, actual, expected) + } +} + +func TestGenerateJobSpanID(t *testing.T) { + expected := "b6f4de49eeda5bb7" + actual := GenerateJobSpanID(runId, runAttempt, jobName) + + if actual != expected { + t.Errorf("GenerateJobSpanID(%s, %s, %s) = %s; want %s", runId, runAttempt, jobName, actual, expected) + } +} + +func TestGenerateStepSpanID(t *testing.T) { + expected := "eecd067487a1f884" + actual := GenerateStepSpanID(runId, runAttempt, jobName, stepName) + + if actual != expected { + t.Errorf("GenerateStepSpanID(%s, %s, %s, %s) = %s; want %s", runId, runAttempt, jobName, stepName, actual, expected) + } +} + +func TestGenerateStepSpanID_Number(t *testing.T) { + expected := "d70487f07693281c" + actual := GenerateStepSpanID_Number(runId, runAttempt, jobName, stepNumber) + + if actual != expected { + t.Errorf("GenerateStepSpanID_Number(%s, %s, %s, %s) = %s; want %s", runId, runAttempt, jobName, stepNumber, actual, expected) + } +} +func TestDeterministic(t *testing.T) { + trace1 := GenerateTraceID(runId, runAttempt) + trace2 := GenerateTraceID(runId, runAttempt) + if trace1 != trace2 { + t.Errorf("TraceID not deterministic: %s != %s", trace1, trace2) + } + + job1 := GenerateJobSpanID(runId, runAttempt, jobName) + job2 := GenerateJobSpanID(runId, runAttempt, jobName) + if job1 != job2 { + t.Errorf("JobSpanID not deterministic: %s != %s", job1, job2) + } + + step1 := GenerateStepSpanID(runId, runAttempt, jobName, stepName) + step2 := GenerateStepSpanID(runId, runAttempt, jobName, stepName) + if step1 != step2 { + t.Errorf("StepSpanID not deterministic: %s != %s", step1, step2) + } +}