Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"ghcr.io/devcontainers/features/go:1": {
"version": "1.24"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"moby": true,
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/dhoeric/features/google-cloud-cli:1": {},
"ghcr.io/devcontainers/features/aws-cli:1": {}
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,45 @@ variables have an effect on leeway:
- `LEEWAY_YARN_MUTEX`: Configures the mutex flag leeway will pass to yarn. Defaults to "network". See https://yarnpkg.com/lang/en/docs/cli/#toc-concurrency-and-mutex for possible values.
- `LEEWAY_EXPERIMENTAL`: Enables exprimental features

# OpenTelemetry Tracing

Leeway supports distributed tracing using OpenTelemetry for build performance visibility.

## Quick Start

```bash
# Local development (Jaeger)
export OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4318
export OTEL_EXPORTER_OTLP_INSECURE=true
leeway build :my-package

# Production (Honeycomb with API key)
export OTEL_EXPORTER_OTLP_ENDPOINT=api.honeycomb.io:443
export OTEL_EXPORTER_OTLP_HEADERS="x-honeycomb-team=YOUR_API_KEY"
leeway build :my-package
```

The OpenTelemetry SDK automatically reads standard `OTEL_EXPORTER_OTLP_*` environment variables.

## Span Hierarchy

Leeway creates a nested span hierarchy for detailed build timeline visualization:

```
leeway.build (root)
├── leeway.package (component:package-1)
│ ├── leeway.phase (prep)
│ ├── leeway.phase (build)
│ └── leeway.phase (test)
└── leeway.package (component:package-2)
├── leeway.phase (prep)
└── leeway.phase (build)
```

Each phase span captures timing, status, and errors for individual build phases (prep, pull, lint, test, build, package).

**For detailed configuration, examples, and span attributes, see [docs/observability.md](docs/observability.md).**

# Provenance (SLSA) - EXPERIMENTAL
leeway can produce provenance information as part of a build. At the moment only [SLSA Provenance v0.2](https://slsa.dev/provenance/v0.2) is supported. This support is **experimental**.

Expand Down
75 changes: 72 additions & 3 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import (
"github.com/gitpod-io/leeway/pkg/leeway/cache"
"github.com/gitpod-io/leeway/pkg/leeway/cache/local"
"github.com/gitpod-io/leeway/pkg/leeway/cache/remote"
"github.com/gitpod-io/leeway/pkg/leeway/telemetry"
"github.com/gookit/color"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

// buildCmd represents the build command
Expand Down Expand Up @@ -47,7 +50,8 @@ Examples:
if pkg == nil {
log.Fatal("build needs a package")
}
opts, localCache := getBuildOpts(cmd)
opts, localCache, shutdown := getBuildOpts(cmd)
defer shutdown()

var (
watch, _ = cmd.Flags().GetBool("watch")
Expand Down Expand Up @@ -209,9 +213,13 @@ func addBuildFlags(cmd *cobra.Command) {
cmd.Flags().Bool("report-github", os.Getenv("GITHUB_OUTPUT") != "", "Report package build success/failure to GitHub Actions using the GITHUB_OUTPUT environment variable")
cmd.Flags().Bool("fixed-build-dir", true, "Use a fixed build directory for each package, instead of based on the package version, to better utilize caches based on absolute paths (defaults to true)")
cmd.Flags().Bool("docker-export-to-cache", false, "Export Docker images to cache instead of pushing directly (enables SLSA L3 compliance)")
cmd.Flags().String("otel-endpoint", os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"), "OpenTelemetry OTLP endpoint URL for tracing (defaults to $OTEL_EXPORTER_OTLP_ENDPOINT)")
cmd.Flags().Bool("otel-insecure", os.Getenv("OTEL_EXPORTER_OTLP_INSECURE") == "true", "Disable TLS for OTLP endpoint (for local development only, defaults to $OTEL_EXPORTER_OTLP_INSECURE)")
cmd.Flags().String("trace-parent", os.Getenv("TRACEPARENT"), "W3C Trace Context traceparent header for distributed tracing (defaults to $TRACEPARENT)")
cmd.Flags().String("trace-state", os.Getenv("TRACESTATE"), "W3C Trace Context tracestate header for distributed tracing (defaults to $TRACESTATE)")
}

func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache, func()) {
// Track if user explicitly set LEEWAY_DOCKER_EXPORT_TO_CACHE before workspace loading.
// This allows us to distinguish:
// - User set explicitly: High priority (overrides package config)
Expand Down Expand Up @@ -312,6 +320,61 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
reporter = append(reporter, leeway.NewGitHubReporter())
}

// Initialize OpenTelemetry reporter if endpoint is configured
var tracerProvider *sdktrace.TracerProvider
var otelShutdown func()
if otelEndpoint, err := cmd.Flags().GetString("otel-endpoint"); err != nil {
log.Fatal(err)
} else if otelEndpoint != "" {
// Set leeway version for telemetry
telemetry.SetLeewayVersion(leeway.Version)

// Get insecure flag
otelInsecure, err := cmd.Flags().GetBool("otel-insecure")
if err != nil {
log.Fatal(err)
}

// Initialize tracer with the provided endpoint and TLS configuration
tp, err := telemetry.InitTracer(context.Background(), otelEndpoint, otelInsecure)
if err != nil {
log.WithError(err).Warn("failed to initialize OpenTelemetry tracer")
} else {
tracerProvider = tp

// Parse trace context if provided
traceParent, _ := cmd.Flags().GetString("trace-parent")
traceState, _ := cmd.Flags().GetString("trace-state")

parentCtx := context.Background()
if traceParent != "" {
if err := telemetry.ValidateTraceParent(traceParent); err != nil {
log.WithError(err).Warn("invalid trace-parent format")
} else {
ctx, err := telemetry.ParseTraceContext(traceParent, traceState)
if err != nil {
log.WithError(err).Warn("failed to parse trace context")
} else {
parentCtx = ctx
}
}
}

// Create OTel reporter
tracer := otel.Tracer("leeway")
reporter = append(reporter, leeway.NewOTelReporter(tracer, parentCtx))

// Create shutdown function
otelShutdown = func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := telemetry.Shutdown(shutdownCtx, tracerProvider); err != nil {
log.WithError(err).Warn("failed to shutdown tracer provider")
}
}
}
}

dontTest, err := cmd.Flags().GetBool("dont-test")
if err != nil {
log.Fatal(err)
Expand Down Expand Up @@ -374,6 +437,11 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
dockerExportSet = true
}

// Create a no-op shutdown function if otelShutdown is nil
if otelShutdown == nil {
otelShutdown = func() {}
}

return []leeway.BuildOption{
leeway.WithLocalCache(localCache),
leeway.WithRemoteCache(remoteCache),
Expand All @@ -391,7 +459,7 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) {
leeway.WithInFlightChecksums(inFlightChecksums),
leeway.WithDockerExportToCache(dockerExportToCache, dockerExportSet),
leeway.WithDockerExportEnv(dockerExportEnvValue, dockerExportEnvSet),
}, localCache
}, localCache, otelShutdown
}

type pushOnlyRemoteCache struct {
Expand Down Expand Up @@ -506,6 +574,7 @@ func getRemoteCache(cmd *cobra.Command) cache.RemoteCache {
SLSA: slsaConfig,
}


switch remoteStorage {
case "GCP":
if slsaConfig != nil && slsaConfig.Verification {
Expand Down
2 changes: 1 addition & 1 deletion cmd/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func TestGetBuildOptsWithInFlightChecksums(t *testing.T) {
}

// Test getBuildOpts function
opts, localCache := getBuildOpts(cmd)
opts, localCache, _ := getBuildOpts(cmd)

// We can't directly test the WithInFlightChecksums option since it's internal,
// but we can verify the function doesn't error and returns options
Expand Down
2 changes: 1 addition & 1 deletion cmd/provenance-assert.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func getProvenanceTarget(cmd *cobra.Command, args []string) (bundleFN, pkgFN str
log.Fatal("provenance export requires a package")
}

_, cache := getBuildOpts(cmd)
_, cache, _ := getBuildOpts(cmd)

var ok bool
pkgFN, ok = cache.Location(pkg)
Expand Down
2 changes: 1 addition & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Should any of the scripts fail Leeway will exit with an exit code of 1 once all
if script == nil {
return errors.New("run needs a script")
}
opts, _ := getBuildOpts(cmd)
opts, _, _ := getBuildOpts(cmd)
return script.Run(opts...)
})
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/sbom-export.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ If no package is specified, the workspace's default target is used.`,
}

// Get build options and cache
_, localCache := getBuildOpts(cmd)
_, localCache, _ := getBuildOpts(cmd)

// Get output format and file
format, _ := cmd.Flags().GetString("format")
Expand Down
2 changes: 1 addition & 1 deletion cmd/sbom-scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ If no package is specified, the workspace's default target is used.`,
}

// Get cache
_, localCache := getBuildOpts(cmd)
_, localCache, _ := getBuildOpts(cmd)

// Get output directory
outputDir, _ := cmd.Flags().GetString("output-dir")
Expand Down
Loading