Telemetry Tutorial#

scafctl emits all three OpenTelemetry signals — logs, traces, and metrics. By default, telemetry is silent (noop providers). When you point scafctl at an OTLP endpoint every signal is exported for backend analysis.


Overview#

SignalDefault outputWith --otel-endpoint
Logsslog text/JSON → stderr (via --log-level)Also batched via OTLP gRPC
TracesDisabled (noop)Exported via OTLP gRPC
MetricsPrometheus /metrics (MCP server)Also pushed via OTLP gRPC

Configuration#

Flags (per-invocation)#

# Point at a local OTel Collector
scafctl run solution -f solution.yaml \
  --otel-endpoint localhost:4317 \
  --otel-insecure          # disable TLS (development only)

# Override with environment variable instead
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 \
  scafctl run solution -f solution.yaml
# Point at a local OTel Collector
scafctl run solution -f solution.yaml `
  --otel-endpoint localhost:4317 `
  --otel-insecure          # disable TLS (development only)

# Override with environment variable instead
$env:OTEL_EXPORTER_OTLP_ENDPOINT = 'localhost:4317'
scafctl run solution -f solution.yaml
FlagEnv overrideDefaultDescription
--otel-endpointOTEL_EXPORTER_OTLP_ENDPOINT(none)OTLP gRPC endpoint. When unset, tracing is disabled (noop).
--otel-insecure(none)falseSkip TLS verification. Use in local dev only.

Signals in Detail#

Logs#

Logs use the go-logr/logr interface throughout the codebase. The underlying sink is a multiSink that fans out to:

  1. slog handler → stderr (text or JSON via --log-format)
  2. otellogr bridge → OTel LoggerProvider → OTLP when --otel-endpoint is set

When no OTLP endpoint is configured, only the slog handler is active — no OTel log records are emitted to stderr. When an active span is in scope, the OTLP log record carries the trace_id and span_id automatically (correlation without any extra code).

Control verbosity:

scafctl run solution -f solution.yaml \
  --log-level debug \
  --log-format json \
  --otel-endpoint localhost:4317 \
  --otel-insecure
scafctl run solution -f solution.yaml `
  --log-level debug `
  --log-format json `
  --otel-endpoint localhost:4317 `
  --otel-insecure

See the Logging Tutorial for full flag reference.

Traces#

Spans are created at every major execution boundary:

SubsystemSpan nameKey attributes
HTTP clienthttp.client.request (otelhttp)http.method, http.url, http.status_code
Provider executorprovider.Executeprovider.name
Resolver executorresolver.Executeresolver.count
Resolver (single)resolver.executeResolverresolver.name, resolver.phase, resolver.sensitive
Solution loadersolution.Getsolution.path
Solution loader (bundle)solution.GetWithBundlesolution.path
Solution (local FS)solution.FromLocalFileSystemsolution.path
Solution (URL)solution.FromURLsolution.url
Action workflowaction.Executeaction.count
Action (single)action.executeActionaction.name
MCP tool callmcp.toolmcp.tool.name

All spans propagate W3C traceparent / tracestate headers on outbound HTTP requests via otelhttp.NewTransport, enabling distributed tracing when calling instrumented backends.

Local trace debugging#

Without --otel-endpoint, tracing is disabled (noop). To inspect traces locally, run a local collector such as otel-desktop-viewer or Jaeger and point scafctl at it:

# Start local Jaeger (see examples/telemetry/ for Docker Compose)
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 \
  scafctl run solution -f solution.yaml --otel-insecure
# Start local Jaeger (see examples/telemetry/ for Docker Compose)
$env:OTEL_EXPORTER_OTLP_ENDPOINT = 'localhost:4317'
scafctl run solution -f solution.yaml --otel-insecure

Metrics#

Metrics are exported via the Prometheus bridge exporter. The MCP server exposes a /metrics endpoint (Prometheus scrape format). When --otel-endpoint is set, the same metrics are also pushed via OTLP gRPC at the default push interval.

Key metrics:

MetricTypeLabels
scafctl_provider_execution_duration_secondsHistogramprovider_name, status
scafctl_provider_execution_totalCounterprovider_name, status
scafctl_http_client_duration_secondsHistogramstatus_code, url, method
scafctl_http_client_requests_totalCounterstatus_code, url, method
scafctl_resolver_execution_duration_secondsHistogramresolver_name, status
scafctl_resolver_executions_totalCounterresolver_name, status
scafctl_get_solution_time_histogramHistogrampath

Running Locally with Jaeger#

The examples/telemetry/ directory contains a ready-to-use Docker Compose stack with Jaeger (all-in-one) and an OTel Collector.

Prerequisites#

  • Docker and Docker Compose

Start the stack#

cd examples/telemetry
docker compose up -d

Services started:

ServiceURL
Jaeger UIhttp://localhost:16686
OTel Collector (OTLP gRPC)localhost:4317
OTel Collector (OTLP HTTP)localhost:4318
Prometheus metrics (collector)http://localhost:8888/metrics

Run a traced command#

OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 \
  scafctl run solution -f examples/actions/hello-world.yaml \
  --otel-insecure \
  --log-level debug
$env:OTEL_EXPORTER_OTLP_ENDPOINT = 'localhost:4317'
scafctl run solution -f examples/actions/hello-world.yaml `
  --otel-insecure `
  --log-level debug

View traces in Jaeger#

  1. Open http://localhost:16686
  2. Select service scafctl from the dropdown
  3. Click Find Traces
  4. Click any trace to see the waterfall of spans

Tear down#

cd examples/telemetry
docker compose down

Running with a Production Collector#

Point scafctl at your organisation’s OTLP endpoint (no --otel-insecure flag for TLS-enabled collectors):

scafctl run solution -f solution.yaml \
  --otel-endpoint otel-collector.example.com:4317 \
  --log-level info
scafctl run solution -f solution.yaml `
  --otel-endpoint otel-collector.example.com:4317 `
  --log-level info

Use the environment variable in CI/CD pipelines instead of flags:

# GitHub Actions
env:
  OTEL_EXPORTER_OTLP_ENDPOINT: otel-collector.example.com:4317
steps:
  - run: scafctl run solution -f solution.yaml --log-level info

Resource Attributes#

Every span, metric, and log record produced by scafctl includes:

AttributeValue
service.namescafctl
service.versionBuild version (e.g. 1.2.3)
commitGit commit SHA
build_timeBinary build timestamp
os.typeHost OS (e.g. darwin, linux)
host.nameHostname

Next Steps#