-
Notifications
You must be signed in to change notification settings - Fork 0
2.3 Logging System
ClioDeck has three logging-adjacent mechanisms, not two as an earlier version of this page said — and one of the three previously documented here turned out to be dead code.
-
Global console filter (
src/shared/console-filter.ts) — intercepts every rawconsole.*call, and is imported by bothsrc/renderer/src/main.tsxandsrc/main/index.ts. But its env-var branching (CLIODESK_DEBUG/CLIODESK_LOG_LEVEL) only has an observable effect in the main process: theBrowserWindowis created withcontextIsolation: true, nodeIntegration: false, sandbox: true(src/main/index.ts, unconditionally, dev and prod alike), so the renderer has noprocessglobal at runtime — this file's ownisProduction()/isDebugEnabled()checks (typeof process !== 'undefined') can never be true there, env vars or not. Separately, Vite'sesbuild.puresetting (see below) already strips rawconsole.log/info/debugcalls from the production renderer bundle regardless.restoreConsole/rawConsole/getFilterStatusare real exports of this file, but in the renderer they operate on a filter that was never actually engaged. -
Renderer logger (
src/renderer/src/utils/logger.ts) — the structured logger actually imported throughout the renderer (stores, components:MainLayout.tsx,similarityStore.ts,editorStore.ts, and more —grep -rn "import { logger }" src/renderer/shows every hit resolves here). See "Renderer Logger (for Developers)" below for its real API. -
Main-process logger (
src/main/utils/logger.ts) — used throughout the main process (IPC handlers, services). Different API (logger.info(module, action, data)), different output format ([2026-…] INFO [ipc] fs:read-directory {...}), and — important — it does not readCLIODESK_LOG_LEVELor any other env var. It hardcodeslevel = 'info'and nothing in the codebase ever calls itssetLevel(). SettingCLIODESK_LOG_LEVEL=debugraises verbosity for the console filter only; main-processdebug-level logs from IPC handlers stay suppressed regardless.
@shared/logger (src/shared/logger.ts) is dead code. An earlier
version of this page documented it as "the" renderer logger, with a
logger.debug(context, message, data) API and a createContextLogger()
helper. Neither has any call site anywhere in the repo outside the file's
own definition (grep -rln "@shared/logger" src/ backend/ returns only
src/shared/logger.ts itself) — every real renderer import goes through
src/renderer/src/utils/logger.ts instead, a different file with a
different API. If you see this page's older text elsewhere describing
logger.createContextLogger, it no longer reflects real code.
If you're debugging something in src/main/ (IPC, services), the env vars below won't help — there is currently no way to raise that logger's level short of editing src/main/utils/logger.ts.
| Environment | console.log | console.info | console.warn | console.error |
|---|---|---|---|---|
| Development | Shown | Shown | Shown | Shown |
| Production | Filtered | Filtered | Shown | Shown |
In production, only console.warn and console.error are displayed to reduce console noise.
This table describes the observable outcome, not one uniform mechanism. In the main process, it's genuinely this file's
getFilterConfig()doing the filtering. In the renderer, this file's filter never actually engages at all —isProduction()always returnsfalsethere (noprocessglobal, sandboxed), soinstallConsoleFilter()installs a no-op pass-through regardless of environment. The renderer's production row holds anyway, but for an unrelated reason: Vite'sesbuild.puresetting (see below) deletesconsole.log/info/debugcalls from the bundle at build time. In renderer development builds, neither mechanism filters anything, which is why rawconsole.logcalls show there too.
⚠️ These env vars do NOT revive the renderer logger'slog/info/debugcalls in a packaged build — only rawconsole.*calls. The renderer logger's prod-gate (isProd = import.meta.env?.PROD === true,src/renderer/src/utils/logger.ts:28) is a build-time Vite constant, not a runtime check. Worse,vite.config.mts:79marksconsole.log,console.info, andconsole.debugaspurefor esbuild — in a production build, the real branch ofisProd ? noop : realCallis physically removed from the shipped bundle before the app ever runs. No environment variable can bring it back. If you're debugging a packaged build and relying onlogger.debug()/logger.log()/logger.info()calls (see "Renderer Logger" below), you will get silence — build from source in development mode instead, or add temporaryrawConsole.log()calls (see "Restoring Logs" below), which bypass this entirely.The env vars below genuinely do work for the separate console-filter mechanism (raw, un-wrapped
console.log/console.infocalls) — but only in the main process. The renderer is sandboxed (contextIsolation, nonodeIntegration) and has noprocessglobal, so the filter's own env-var checks can never fire there; and any raw console call that survives esbuild'spurestripping in a renderer production build is vanishingly rare to begin with. Either way, they never affect this renderer logger's own methods.
# macOS / Linux
CLIODESK_DEBUG=1 /path/to/ClioDeck.app/Contents/MacOS/ClioDeck
# Windows
set CLIODESK_DEBUG=1
"C:\Program Files\ClioDeck\ClioDeck.exe"# Available levels: debug, info, warn, error
CLIODESK_LOG_LEVEL=debug /path/to/ClioDeckDEBUG=1 /path/to/ClioDeckBy default, Electron DevTools are disabled in production.
To enable them, src/main/index.ts checks CLIODESK_DEBUG=1 or DEBUG=1
specifically — not CLIODESK_LOG_LEVEL, which only affects the console
filter (see above), not this check:
# macOS / Linux
CLIODESK_DEBUG=1 /path/to/ClioDeck.app/Contents/MacOS/ClioDeck
# Windows
set CLIODESK_DEBUG=1
"C:\Program Files\ClioDeck\ClioDeck.exe"This opens the DevTools window on the renderer's webContents — but that
window will not show revived renderer console.log/info/debug
calls. Those were already removed from the production renderer bundle at
build time by esbuild's pure stripping, regardless of any env var, so
there's nothing left for DevTools to display on that front. What
CLIODESK_DEBUG=1 genuinely does, in addition to opening DevTools, is
un-suppress raw console.log/info calls in the main process (the
~40+ files under src/main/ that call console.log directly rather than
through the structured main-process logger) — those print to the terminal
you launched ClioDeck from, not to the DevTools panel.
For renderer code, use src/renderer/src/utils/logger.ts — this is the
one actually imported throughout the renderer, not @shared/logger:
import { logger } from '../utils/logger'; // path relative to caller
// Variadic, mirrors console.* — stripped in production (both by esbuild's
// `pure` annotation on console.log/info/debug in vite.config.mts, and by
// an explicit no-op gate inside the module itself)
logger.log('Debug message', { data });
logger.info('Important information');
logger.debug('Verbose detail');
// warn/error always emit, in prod too — diagnostic surface for triage
logger.warn('Warning');
logger.error('MyComponent', 'Something failed', error);
// Typed channels, kept for existing call sites
logger.ipc('fs:read-directory', { dirPath });
logger.store('similarityStore', 'analyze', { segments: 12 });
logger.component('SimilarityCard', 'Inserting citation', { zoteroKey });-
log/info/debugare compiled out of the production bundle entirely (not just hidden — removed), so expensive argument construction never runs in prod either -
warn/erroralways reach the console, in every environment - Typed channel helpers (
ipc/store/component/error) give consistent, greppable prefixes without extra ceremony
For code under src/main/ (IPC handlers, services), use the separate main-process logger instead:
import { logger } from '../../utils/logger.js';
logger.info('ipc', 'fs:read-directory', { dirPath });
logger.error('ipc', 'fs:read-directory', { error: error.message });
// Measuring duration:
const elapsed = logger.startTimer();
await someWork();
logger.info('ipc', 'pdf:index', { durationMs: elapsed() });Output: [2026-02-19T20:00:00.000Z] INFO [ipc] fs:read-directory { dirPath: "/home/user" }. Level defaults to info and cannot currently be changed at runtime — see the caveat above.
src/shared/
├── logger.ts # Dead code — no import anywhere outside this file
└── console-filter.ts # Global console.* filter, env-var-controlled —
# only actually live in the main process; the
# renderer's copy never engages (see Overview above)
src/renderer/src/utils/
└── logger.ts # Real renderer logger — what every renderer import resolves to
src/main/utils/
└── logger.ts # Main-process logger, fixed at 'info', no env var
The console filter is automatically imported at application startup:
- Main process:
src/main/index.ts - Renderer process:
src/renderer/src/main.tsx
Environment is automatically detected via:
process.env.NODE_ENV === 'production'process.env.ELECTRON_IS_PACKAGED === 'true'
import { restoreConsole, rawConsole } from '@shared/console-filter';
// Restore all console.*
restoreConsole();
// Or use rawConsole to bypass filter
rawConsole.log('This message will always be displayed');import { getFilterStatus } from '@shared/console-filter';
const status = getFilterStatus();
console.log(status);
// { isProduction: true, isDebugEnabled: false, isFiltering: true }