Skip to content

2.3 Logging System

Frédéric Clavert edited this page Jul 24, 2026 · 9 revisions

Overview

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 raw console.* call, and is imported by both src/renderer/src/main.tsx and src/main/index.ts. But its env-var branching (CLIODESK_DEBUG/CLIODESK_LOG_LEVEL) only has an observable effect in the main process: the BrowserWindow is created with contextIsolation: true, nodeIntegration: false, sandbox: true (src/main/index.ts, unconditionally, dev and prod alike), so the renderer has no process global at runtime — this file's own isProduction()/isDebugEnabled() checks (typeof process !== 'undefined') can never be true there, env vars or not. Separately, Vite's esbuild.pure setting (see below) already strips raw console.log/info/debug calls from the production renderer bundle regardless. restoreConsole/rawConsole/getFilterStatus are 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 read CLIODESK_LOG_LEVEL or any other env var. It hardcodes level = 'info' and nothing in the codebase ever calls its setLevel(). Setting CLIODESK_LOG_LEVEL=debug raises verbosity for the console filter only; main-process debug-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.

Default Behavior

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 returns false there (no process global, sandboxed), so installConsoleFilter() installs a no-op pass-through regardless of environment. The renderer's production row holds anyway, but for an unrelated reason: Vite's esbuild.pure setting (see below) deletes console.log/info/debug calls from the bundle at build time. In renderer development builds, neither mechanism filters anything, which is why raw console.log calls show there too.

Enabling Debug Logs in Production

⚠️ These env vars do NOT revive the renderer logger's log/info/debug calls in a packaged build — only raw console.* 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:79 marks console.log, console.info, and console.debug as pure for esbuild — in a production build, the real branch of isProd ? noop : realCall is 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 on logger.debug()/logger.log()/logger.info() calls (see "Renderer Logger" below), you will get silence — build from source in development mode instead, or add temporary rawConsole.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.info calls) — but only in the main process. The renderer is sandboxed (contextIsolation, no nodeIntegration) and has no process global, so the filter's own env-var checks can never fire there; and any raw console call that survives esbuild's pure stripping in a renderer production build is vanishingly rare to begin with. Either way, they never affect this renderer logger's own methods.

Method 1: CLIODESK_DEBUG Environment Variable

# macOS / Linux
CLIODESK_DEBUG=1 /path/to/ClioDeck.app/Contents/MacOS/ClioDeck

# Windows
set CLIODESK_DEBUG=1
"C:\Program Files\ClioDeck\ClioDeck.exe"

Method 2: CLIODESK_LOG_LEVEL Environment Variable

# Available levels: debug, info, warn, error
CLIODESK_LOG_LEVEL=debug /path/to/ClioDeck

Method 3: Standard DEBUG Variable

DEBUG=1 /path/to/ClioDeck

DevTools in Production

By 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.

Renderer Logger (for Developers)

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 });

Renderer Logger Benefits

  • log/info/debug are compiled out of the production bundle entirely (not just hidden — removed), so expensive argument construction never runs in prod either
  • warn/error always reach the console, in every environment
  • Typed channel helpers (ipc/store/component/error) give consistent, greppable prefixes without extra ceremony

Main-Process Logger (for Developers)

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.

Architecture

src/shared/
├── logger.ts          # Dead code — no import anywhere outside this file
└── console-filter.ts  # Global console.* filter, env-var-controlled (live)

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 Detection

Environment is automatically detected via:

  1. process.env.NODE_ENV === 'production'
  2. process.env.ELECTRON_IS_PACKAGED === 'true'

Restoring Logs (for Testing)

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');

Checking Filter Status

import { getFilterStatus } from '@shared/console-filter';

const status = getFilterStatus();
console.log(status);
// { isProduction: true, isDebugEnabled: false, isFiltering: true }

Clone this wiki locally