Logging
Holocron has two parallel output channels:
print— user-facing UX output: formatted results, success lines, the text a user is meant to read.logger— operational output: internal state, debug traces, errors, and the structured context that routes to Axiom.
They are not a hierarchy. logger does not wrap or replace print. This page covers
logger, implemented by the @theholocron/logger
package.
The Logger interface
Section titled “The Logger interface”Every command and plugin imports Logger — never Pino directly. Pino is an
implementation detail hidden behind the interface, so the logging library can be
swapped without touching a call site.
interface Logger { debug(obj: Record<string, unknown>, msg?: string): void; debug(msg: string): void; info(obj: Record<string, unknown>, msg?: string): void; info(msg: string): void; warn(obj: Record<string, unknown>, msg?: string): void; warn(msg: string): void; error(obj: Record<string, unknown>, msg?: string): void; error(msg: string): void; child(bindings: Record<string, unknown>): Logger;}Creating a logger
Section titled “Creating a logger”import { createLogger } from "@theholocron/logger";
const { logger, runId } = createLogger({ level: "info" });const log = logger.child({ command: "sync-github", repo: "theholocron/configs" });
log.info({ branch: "main" }, "opening PR");createLogger returns { logger, runId }. The runId is a UUID bound to every line
the logger and its children emit — one query pulls a whole run back out of Axiom.
It is printed via print when --debug or --verbose is passed.
Child loggers inherit every binding from their parent and add their own, so passing
logger.child({ module }) into a module means every line from that module carries
command, repo, runId, env, and module with no extra wiring.
Standard context fields
Section titled “Standard context fields”| Field | Set by | Value |
|---|---|---|
runId |
root logger (startup) | UUID, unique per command invocation |
env |
root logger (startup) | "ci" when CI is detected, else "local" |
command |
command child logger | active holocron command name |
module |
module child logger | e.g. "sync-github", "setup-workflows" |
repo |
command child logger | "owner/name" when available |
level |
Pino | level name string |
time |
Pino | ISO 8601 timestamp |
What the commands emit
Section titled “What the commands emit”setup, doctor, sync, secrets sync, deploy, and cleanup-preview
emit a structured line per unit of work — a <command>: start, one line
per step / row / file (with capability, step, status, detail), and
a <command>: done carrying the ok / fail / skip counts. Pull a
whole run back out of Axiom by its runId:
['holocron-ci'] | where runId == "<uuid>" | sort by _time ascLog level
Section titled “Log level”Resolved in priority order:
--verbose→debug,--quiet→error(resolved by the command layer)HOLOCRON_LOG_LEVELenv var (debug|info|warn|error)log.levelinholocron.config"info"(default)
The resolved level applies to every transport — there is no separate Axiom gate.
--debug is distinct from --verbose: it does not change the level. It prints
one line — Run ID: <uuid> — at the end of the command so you have a reference for
querying Axiom, without the full debug trace. (--verbose prints it too.)
Transports
Section titled “Transports”| Environment | Output |
|---|---|
| Local, on a TTY | pino-pretty — colourised, human-readable |
CI (CI truthy) |
newline-delimited JSON to stdout |
| Axiom configured | Axiom, in a non-blocking Pino worker thread |
The Axiom transport is added when Axiom credentials are present and
HOLOCRON_TELEMETRY is not "false".
| Env var | Fallback | Purpose |
|---|---|---|
HOLOCRON_AXIOM_TOKEN |
AXIOM_TOKEN |
Axiom API token |
HOLOCRON_AXIOM_DATASET |
AXIOM_DATASET |
target dataset name |
Two datasets, selected by the single HOLOCRON_AXIOM_DATASET value:
| Value | When |
|---|---|
holocron-ci |
CI |
holocron-local |
local — opt-in |
The transport activates only when both a token and a dataset resolve. With no dataset, runs write only to their console transport and never attempt an Axiom connection.
The token resolves from HOLOCRON_AXIOM_TOKEN → AXIOM_TOKEN → the OS
keyring (axiom.<org>, then bare axiom). The dataset resolves from
HOLOCRON_AXIOM_DATASET → AXIOM_DATASET → holocron.config
log.axiom.dataset. Env vars always win.
The sync and sync-github reusable workflows read the token from the
HOLOCRON_AXIOM_TOKEN secret and the dataset from the
HOLOCRON_AXIOM_DATASET variable — set both at the org level once
(HOLOCRON_AXIOM_DATASET = holocron-ci). Thin callers forward them via
secrets: inherit; the variable is visible to the reusable workflow
automatically. HOLOCRON_TELEMETRY=false in a workflow disables shipping
without removing the secret.
Store the token in the OS keyring once:
holocron auth set axiom.theholocron <TOKEN>That token backs both holocron setup / holocron doctor (the logs
capability, which provisions and checks the datasets) and the logger’s
Axiom transport. Then opt local runs into shipping by naming the dataset in
holocron.config — log: { axiom: { dataset: "holocron-local" } }.
Leave log.axiom.dataset unset (and HOLOCRON_AXIOM_DATASET unexported)
to keep local runs on pino-pretty only.
Opting out
Section titled “Opting out”HOLOCRON_TELEMETRY=false disables the Axiom transport (the logger keeps working
— it just writes locally). The same switch also turns off Sentry and PostHog —
see Telemetry for all three sinks.
For a committed, repo-wide choice rather than an environment variable, the
telemetry block in holocron.config ({ enabled: false }, or
{ analytics: "none" } to keep error reporting) is the version-controlled peer
of HOLOCRON_TELEMETRY=false. The env var still wins — see
Telemetry → Opting out.
Redaction
Section titled “Redaction”Sensitive field paths are stripped in Pino’s serialisation layer, before any
transport sees a line. Redacted values become [Redacted].
Redacted: token, secret, password, apiKey, secrets[*].value,
headers.authorization, headers["x-api-key"], and the same keys one level down
(*.token, *.secret, …). The full list is exported as REDACTED_PATHS.
holocron.config
Section titled “holocron.config”import { defineConfig } from "@theholocron/holocron-config";
export default defineConfig({ log: { level: "info", // "debug" | "info" | "warn" | "error" axiom: { dataset: "holocron-local" }, // opt local runs into Axiom shipping },});Only the level and the non-secret Axiom dataset name belong in config. The Axiom token never does — it comes from an env var or the OS keyring.