Skip to main content

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:

FormatFlag valueDescription
Human-readablehuman (default)Rich-formatted tables with colors and unicode
JSONjsonMachine-readable JSON to stdout
TabletableRich table (same as human for list commands)
CSVcsvComma-separated values with a header row
QuietquietMinimal output — IDs, paths, or single values only

Output Rules

  • --output json writes valid JSON to stdout with no surrounding text.
  • All non-data output (spinners, progress, status messages) goes to stderr when --output json is active.
  • JSON output follows the project response envelope: { "data": [...], "meta": { ... } }.
  • CSV includes a header row and follows RFC 4180 quoting rules.
tip

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

OperatorSyntaxExample
Equalskey=value--filter status=running
Not equalskey!=value--filter status!=failed
Containskey~=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).
  • --sort and --sort-desc are mutually exclusive.

Grouping

gl test run --group-by stack
  • In human output, each group renders as a separate Rich table or section.
  • In json output, data is nested by group key: { "data": { "python": [...], "go": [...] } }.

Limiting & Pagination

gl download list-catalog --limit 20 --offset 40
FlagDefaultDescription
--limit NNo limitReturn at most N items
--offset N0Skip 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, and csv modes.
  • Limits JSON keys in json mode.
  • Invalid field names produce a warning but do not fail the command.

Logging & Verbosity

Verbosity Levels

LevelFlagLog levelUser seesStructured log level
Quiet-qWARNINGErrors and warnings onlyWARNING
Normal(default)INFOResults, status, progressINFO
Verbose-vDEBUGNormal + detailed action stepsDEBUG
Debug-vvDEBUGEverything + stack traces, payloadsDEBUG

User Output vs Structured Logs

The CLI maintains a strict separation between two output streams:

StreamDestinationContentFormat
User outputstderrProgress spinners, status messages, headers, bannersRich-formatted
Data outputstdoutFinal structured result of the commandJSON, CSV, or human table
Structured logslog file (optionally stderr with -v)Timestamped events with contextNDJSON

This separation ensures that piping always works:

# JSON piping is clean regardless of verbosity
gl dev status -v --output json | jq .

Rolling Log File

SettingDefaultOverride
Location$XDG_STATE_HOME/gospelib/gl.log--log-file PATH
Fallback path~/.local/state/gospelib/gl.log
Rotation5 MB max per file
Retention3 rotated files
FormatNDJSON (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)
info

Consistent event names make it easy to search and aggregate logs — for example, grep "command_failed" gl.log instantly shows all CLI failures.