Documentation
¶
Overview ¶
Package cmdkit is the paved path for forge's non-server binary shapes: CLI subcommands, one-shot admin tools, and standalone long-running binaries that ship in the same image as the Connect server but are not themselves Connect services.
serverkit owns the server lifecycle (listener, readiness flip, worker supervision, graceful shutdown). Everything else — a `report` subcommand, a queue drainer, a backfill job, a reverse proxy — got nothing, so each one re-invented the same five things by hand and inconsistently: a timeout literal, a clutch of os.Getenv reads, a freshly-built slog.Logger pointed at the wrong stream, hand-rolled DB open/ping, and fmt.Println for output. cmdkit centralizes exactly those five.
Design constraints:
- Config-agnostic. The typed config struct (config.Config) is generated per-project in the *project's* module, so cmdkit cannot import it. Helpers take plain values (a DSN, a duration) or small option structs. A command loads config.Load(cmd) itself and passes the fields it needs — config stays the single typed source, cmdkit stays reusable across every project.
- Driver-agnostic. OpenDB takes the database/sql driver name; cmdkit never imports a driver, so a project picks pgx / sqlite / etc. and blank-imports it as it already does.
- stdout is for data, stderr is for diagnostics. Logger writes to stderr; PrintJSON writes machine-consumable output to stdout. A command's structured result can be piped to jq while its log lines stay out of the pipe.
Index ¶
- func ContextWithLogger(ctx context.Context, opts LoggerOptions) (context.Context, *slog.Logger)
- func ContextWithTimeout(parent context.Context, d, fallback time.Duration) (context.Context, context.CancelFunc)
- func FirstNonEmpty(vals ...string) string
- func Logger(opts LoggerOptions) *slog.Logger
- func LoggerFromContext(ctx context.Context) *slog.Logger
- func OpenDB(ctx context.Context, opts DBOptions) (*sql.DB, error)
- func ParseLevel(s string) slog.Level
- func PrintJSON(w io.Writer, v any) error
- func Resolve(cmd *cobra.Command, flagName, envVar, defaultVal string) string
- type DBOptions
- type LoggerOptions
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ContextWithLogger ¶
ContextWithLogger builds a Logger from opts and stores it on ctx via observe.WithLogger, returning both. Downstream code recovers it with observe.FromContext(ctx) (re-exported here as LoggerFromContext) — no extra parameter threaded through call signatures.
func ContextWithTimeout ¶
func ContextWithTimeout(parent context.Context, d, fallback time.Duration) (context.Context, context.CancelFunc)
ContextWithTimeout derives a timeout context from parent. When d is non-positive the fallback is used instead — so a command can pass a config-sourced duration that defaults to zero and still get a sane bound without an inline `if d == 0` at every call site. It replaces the scattered context.WithTimeout(cmd.Context(), 60*time.Second) literals that disagreed across commands.
parent may be nil; context.Background() is substituted.
func FirstNonEmpty ¶
FirstNonEmpty returns the first non-empty, non-whitespace string from vals, or "" if all are empty. Handy for "flag value, then env, then fallback" chains the caller has already read into locals:
dsn := cmdkit.FirstNonEmpty(flagDST, os.Getenv("KALSHI_TRADER_DATABASE_URL"))
func Logger ¶
func Logger(opts LoggerOptions) *slog.Logger
Logger builds a *slog.Logger for a CLI command or standalone binary.
It is the one-line replacement for the ad-hoc slog.New(slog.NewJSONHandler(os.Stderr, nil)) that every command was rebuilding — and it fixes the recurring bug of logging to os.Stdout, which corrupts piped data output. Pass the binary/subcommand name so records carry a "cmd" attribute.
To honour a project's config, source Level/Format from config.Load:
cfg, _ := config.Load(cmd)
log := cmdkit.Logger(cmdkit.LoggerOptions{
Name: "pnl-report",
Format: cfg.LogFormat,
Level: cmdkit.ParseLevel(cfg.LogLevel),
})
func LoggerFromContext ¶
LoggerFromContext returns the request/command-scoped logger stored on ctx, or slog.Default() if none. It is a thin re-export of observe.FromContext so a command package depends on cmdkit alone.
func OpenDB ¶
OpenDB opens a *sql.DB, applies pool tuning, and (unless SkipPing) verifies connectivity with a bounded ping. It is the single replacement for the per-command sql.Open + PingContext blocks that every report/backfill command was copying.
The returned *sql.DB is the caller's to close (defer db.Close()). On a ping failure the pool is closed before returning, so a failed OpenDB never leaks a half-open pool.
func ParseLevel ¶
ParseLevel converts a config log-level string ("debug", "info", "warn", "error", case-insensitive) to a slog.Level. Unrecognized or empty values map to slog.LevelInfo — a CLI tool should stay loud enough to be useful rather than silently dropping to a stricter level on a typo.
func PrintJSON ¶
PrintJSON writes v as indented JSON followed by a newline to w. It is the replacement for hand-rolled json.NewEncoder(os.Stdout) + SetIndent blocks and for fmt.Println-based output. Write structured results to os.Stdout so they pipe cleanly to jq; keep human-facing chatter on the Logger (stderr).
func Resolve ¶
Resolve returns the value of a string config field with the canonical forge precedence: an explicit CLI flag beats an environment variable, which beats the supplied default. It is the small, command-local twin of the generated config.Load precedence, for ad-hoc per-command inputs that don't live in the typed config proto (a --dst Postgres URL, a --since cutoff) — replacing the repeated
if flagVal == "" { flagVal = os.Getenv("X") }
if flagVal == "" { return errors.New("X required") }
dance. Pass envVar == "" to skip the env lookup; pass flagName == "" to skip the flag lookup.
Precedence rationale matches the generated loader: a flag is typed by an operator on this invocation; an env var is ambient process state a wrapper script may have set. The more deliberate, more local intent wins.
Types ¶
type DBOptions ¶
type DBOptions struct {
// Driver is the database/sql driver name (e.g. "pgx", "sqlite3").
// The caller blank-imports the driver as usual; cmdkit never imports
// one. Zero value defaults to "pgx" — the forge default.
Driver string
// DSN is the data source name / connection string. Required.
DSN string
// MaxOpenConns, when > 0, caps the open-connection pool. A one-shot
// CLI tool usually wants a small number (or the default).
MaxOpenConns int
// MaxIdleConns, when > 0, caps idle connections kept in the pool.
MaxIdleConns int
// ConnMaxIdleTime, when > 0, bounds idle-connection lifetime.
ConnMaxIdleTime time.Duration
// ConnMaxLifetime, when > 0, bounds total connection reuse.
ConnMaxLifetime time.Duration
// PingTimeout bounds the open-time connectivity check. Zero value
// defaults to 10s. The check fails fast on an unreachable DB rather
// than letting the first real query hang — the fail-fast-on-setup
// posture CLI tools want.
PingTimeout time.Duration
// SkipPing disables the open-time ping entirely. Use only when the
// caller deliberately wants lazy connection (rare for CLI tools).
SkipPing bool
}
DBOptions configures OpenDB. Only DSN is required; the rest tune the connection pool and the open-time ping.
type LoggerOptions ¶
type LoggerOptions struct {
// Name, when non-empty, is attached as a "cmd" attribute on every
// record so log lines from different subcommands are distinguishable
// in aggregate.
Name string
// Level is the minimum slog level. Zero value is slog.LevelInfo.
Level slog.Level
// Format selects the handler: "text" emits the text handler, any
// other value (including "") emits JSON. Mirrors serverkit.Config's
// LogFormat semantics so a server and its CLI tools log alike.
Format string
// Out is the destination. Zero value is os.Stderr — diagnostics
// belong on stderr so stdout stays clean for piped data.
Out io.Writer
}
LoggerOptions configures Logger. The zero value is a usable default: info level, JSON format, writing to stderr.