diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index 36c5fd6bb9c..d58100f0eed 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -16,16 +16,22 @@ package cli import ( + "context" "fmt" + "net/http" "os" cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd" "github.com/google/go-containerregistry/pkg/logs" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/templates" + itracing "github.com/sigstore/cosign/v3/internal/tracing" + "github.com/sigstore/model-validation-operator/pkg/tracing" "github.com/spf13/cobra" "github.com/spf13/pflag" cobracompletefig "github.com/withfig/autocomplete-tools/integrations/cobra" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/trace" "sigs.k8s.io/release-utils/version" ) @@ -33,6 +39,22 @@ var ( ro = &options.RootOptions{} ) +// tracingTransport injects the active trace context into outgoing HTTP requests +// that were created without context (http.NewRequest instead of http.NewRequestWithContext). +type tracingTransport struct { + base http.RoundTripper +} + +func (t *tracingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if itracing.ActiveCtx != nil { + // Inject the active span into the request context so otelhttp + // creates child spans under the root operation span. + // Preserve any deadline/values from the original request context. + req = req.WithContext(trace.ContextWithSpan(req.Context(), trace.SpanFromContext(itracing.ActiveCtx))) + } + return t.base.RoundTrip(req) +} + func normalizeCertificateFlags(_ *pflag.FlagSet, name string) pflag.NormalizedName { switch name { case "cert": @@ -53,7 +75,8 @@ func normalizeCertificateFlags(_ *pflag.FlagSet, name string) pflag.NormalizedNa func New() *cobra.Command { var ( - out, stdout *os.File + out, stdout *os.File + tracingShutdown func(context.Context) error ) cmd := &cobra.Command{ @@ -77,9 +100,38 @@ func New() *cobra.Command { logs.Debug.SetOutput(os.Stderr) } + if ro.TracingEnabled { + var tracingOpts []tracing.Option + tracingOpts = append(tracingOpts, tracing.WithServiceName("cosign")) + if ro.TracingEndpoint != "" { + tracingOpts = append(tracingOpts, tracing.WithEndpoint(ro.TracingEndpoint)) + } + tracingOpts = append(tracingOpts, tracing.WithInsecure(ro.TracingInsecure)) + if ro.TracingStdout { + tracingOpts = append(tracingOpts, tracing.WithStdoutExporter()) + } + + shutdown, err := tracing.SetupTracing(cmd.Context(), tracingOpts...) + if err != nil { + return fmt.Errorf("setting up tracing: %w", err) + } + tracingShutdown = shutdown + + // Wrap http.DefaultTransport so all outgoing HTTP requests + // (to Fulcio, Rekor, TSA, etc.) propagate trace context. + // The tracingTransport injects the active span context into + // requests created without context (e.g. inside sigstore-go). + http.DefaultTransport = &tracingTransport{ + base: otelhttp.NewTransport(http.DefaultTransport), + } + } + return nil }, - PersistentPostRun: func(_ *cobra.Command, _ []string) { + PersistentPostRun: func(cmd *cobra.Command, _ []string) { + if tracingShutdown != nil { + _ = tracingShutdown(cmd.Context()) + } if out != nil { _ = out.Close() } diff --git a/cmd/cosign/cli/options/root.go b/cmd/cosign/cli/options/root.go index 92bb01e6a92..4aceb55b3b4 100644 --- a/cmd/cosign/cli/options/root.go +++ b/cmd/cosign/cli/options/root.go @@ -33,6 +33,11 @@ type RootOptions struct { OutputFile string Verbose bool Timeout time.Duration + + TracingEnabled bool + TracingEndpoint string + TracingInsecure bool + TracingStdout bool } // DefaultTimeout specifies the default timeout for commands. @@ -51,6 +56,15 @@ func (o *RootOptions) AddFlags(cmd *cobra.Command) { cmd.PersistentFlags().DurationVarP(&o.Timeout, "timeout", "t", DefaultTimeout, "timeout for commands") + + cmd.PersistentFlags().BoolVar(&o.TracingEnabled, "tracing-enabled", false, + "enable OpenTelemetry tracing") + cmd.PersistentFlags().StringVar(&o.TracingEndpoint, "tracing-endpoint", "", + "OTLP gRPC collector endpoint (e.g. host:4317)") + cmd.PersistentFlags().BoolVar(&o.TracingInsecure, "tracing-insecure", true, + "use insecure gRPC connection for tracing") + cmd.PersistentFlags().BoolVar(&o.TracingStdout, "tracing-stdout", false, + "export traces to stdout instead of OTLP") } func BindViper(cmd *cobra.Command, args []string) { diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index d027ba3194a..e1075df6c19 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -45,7 +45,9 @@ import ( ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/walk" "github.com/sigstore/cosign/v3/pkg/types" + itracing "github.com/sigstore/cosign/v3/internal/tracing" sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" + "go.opentelemetry.io/otel" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" @@ -65,6 +67,10 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot // nolint func SignCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string) error { + ctx, span := otel.Tracer("cosign").Start(ctx, "sign") + defer span.End() + itracing.ActiveCtx = ctx + if options.NOf(ko.KeyRef, ko.Sk) > 1 { return &options.KeyParseError{} } diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index 9c1527562a1..55c85731fb2 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -32,6 +32,7 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" + itracing "github.com/sigstore/cosign/v3/internal/tracing" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" @@ -43,6 +44,7 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" + "go.opentelemetry.io/otel" ) func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Hash) (internal.HashReader, func() error, error) { @@ -59,6 +61,10 @@ func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Has // nolint func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, payloadPath, certPath, certChainPath string, b64 bool, outputSignature string, outputCertificate string, tlogUpload bool) ([]byte, error) { + ctx, span := otel.Tracer("cosign").Start(ctx, "sign-blob") + defer span.End() + itracing.ActiveCtx = ctx + var payload internal.HashReader ctx, cancel := context.WithTimeout(ctx, ro.Timeout) diff --git a/go.mod b/go.mod index c83c6d86b71..e0818e8621a 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/open-policy-agent/opa v1.15.0 github.com/secure-systems-lab/go-securesystemslib v0.11.0 github.com/sigstore/fulcio v1.8.5 + github.com/sigstore/model-validation-operator/pkg/tracing v0.0.0-20260613122948-a36fd5feae2c github.com/sigstore/protobuf-specs v0.5.1 github.com/sigstore/rekor v1.5.2 github.com/sigstore/rekor-tiles/v2 v2.2.2-0.20260601073857-5d098a2b6443 @@ -53,6 +54,9 @@ require ( github.com/transparency-dev/merkle v0.0.2 github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 gitlab.com/gitlab-org/api/client-go v1.23.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 golang.org/x/crypto v0.52.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 @@ -271,10 +275,12 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect diff --git a/go.sum b/go.sum index 0f6f5b1dbab..14ee757fd17 100644 --- a/go.sum +++ b/go.sum @@ -632,6 +632,8 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/sigstore/fulcio v1.8.5 h1:HYTD1/L5wlBp8JxsWxUf8hmfaNBBF/x3r3p5l6tZwbA= github.com/sigstore/fulcio v1.8.5/go.mod h1:tSLYK3JsKvJpDW1BsIsVHZgHj+f8TjXARzqIUWSsSPQ= +github.com/sigstore/model-validation-operator/pkg/tracing v0.0.0-20260613122948-a36fd5feae2c h1:l3y3OnGVK5kd3xUOvHjxW6JwYpDXaOphSm0zDO2wTUU= +github.com/sigstore/model-validation-operator/pkg/tracing v0.0.0-20260613122948-a36fd5feae2c/go.mod h1:hhrzef9B/mFrTuu3QUohVWCE/YAlGh2y5dX+kDv0Ytg= github.com/sigstore/protobuf-specs v0.5.1 h1:/5OPaNuolRJmQfeZLayJGFXMpsRJEdgC6ah1/+7Px7U= github.com/sigstore/protobuf-specs v0.5.1/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= github.com/sigstore/rekor v1.5.2 h1:k6pX4o1zFAzAvDbXiVIp5IHj1b0wcDaxsbsbNpuRO8o= @@ -755,18 +757,26 @@ 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/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +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.step.sm/crypto v0.77.7 h1:6azC+pD678Vjju8yXnMDHCZJ+HzFaEmL3sCryiezTIA= go.step.sm/crypto v0.77.7/go.mod h1:OW/2sEHwTtDKq70PvSQ5B0JGy/CrLyDKOiVy3YvZMTQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/internal/tracing/context.go b/internal/tracing/context.go new file mode 100644 index 00000000000..dc5e1c91609 --- /dev/null +++ b/internal/tracing/context.go @@ -0,0 +1,7 @@ +package tracing + +import "context" + +// ActiveCtx holds the context with the active span so that HTTP requests +// created without context (e.g. inside sigstore-go) can inherit trace context. +var ActiveCtx context.Context