LDM Logging Strategy¶
This document defines the official logging architecture for Liferay Docker Manager. It exists to prevent inconsistent use of output levels across the codebase and to ensure the CLI feels appropriately terse to end users while remaining fully debuggable for developers and CI pipelines.
Logging Tier Architecture¶
LDM uses four console output tiers and one silent file tier:
| Tier | Method | Flag | Icon | Audience |
|---|---|---|---|---|
| Standard | UI.success() UI.warning() UI.error() UI.info() UI.heading() |
(always) | ✅ ⚠️ ❌ ℹ | All users |
| Info | UI.detail() |
--info / -i |
ℹ | Power users, troubleshooting |
| Verbose | UI.debug() |
--verbose / -v |
⚙️ | Developers, CI debug |
| Trace | UI.trace() |
(always, file only) | — | Log analysis (~/.ldm/last-command.log) |
ldm run <project> # Standard output only
ldm run <project> --info # Standard + Info tier
ldm run <project> --verbose # Standard + Info + Verbose
Rule: Every log call must be placed at the correct tier at the point of writing. Do not escalate to a higher tier for convenience — demote to the correct tier.
Method Reference¶
UI.info(msg) — Standard tier, always shown¶
Use for phase-level milestones that mark meaningful progress a user needs to track.
Use when:
- A named phase is starting or completing
- The user would be confused or alarmed if this line didn't appear
- This represents a top-level action the user explicitly requested
Examples:
UI.info("Downloading LDM package: liferay-ai-commerce-accelerator.ldmp...")
UI.info("Triggering orchestrated database restore (postgresql)...")
UI.info(f"Starting {project_id} stack ({tag}, {db_mode}, {host_name})...")
UI.info("Generating SSL certificates for aica.local...")
UI.info("Waiting for Elasticsearch to become ready...")
Do NOT use for:
- Sub-steps within a phase (use
detail()) - Docker container lifecycle ops (use
detail()) - Internal state reads/writes (use
debug()) - File-level operations unless directly user-visible (use
detail())
UI.detail(msg) — Info tier, shown with --info¶
Use for sub-step operations within a phase that a power user may want to follow.
Use when:
- This is a meaningful step inside a larger named phase
- A developer or advanced user would want visibility into this step for troubleshooting
- The step is expected and routine (not alarming)
Examples:
UI.detail("Scaffolding Docker environment for restore...")
UI.detail(f"Hydrating volume: {volume_name} from host")
UI.detail(f"Synchronising virtual host to {host_name}")
UI.detail(f"Reclaimed volume ownership across {n} directories")
UI.detail("Checking infrastructure stack (Traefik SSL Proxy)...")
UI.detail(f"Synced license from Common: {match.name}")
UI.detail("Scheduled automatic search reindex for next boot.")
Do NOT use for:
- Top-level phase milestones (use
info()) - Raw Docker command output (use
debug()) - API response payloads (use
debug())
UI.debug(msg) — Verbose tier, shown with --verbose¶
Use for developer-level internals that are needed to diagnose failures but produce noise for normal users.
Use when:
- The content is a raw command, API payload, or internal state value
- Only a developer or CI engineer debugging a failure would need this
- The output would be meaningless to an end user
Examples:
UI.debug(f"Running: {' '.join(cmd)}")
UI.debug(f"docker inspect output: {result}")
UI.debug(f"Detected ports {ports} from {lcp_file.name}")
UI.debug(f"Tag cache hit: {cache_key}")
UI.debug(f" Reclaiming: {path}") # per-directory detail within a batch
UI.debug(f"API response: {response.status_code} {response.text[:200]}")
UI.success(msg) — Standard tier, always shown¶
Use for completed milestones that confirm a meaningful outcome to the user.
Use when:
- A phase or major operation has completed successfully
- The user needs positive confirmation that something worked
Examples:
UI.success("LDM package checksum verified successfully.")
UI.success("Snapshot integrity verified.")
UI.success("Database restored successfully.")
UI.success("Restore complete.")
UI.success(f"Liferay is ready ({elapsed})")
Do NOT use for:
- Sub-step completions within a phase (use
detail()) - Trivial internal state confirmations (use
debug())
UI.warning(msg) — Standard tier, always shown¶
Use only for genuine user-actionable advisories where something unexpected occurred or the user may need to act.
Use when:
- Something unexpected happened but execution can continue
- The user may need to take action (e.g. check a setting, restart a service)
- A best-practice violation was detected
Legitimate warning() uses:
UI.warning(f"Port conflict detected! Using {new_port} instead of {orig_port}.")
UI.warning("Version downgrade detected. A pre-upgrade backup has been created.")
UI.warning(f"SSL certificate for {host} is not trusted by the system keychain.")
UI.warning(
f"Search restore timed out. Indices may be incomplete — run: ldm reindex {project_id}"
)
UI.warning(f"Stale project lock auto-recovered (PID {pid} no longer running).")
UI.warning("Using '.local' TLD — ensure mkcert is installed and trusted.")
Do NOT use for:
- Expected, routine operations (use
info()ordetail()) - Volume permission reclaims — these are expected Docker behaviour (use
detail()) - Container teardown during a reset — this is expected (use
detail())
The warning icon carries emotional weight. Every
warning()line tells the user "something may be wrong". Use it sparingly so genuine warnings are not ignored.
UI.error(msg) — Standard tier, stderr¶
Use when an operation failed but execution continues in a degraded state.
UI.error("Client extension deployment failed.", details=str(e), tip="Check: ldm logs")
UI.error("Elasticsearch failed to become ready in time.")
UI.die(msg, exit_code=1) — Standard tier, stderr, halts execution¶
Use when a failure is unrecoverable and execution must stop.
UI.die(
"Database restore failed after all retries. Original data preserved.", exit_code=3
)
UI.die("Elasticsearch failed after 2 restart attempts.", exit_code=3)
UI.heading(msg) — Standard tier, section headers¶
Use for top-level command banners, not for phase progress. Phase progress uses UI.phase().
UI.phase(current, total, label) — Standard tier, phase progress¶
Use for multi-step command progress orientation. Only applicable to commands with 3+ distinct phases.
Decision Tree¶
When writing a new log call, apply this decision tree:
Is this a failure?
+-- Yes, unrecoverable --> UI.die()
+-- Yes, execution can continue --> UI.error()
+-- No --> continue
Is this a genuine user-actionable advisory?
+-- Yes --> UI.warning()
+-- No --> continue
Is this a completed milestone or positive confirmation?
+-- Yes --> UI.success()
+-- No --> continue
Is this a top-level phase the user explicitly triggered?
+-- Yes --> UI.info()
+-- No --> continue
Is this a sub-step within a named phase?
+-- Yes --> UI.detail()
+-- No --> continue
Is this an internal state value, raw command, or API payload?
+-- Yes --> UI.debug()
Anti-Patterns¶
Do not use warning() for expected operations¶
# Wrong - routine Docker operation
UI.warning(f"Reclaiming volume permissions for {path}...")
# Right - batch summary at detail level
UI.detail(f"Reclaimed volume ownership across {n} directories")
Do not use info() for sub-steps¶
# Wrong - sub-step of restore phase
UI.info("Scaffolding Docker environment for restore...")
# Right
UI.detail("Scaffolding Docker environment for restore...")
Do not use info() for internal state dumps¶
Do not emit O(n) lines where n is unbounded¶
# Wrong - 1 line per directory
for path in paths:
UI.warning(f"Reclaiming volume permissions for {path}...")
# Right - 1 summary line, per-item detail only in verbose
UI.detail(f"Reclaimed volume ownership across {len(paths)} directories")
for path in paths:
UI.debug(f" {path}")
Do not call trace() manually¶
UI.trace() is called automatically by UI._print(). All output goes to ~/.ldm/last-command.log regardless of level.
One narrow exception: facts that are never printed at all. A handful of
call sites record something the console deliberately never shows, so
UI._print() never runs and there is nothing for it to trace. These are not a
loophole for demoting noisy output -- if a line has a console tier, put it on
that tier and let _print() trace it.
The exception covers exactly:
CommandRunner.runinldm_core/utils.py, which records[CMD],[EXIT n],[STDOUT]and[STDERR]for every command it runs. LDM-#1615 added the[EXIT n]/[STDERR]pair to thecheck=Falsefailure branch: that branch returnsNoneand used to discardresult.stderroutright, so a trace log from a failed run was indistinguishable from one from a successful run, and docker's own explanation of a failure could not be recovered by anyone. Seelast_command_failure()for the in-process half of the same record.cli.py's catch-allexcept Exception, which traces the traceback. The terminal gets the exception type and message; the full traceback belongs in the file, not in a user's console.
Phase Headers¶
Multi-step commands must use phase headers to give users orientation:
# handlers/workspace.py - quickstart
UI.phase(1, 5, "Downloading")
# ... download work ...
UI.phase(2, 5, "Restoring")
# ... restore work ...
Commands that do NOT need phase headers (single-purpose):
ldm logs, ldm status, ldm config, ldm snapshots, ldm stop, ldm nuke
Completion Banners¶
Any command that involves a Liferay boot wait (_wait_for_ready) must emit a completion banner:
Liferay is ready (3m 42s)
https://aica.local
/Users/peterrichards/ldm/aica
Next: ldm logs aica View live logs
ldm snapshot aica Take a backup
In --non-interactive mode, emit a single structured line:
Reviewing Log Calls¶
When reviewing PRs, flag any log call that:
- Uses
UI.warning()for an expected/routine operation - Uses
UI.info()for a sub-step that is not a top-level phase milestone - Emits O(n) lines where n is unbounded (batching required)
- Uses
UI.success()for a trivial internal confirmation - Duplicates a message that was already emitted in the same command run
- Appears inside a tight loop without a batching guard
The Python logging module¶
The tiers above are LDM's own output. A handful of call sites — currently only
ldm_core/pipelines/base.py — use Python's logging module instead, because they
report on pipeline stage machinery rather than on anything a user asked for.
Those records go to the trace log, never to the console. UI.init_trace_log()
installs _TraceLogHandler on the root logger, which formats each record — including
exc_info tracebacks — and writes it via UI.trace().
| Logger | Level | Where it lands |
|---|---|---|
pipeline.* (LDM's own) |
DEBUG |
~/.ldm/last-command.log |
| everything else (docker, urllib3, werkzeug) | WARNING |
~/.ldm/last-command.log |
| anything | — | never the console |
Why this exists (LDM-#1669)¶
Before it, nothing connected logging to LDM at all. Records at DEBUG were discarded
outright, and WARNING+ were handled by logging.lastResort — a bare stderr
StreamHandler, fixed at WARNING, with no formatter and no way to switch it off.
That is why LDM-#1668 could print a full Python traceback over a controlled UI.die,
and why no flag suppressed it: -v/--verbose sets UI.VERBOSE, which belongs to the
tier system above and has nothing to do with the logging module.
Installing any handler retires lastResort, so attaching the bridge is what keeps
logging output off the console — not a separate suppression step.
Rules¶
- Prefer the
UI.*tiers. They are the documented interface, and they reach the trace log anyway viaUI._print(). Reach forloggingonly for machinery a user would never be shown. - Never raise a record above
DEBUGto make it visible. It will not appear on the console regardless, and atWARNING+ it merely becomes noise in a shared file. If a user needs to see something, that is whatUI.info()/UI.warning()are for. - Do not add a second logging destination. The trace log is truncated per invocation and already carries a header (timestamp, command line, platform, Python version). A parallel log would split the evidence needed to diagnose one run.
Related Issues¶
- #754 — Surface
--infoflag as documented CLI option - #755 — Reclassify ~200 UI.info() calls to detail()/debug()
- #756 — Restore UI.warning() semantic integrity
- #757 — Add phase headers to multi-step commands
- #758 — Add completion banner to quickstart and run
Last Updated: 2026-09-11 | Last Reviewed: 2026-09-11