diff --git a/cmd/server/server.go b/cmd/server/server.go index ad5764b..d3e2f6f 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/k6build/pkg/store" "github.com/grafana/k6build/pkg/store/client" "github.com/grafana/k6build/pkg/store/s3" + "github.com/grafana/k6build/pkg/tracing" "github.com/grafana/k6build/pkg/util" "github.com/prometheus/client_golang/prometheus" @@ -215,6 +216,7 @@ type serverConfig struct { verbose bool shutdownTimeout time.Duration cacheMaxAge time.Duration + otelEndpoint string } // New creates new cobra command for the server command. @@ -239,6 +241,15 @@ func New() *cobra.Command { //nolint:funlen return err } + shutdownTracer, err := tracing.Setup(cmd.Context(), tracing.Config{ + Endpoint: cfg.otelEndpoint, + ServiceName: "k6build-server", + }) + if err != nil { + return fmt.Errorf("setting up tracing: %w", err) + } + defer shutdownTracer(cmd.Context()) //nolint:errcheck + if cfg.enableCgo { log.Warn("CGO is enabled by default. Use --enable-cgo=false to disable it.") } @@ -348,6 +359,12 @@ func New() *cobra.Command { //nolint:funlen time.Second, "time between retries for acquiring a lock", ) + cmd.Flags().StringVar( + &cfg.otelEndpoint, + "otel-endpoint", + "", + "OTLP gRPC endpoint for traces (e.g. localhost:4317). If empty, tracing is disabled.", + ) return cmd } diff --git a/cmd/store/store.go b/cmd/store/store.go index 9360932..ce83145 100644 --- a/cmd/store/store.go +++ b/cmd/store/store.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/k6build/pkg/httpserver" "github.com/grafana/k6build/pkg/store/file" "github.com/grafana/k6build/pkg/store/server" + "github.com/grafana/k6build/pkg/tracing" "github.com/grafana/k6build/pkg/util" "github.com/spf13/cobra" @@ -48,13 +49,14 @@ curl http://external.url:9000/store/objectID/download ) // New creates new cobra command for store command. -func New() *cobra.Command { +func New() *cobra.Command { //nolint:funlen var ( storeDir string storeSrvURL string port int logLevel string shutdownTimeout time.Duration + otelEndpoint string ) cmd := &cobra.Command{ @@ -67,6 +69,15 @@ func New() *cobra.Command { // this is needed to prevent cobra to print errors reported by subcommands in the stderr SilenceErrors: true, RunE: func(cmd *cobra.Command, _ []string) error { + shutdownTracer, err := tracing.Setup(cmd.Context(), tracing.Config{ + Endpoint: otelEndpoint, + ServiceName: "k6build-store", + }) + if err != nil { + return fmt.Errorf("setting up tracing: %w", err) + } + defer shutdownTracer(cmd.Context()) //nolint:errcheck + // set log ll, err := util.ParseLogLevel(logLevel) if err != nil { @@ -130,6 +141,12 @@ func New() *cobra.Command { 10*time.Second, "maximum time to wait for graceful shutdown", ) + cmd.Flags().StringVar( + &otelEndpoint, + "otel-endpoint", + "", + "OTLP gRPC endpoint for traces (e.g. localhost:4317). If empty, tracing is disabled.", + ) return cmd } diff --git a/go.mod b/go.mod index 9b1f224..a7de0ce 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,12 @@ require ( github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0 - golang.org/x/sync v0.18.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 + go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 + go.opentelemetry.io/otel/sdk v1.42.0 + go.opentelemetry.io/otel/trace v1.42.0 + golang.org/x/sync v0.19.0 ) require ( @@ -45,6 +50,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.41.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -61,7 +67,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect @@ -92,16 +98,18 @@ require ( github.com/tklauser/numcpus v0.7.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/sdk v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/sys v0.40.0 // indirect - google.golang.org/grpc v1.75.1 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.2 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 76d031a..bf8307c 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -88,6 +90,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +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.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -97,8 +101,8 @@ github.com/grafana/k6foundry v0.4.7 h1:1YXkTBwO/2dSx0pqJrraJATsFlsIX0vEpaEjV7E35 github.com/grafana/k6foundry v0.4.7/go.mod h1:eLsr0whhH+5Y1y7YpDxJi3Jv5wHMuf+80vdRyMH10pg= github.com/grafana/s3-mock v0.2.0 h1:bkoCq76q7w2yTO7+ISYoTb5nXvsZ9WaaXuWi9X0EjQo= github.com/grafana/s3-mock v0.2.0/go.mod h1:n47RcD22Xl5qkxJa8O2iJ/QuuZ1I2Pl3x6q8pB4sUtg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -183,53 +187,59 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= -golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU= -google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +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-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index 9dd2838..40a1bab 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -22,6 +22,10 @@ import ( "github.com/grafana/k6foundry" "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) const ( @@ -40,6 +44,8 @@ var ( ErrInitializingBuilder = errors.New("initializing builder") constrainRe = regexp.MustCompile(opRe + verRe + buildRe) + + tracer = otel.Tracer("github.com/grafana/k6build/pkg/builder") //nolint:gochecknoglobals ) // GoOpts defines the options for the go build environment @@ -143,6 +149,19 @@ func (b *Builder) Build( //nolint:funlen k6Constrains string, deps []k6build.Dependency, ) (artifact k6build.Artifact, buildErr error) { + ctx, span := tracer.Start(ctx, "Builder.Build", trace.WithAttributes( + attribute.String("k6build.platform", platform), + attribute.String("k6build.k6_constrains", k6Constrains), + attribute.Int("k6build.deps_count", len(deps)), + )) + defer func() { + if buildErr != nil { + span.RecordError(buildErr) + span.SetStatus(codes.Error, buildErr.Error()) + } + span.End() + }() + b.metrics.requestCounter.Inc() requestTimer := prometheus.NewTimer(b.metrics.requestTimeHistogram) @@ -167,6 +186,7 @@ func (b *Builder) Build( //nolint:funlen } id := generateArtifactID(platform, resolved) + span.SetAttributes(attribute.String("k6build.artifact_id", id)) // Try to get the object, if not found, try to acquire a build lock. // If the build lock is not acquired, assume some other builder is building the binary. @@ -176,6 +196,7 @@ func (b *Builder) Build( //nolint:funlen artifactObject, err = b.store.Get(ctx, id) if err == nil { b.metrics.storeHitsCounter.Inc() + span.SetAttributes(attribute.Bool("k6build.cache_hit", true)) return k6build.Artifact{ ID: id, @@ -249,6 +270,9 @@ func (b *Builder) resolveDependencies( k6Constrains string, deps []k6build.Dependency, ) (map[string]catalog.Module, error) { + ctx, span := tracer.Start(ctx, "Builder.resolveDependencies") + defer span.End() + ctlg, err := catalog.NewCatalog(ctx, b.catalog) if err != nil { return nil, k6build.NewWrappedError(k6build.ErrCatalog, err) @@ -351,6 +375,11 @@ func (b *Builder) buildArtifact( deps map[string]catalog.Module, artifactBuffer io.Writer, ) error { + ctx, span := tracer.Start(ctx, "Builder.buildArtifact", trace.WithAttributes( + attribute.String("k6build.platform", platform), + )) + defer span.End() + // already checked the platform is valid, should be safe to ignore the error buildPlatform, _ := k6foundry.ParsePlatform(platform) diff --git a/pkg/client/client.go b/pkg/client/client.go index 65c5d04..69b3ccf 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -15,6 +15,8 @@ import ( "github.com/grafana/k6build" "github.com/grafana/k6build/pkg/api" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) // ErrInvalidConfiguration signals an error in the configuration @@ -65,7 +67,9 @@ func NewBuildServiceClient(config BuildServiceClientConfig) (k6build.BuildServic client := config.HTTPClient if client == nil { - client = http.DefaultClient + client = &http.Client{ + Transport: otelhttp.NewTransport(http.DefaultTransport), + } } return &BuildClient{ srvURL: srvURL, diff --git a/pkg/httpserver/httpserver.go b/pkg/httpserver/httpserver.go index 3bf416c..f107afd 100644 --- a/pkg/httpserver/httpserver.go +++ b/pkg/httpserver/httpserver.go @@ -14,6 +14,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) const ( @@ -100,7 +101,7 @@ func (s *Server) Start(ctx context.Context) error { srv := http.Server{ Addr: fmt.Sprintf(":%d", s.port), - Handler: s.srv, + Handler: otelhttp.NewHandler(s.srv, "k6build"), ReadHeaderTimeout: s.readHeaderTimeout, } diff --git a/pkg/store/client/client.go b/pkg/store/client/client.go index 3b39c79..9a072a5 100644 --- a/pkg/store/client/client.go +++ b/pkg/store/client/client.go @@ -13,6 +13,8 @@ import ( "github.com/grafana/k6build" "github.com/grafana/k6build/pkg/store" "github.com/grafana/k6build/pkg/store/api" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) // ErrInvalidConfig signals an error with the client configuration @@ -39,7 +41,9 @@ func NewStoreClient(config StoreClientConfig) (*StoreClient, error) { client := config.HTTPClient if client == nil { - client = http.DefaultClient + client = &http.Client{ + Transport: otelhttp.NewTransport(http.DefaultTransport), + } } return &StoreClient{ server: srvURL, diff --git a/pkg/store/file/file.go b/pkg/store/file/file.go index fbaf1b1..3e1fd41 100644 --- a/pkg/store/file/file.go +++ b/pkg/store/file/file.go @@ -52,10 +52,7 @@ func (f *Store) Put(_ context.Context, id string, content io.Reader) (store.Obje objectDir := filepath.Join(f.dir, id) - if _, err := os.Stat(objectDir); !errors.Is(err, os.ErrNotExist) { //nolint:forbidigo - return store.Object{}, fmt.Errorf("%w: %q", store.ErrDuplicateObject, id) - } - + // create the directory first so the lock file can be placed inside it // TODO: check permissions err := os.MkdirAll(objectDir, 0o750) //nolint:forbidigo if err != nil { @@ -69,6 +66,11 @@ func (f *Store) Put(_ context.Context, id string, content io.Reader) (store.Obje } defer unlock() + // check for duplicate after acquiring the lock to avoid TOCTOU races + if _, err := os.Stat(filepath.Join(objectDir, "checksum")); !errors.Is(err, os.ErrNotExist) { //nolint:forbidigo + return store.Object{}, fmt.Errorf("%w: %q", store.ErrDuplicateObject, id) + } + objectFile, err := os.Create(filepath.Join(objectDir, "data")) //nolint:gosec,forbidigo if err != nil { return store.Object{}, k6build.NewWrappedError(store.ErrCreatingObject, err) @@ -122,6 +124,10 @@ func (f *Store) Get(_ context.Context, id string) (store.Object, error) { checksum, err := os.ReadFile(filepath.Join(objectDir, "checksum")) //nolint:gosec,forbidigo if err != nil { + // directory exists but checksum not yet written — treat as not found + if errors.Is(err, os.ErrNotExist) { //nolint:forbidigo + return store.Object{}, fmt.Errorf("%w (%s)", store.ErrObjectNotFound, id) + } return store.Object{}, k6build.NewWrappedError(store.ErrAccessingObject, err) } diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go new file mode 100644 index 0000000..85de100 --- /dev/null +++ b/pkg/tracing/tracing.go @@ -0,0 +1,70 @@ +// Package tracing provides OpenTelemetry tracing setup for k6build services. +package tracing + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" +) + +// Config holds tracing configuration. +type Config struct { + // Endpoint is the OTLP gRPC endpoint (e.g. "localhost:4317"). + // If empty, tracing is disabled. + Endpoint string + // ServiceName identifies this service in traces. + ServiceName string +} + +// Setup initializes the OpenTelemetry tracer provider with an OTLP/gRPC exporter. +// Returns a shutdown function that must be called on application exit. +// If Config.Endpoint is empty, tracing is disabled and a no-op provider is used. +func Setup(ctx context.Context, cfg Config) (shutdown func(context.Context) error, err error) { + if cfg.Endpoint == "" { + otel.SetTracerProvider(noop.NewTracerProvider()) + return func(context.Context) error { return nil }, nil + } + + exporter, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(cfg.Endpoint), + otlptracegrpc.WithInsecure(), + ) + if err != nil { + return nil, fmt.Errorf("creating OTLP trace exporter: %w", err) + } + + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName(cfg.ServiceName), + ), + ) + if err != nil { + return nil, fmt.Errorf("creating trace resource: %w", err) + } + + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exporter), + sdktrace.WithResource(res), + ) + + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + return tp.Shutdown, nil +} + +// Tracer returns a named tracer from the global provider. +func Tracer(name string) trace.Tracer { + return otel.GetTracerProvider().Tracer(name) +}