Output & Logging
This page covers how the gl CLI formats its output, supports machine-readable modes, and manages structured logging.
Output Formats
Every command that produces structured data supports the --output / -o flag:
| Format | Flag value | Description |
|---|---|---|
| Human-readable | human (default) | Rich-formatted tables with colors and unicode |
| JSON | json | Machine-readable JSON to stdout |
| Table | table | Rich table (same as human for list commands) |
| CSV | csv | Comma-separated values with a header row |
| Quiet | quiet | Minimal output — IDs, paths, or single values only |
Output Rules
--output jsonwrites valid JSON to stdout with no surrounding text.- All non-data output (spinners, progress, status messages) goes to stderr when
--output jsonis active. - JSON output follows the project response envelope:
{ "data": [...], "meta": { ... } }. - CSV includes a header row and follows RFC 4180 quoting rules.
The stdout/stderr separation means piping always works as expected:
gl dev status --output json | jq '.data[] | select(.status == "running")'
Spinners and progress bars print to stderr and never pollute the JSON stream.
Filtering
Commands that list multiple items support --filter:
gl dev status --filter status=running
gl test run --filter stack=python
gl health --filter type=service
Syntax: --filter KEY=VALUE. Multiple --filter flags combine with AND logic.
Filter Operators
| Operator | Syntax | Example |
|---|---|---|
| Equals | key=value | --filter status=running |
| Not equals | key!=value | --filter status!=failed |
| Contains | key~=value | --filter name~=content |
Sorting
gl dev status --sort name # Ascending by name
gl dev status --sort-desc uptime # Descending by uptime
- Each command defines its own default sort order (usually alphabetical by name).
--sortand--sort-descare mutually exclusive.
Grouping
gl test run --group-by stack
- In
humanoutput, each group renders as a separate Rich table or section. - In
jsonoutput, data is nested by group key:{ "data": { "python": [...], "go": [...] } }.
Limiting & Pagination
gl download list-catalog --limit 20 --offset 40
| Flag | Default | Description |
|---|---|---|
--limit N | No limit | Return at most N items |
--offset N | 0 | Skip the first N items |
In json output, the response includes meta.total and meta.offset for programmatic pagination.
Field Selection
gl dev status --fields name,port,status
- Limits visible columns in
human,table, andcsvmodes. - Limits JSON keys in
jsonmode. - Invalid field names produce a warning but do not fail the command.
Logging & Verbosity
Verbosity Levels
| Level | Flag | Log level | User sees | Structured log level |
|---|---|---|---|---|
| Quiet | -q | WARNING | Errors and warnings only | WARNING |
| Normal | (default) | INFO | Results, status, progress | INFO |
| Verbose | -v | DEBUG | Normal + detailed action steps | DEBUG |
| Debug | -vv | DEBUG | Everything + stack traces, payloads | DEBUG |
User Output vs Structured Logs
The CLI maintains a strict separation between two output streams:
| Stream | Destination | Content | Format |
|---|---|---|---|
| User output | stderr | Progress spinners, status messages, headers, banners | Rich-formatted |
| Data output | stdout | Final structured result of the command | JSON, CSV, or human table |
| Structured logs | log file (optionally stderr with -v) | Timestamped events with context | NDJSON |
This separation ensures that piping always works:
# JSON piping is clean regardless of verbosity
gl dev status -v --output json | jq .
Rolling Log File
| Setting | Default | Override |
|---|---|---|
| Location | $XDG_STATE_HOME/gospelib/gl.log | --log-file PATH |
| Fallback path | ~/.local/state/gospelib/gl.log | — |
| Rotation | 5 MB max per file | — |
| Retention | 3 rotated files | — |
| Format | NDJSON (one JSON object per line) | --log-format console |
structlog Configuration
All Python CLI tools must configure structlog identically:
import logging
import structlog
def configure_logging(
*,
verbosity: int = 0,
log_file: str | None = None,
log_format: str = "json",
) -> None:
level = logging.WARNING if verbosity < 0 else (
logging.DEBUG if verbosity >= 1 else logging.INFO
)
processors: list[structlog.types.Processor] = [
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.dev.set_exc_info,
structlog.processors.TimeStamper(fmt="iso"),
]
if log_format == "console" or verbosity >= 1:
processors.append(structlog.dev.ConsoleRenderer())
else:
processors.append(structlog.processors.JSONRenderer())
structlog.configure(
processors=processors,
wrapper_class=structlog.make_filtering_bound_logger(level),
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
cache_logger_on_first_use=True,
)
Log Event Naming
Log events use snake_case verb-noun format:
log.info("service_started", service="content", port=8100)
log.warning("health_check_degraded", service="redis", latency_ms=450)
log.error("command_failed", command="dev start", exit_code=1)
Consistent event names make it easy to search and aggregate logs — for example, grep "command_failed" gl.log instantly shows all CLI failures.