Logging & Debugging Tutorial#

This tutorial covers controlling scafctl’s log output — verbosity, format, destination, and environment variable overrides.

Overview#

By default, scafctl produces no structured log output. Only styled user messages (errors, warnings, success indicators) are shown. This keeps the CLI clean for human users and pipe-friendly for scripts.

When you need to see what’s happening under the hood — debugging a solution, reporting a bug, or feeding structured logs to a log aggregation system — scafctl gives you full control over log verbosity, format, and destination.

FlagDescription
(default)No logs, just styled output
--debugConsole-format debug logs
--log-levelNamed or numeric verbosity
--log-formatconsole (default) or json
--log-fileWrite logs to a file
Env varsOverride from CI/CD or scripts

Quick Start#

Default Behavior#

Run any command — you’ll see only styled user-facing messages:

scafctl run solution -f solution.yaml
# Output: styled results, errors show as ❌ messages

scafctl run solution -f invalid.yaml
# Output: ❌ failed to load solution from 'invalid.yaml': ...
# No JSON log noise on stderr
scafctl run solution -f solution.yaml
# Output: styled results, errors show as ❌ messages

scafctl run solution -f invalid.yaml
# Output: ❌ failed to load solution from 'invalid.yaml': ...
# No JSON log noise on stderr

Enable Debug Logging#

The quickest way to see what scafctl is doing:

scafctl run solution -f solution.yaml --debug

# or equivalently:
scafctl run solution -f solution.yaml --log-level debug
scafctl run solution -f solution.yaml --debug

# or equivalently:
scafctl run solution -f solution.yaml --log-level debug

This shows colored, human-readable logs on stderr alongside normal output:

2026-01-15T10:30:00.000-0500    DEBUG   run/solution.go:326     running solution {"file": "solution.yaml", ...}
2026-01-15T10:30:00.001-0500    DEBUG   get/get.go:347  Reading solution from local filesystem  {"path": "solution.yaml"}

Log Levels#

scafctl supports both named levels (recommended) and numeric V-levels for fine-grained control.

Named Levels#

LevelDescriptionUse Case
noneSuppress all structured log outputDefault; normal CLI usage
errorErrors onlyProduction monitoring
warnWarnings and errorsCatch potential issues
infoInformational messagesSee what’s happening
debugVerbose debuggingTroubleshooting solutions
traceVery verboseDeep debugging
# Show only errors
scafctl run solution -f solution.yaml --log-level error

# Show info and above
scafctl run solution -f solution.yaml --log-level info

# Maximum verbosity
scafctl run solution -f solution.yaml --log-level trace
# Show only errors
scafctl run solution -f solution.yaml --log-level error

# Show info and above
scafctl run solution -f solution.yaml --log-level info

# Maximum verbosity
scafctl run solution -f solution.yaml --log-level trace

Numeric V-Levels#

For fine-grained control matching the internal V() levels used in the code:

V-LevelEquivalent Named LevelWhat It Shows
1debugProvider execution, resolver lifecycle
2traceInternal data flow, template rendering
3(no alias)Ultra-verbose internals
# Same as --log-level debug
scafctl run solution -f solution.yaml --log-level 1

# Same as --log-level trace
scafctl run solution -f solution.yaml --log-level 2

# Ultra-verbose (no named alias)
scafctl run solution -f solution.yaml --log-level 3
# Same as --log-level debug
scafctl run solution -f solution.yaml --log-level 1

# Same as --log-level trace
scafctl run solution -f solution.yaml --log-level 2

# Ultra-verbose (no named alias)
scafctl run solution -f solution.yaml --log-level 3

Log Formats#

Console Format (Default)#

Human-readable, colored output. Best for terminal use:

scafctl run solution -f solution.yaml --debug
# or explicitly:
scafctl run solution -f solution.yaml --log-level debug --log-format console
scafctl run solution -f solution.yaml --debug
# or explicitly:
scafctl run solution -f solution.yaml --log-level debug --log-format console

Output:

2026-01-15T10:30:00.000-0500    DEBUG   run/solution.go:326     running solution {"file": "solution.yaml", ...}
2026-01-15T10:30:00.001-0500    ERROR   get/get.go:360  Failed to unmarshal     {"error": "..."}

JSON Format#

Structured JSON, ideal for log aggregation (Splunk, Datadog, ELK), piping to jq, or machine parsing:

scafctl run solution -f solution.yaml --log-level info --log-format json
scafctl run solution -f solution.yaml --log-level info --log-format json

Output:

{"level":"info","timestamp":"2026-01-15T10:30:00.000-0500","caller":"run/solution.go:326","message":"running solution","file":"solution.yaml"}
{"level":"error","timestamp":"2026-01-15T10:30:00.001-0500","caller":"get/get.go:360","message":"Failed to unmarshal","error":"..."}

Filter with jq:

scafctl run solution -f solution.yaml --log-level debug --log-format json 2>&1 | jq 'select(.level == "error")'

Note: The above command uses jq , a command-line JSON processor. Install it separately if not already available.

scafctl run solution -f solution.yaml --log-level debug --log-format json 2>&1 |
  ForEach-Object { $_ | ConvertFrom-Json } |
  Where-Object { $_.level -eq 'error' }

Log File Output#

Write logs to a file instead of (or in addition to) stderr:

# Logs go to file only; stderr shows just styled output
scafctl run solution -f solution.yaml --log-level debug --log-file /tmp/scafctl.log

# Logs go to BOTH file and stderr (combine with --debug)
scafctl run solution -f solution.yaml --debug --log-file /tmp/scafctl.log
# Logs go to file only; stderr shows just styled output
scafctl run solution -f solution.yaml --log-level debug --log-file /tmp/scafctl.log

# Logs go to BOTH file and stderr (combine with --debug)
scafctl run solution -f solution.yaml --debug --log-file /tmp/scafctl.log

When --debug and --log-file are used together, logs are written to both destinations. Without --debug, the file receives the logs and stderr stays clean.

View the log file:

tail -f /tmp/scafctl.log
tail -f /tmp/scafctl.log

Environment Variables#

Environment variables are useful for CI/CD pipelines, container environments, and scripts where you can’t pass flags.

VariableDescriptionExample
SCAFCTL_DEBUGEnable debug logging (set to 1, true, or any non-empty non-0 value)SCAFCTL_DEBUG=1
SCAFCTL_LOG_LEVELSet log levelSCAFCTL_LOG_LEVEL=info
SCAFCTL_LOG_FORMATSet log formatSCAFCTL_LOG_FORMAT=json
SCAFCTL_LOG_PATHWrite logs to a fileSCAFCTL_LOG_PATH=/var/log/scafctl.log

Examples#

# CI/CD: structured JSON logs for log aggregation
export SCAFCTL_LOG_LEVEL=info
export SCAFCTL_LOG_FORMAT=json
scafctl run solution -f solution.yaml

# Container: debug to a file
export SCAFCTL_DEBUG=1
export SCAFCTL_LOG_PATH=/var/log/scafctl.log
scafctl run solution -f solution.yaml

# Quick debug in shell
SCAFCTL_DEBUG=1 scafctl run solution -f solution.yaml
# CI/CD: structured JSON logs for log aggregation
$env:SCAFCTL_LOG_LEVEL = "info"
$env:SCAFCTL_LOG_FORMAT = "json"
scafctl run solution -f solution.yaml

# Container: debug to a file
$env:SCAFCTL_DEBUG = "1"
$env:SCAFCTL_LOG_PATH = "/var/log/scafctl.log"
scafctl run solution -f solution.yaml

# Quick debug in shell
SCAFCTL_DEBUG=1 scafctl run solution -f solution.yaml

Precedence#

When multiple sources set the log level, scafctl applies them in this order (highest priority first):

1. --log-level flag          (explicit flag always wins)
2. --debug / -d flag         (shorthand for --log-level debug)
3. SCAFCTL_LOG_LEVEL env     (environment variable)
4. SCAFCTL_DEBUG env         (debug shortcut)
5. config file logging.level (persistent preference)
6. default: "none"           (no logs)

The same pattern applies to format and file path:

  • --log-format flag > SCAFCTL_LOG_FORMAT env > config logging.format > default "console"
  • --log-file flag > SCAFCTL_LOG_PATH env > default (no file)

Configuration File#

You can set logging defaults in your config file so you don’t need flags every time:

# ~/.config/scafctl/config.yaml (Linux)
# ~/.config/scafctl/config.yaml (macOS)
logging:
  level: "info"       # Always show info-level logs
  format: "console"   # Human-readable (default)
  timestamps: true    # Include timestamps (default)

Manage via CLI:

# Set persistent log level
scafctl config set logging.level info

# Set persistent format
scafctl config set logging.format json

# Reset to defaults
scafctl config unset logging.level
scafctl config unset logging.format
# Set persistent log level
scafctl config set logging.level info

# Set persistent format
scafctl config set logging.format json

# Reset to defaults
scafctl config unset logging.level
scafctl config unset logging.format

Common Workflows#

Debugging a Failing Solution#

# Step 1: Run with debug to see what's happening
scafctl run solution -f solution.yaml --debug

# Step 2: If you need even more detail
scafctl run solution -f solution.yaml --log-level trace

# Step 3: Capture logs for a bug report
scafctl run solution -f solution.yaml --log-level trace --log-format json --log-file debug.log 2>&1
# Step 1: Run with debug to see what's happening
scafctl run solution -f solution.yaml --debug

# Step 2: If you need even more detail
scafctl run solution -f solution.yaml --log-level trace

# Step 3: Capture logs for a bug report
scafctl run solution -f solution.yaml --log-level trace --log-format json --log-file debug.log 2>&1

CI/CD Pipeline#

# Fail-fast with only error logs in JSON for log aggregation
scafctl run solution -f solution.yaml --log-level error --log-format json
# Fail-fast with only error logs in JSON for log aggregation
scafctl run solution -f solution.yaml --log-level error --log-format json

Or set via environment:

# GitHub Actions example
env:
  SCAFCTL_LOG_LEVEL: error
  SCAFCTL_LOG_FORMAT: json
steps:
  - run: scafctl run solution -f solution.yaml

Separating Logs from Output#

Use --log-file to keep stderr clean while still capturing logs:

# Logs to file, styled output to stderr, data to stdout
scafctl run solution -f solution.yaml --log-level debug --log-file /tmp/run.log -o json > results.json

# Review logs separately
cat /tmp/run.log
# Logs to file, styled output to stderr, data to stdout
scafctl run solution -f solution.yaml --log-level debug --log-file /tmp/run.log -o json > results.json

# Review logs separately
cat /tmp/run.log

Temporary Debug Session#

Set and unset config for a debugging session:

# Enable debug temporarily via config
scafctl config set logging.level debug

# Run your commands...
scafctl run solution -f solution.yaml

# Reset when done
scafctl config unset logging.level
# Enable debug temporarily via config
scafctl config set logging.level debug

# Run your commands...
scafctl run solution -f solution.yaml

# Reset when done
scafctl config unset logging.level

Or use the environment variable (no config changes needed):

SCAFCTL_DEBUG=1 scafctl run solution -f solution.yaml
SCAFCTL_DEBUG=1 scafctl run solution -f solution.yaml

OpenTelemetry Integration#

scafctl uses an OpenTelemetry logging bridge so that structured log records are forwarded to an OTLP-compatible backend when --otel-endpoint is set. When an active trace span is in progress, the OTel bridge automatically attaches the current trace ID and span ID to every log record. This appears as extra fields in JSON-format log output:

{"time":"2026-02-24T10:00:00Z","level":"DEBUG","msg":"executing resolver","resolver":"env-name","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7"}

What the IDs mean#

FieldDescription
trace_id128-bit W3C trace ID — same value across all spans in one CLI invocation
span_id64-bit span ID of the currently active operation (resolver, action, etc.)

These IDs let you correlate log records with spans in Jaeger or another trace backend:

  1. Note the trace_id from a log record.
  2. Search for that trace in Jaeger: http://localhost:16686/trace/<trace_id>.
  3. Every span in the waterfall appears with its own log records alongside.

Enabling trace context in logs#

Trace IDs only appear when both conditions are true:

  1. --otel-endpoint is set (or OTEL_EXPORTER_OTLP_ENDPOINT env var).
  2. A trace span is active at the time the log record is emitted.
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 \
  scafctl run solution -f solution.yaml --log-level debug --log-format json
OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 `
  scafctl run solution -f solution.yaml --log-level debug --log-format json

Local console-format logs (without --log-format json) do not include the trace/span IDs — they only appear in the OTLP log stream and in JSON format.

See the Telemetry Tutorial for a complete walkthrough.

Summary#

What You WantCommand
Clean output (default)scafctl run solution -f s.yaml
Quick debugscafctl ... --debug
Specific levelscafctl ... --log-level warn
JSON for toolingscafctl ... --log-level info --log-format json
Logs to filescafctl ... --debug --log-file debug.log
CI/CD env varsSCAFCTL_LOG_LEVEL=error SCAFCTL_LOG_FORMAT=json
Logs + traces to OTLPOTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317 scafctl ... --log-level debug

Next Steps#