Skip to content

fix(logging): Apply LOG_JSON pattern to all logger calls (multiline output remains in 144 places) #209

@polaz

Description

@polaz

Problem

Issue #207 fixed multiline output for access logs only, but the same problem persists in 144 other places across the codebase.

Evidence from production logs (gm.sw.foundation)

Jan 25 14:25:37 gitlab-mcp.sw.foundation node[39293]: [14:25:37.597] INFO (gitlab-mcp): OAuth mode: unauthenticated version detection failed, deferring all introspection
Jan 25 14:25:37 gitlab-mcp.sw.foundation node[39293]:     status: 401

The status: 401 appears on a separate line because of:

// src/services/ConnectionManager.ts:108-111
logger.info(
  { status: versionResponse.status },
  "OAuth mode: unauthenticated version detection failed, deferring all introspection"
);

Scope of the Problem

$ grep -rE 'logger\.(info|warn|error|debug)\(\s*\{' src/ | wc -l
144

144 occurrences across 28 files use logger.level({ object }, "message") pattern which causes multiline output in plain text mode.

Files Affected

File Count
src/oauth/session-store.ts 20
src/server.ts 15
src/oauth/storage/memory.ts 11
src/discovery/auto.ts 10
src/main.ts 8
src/entities/context/context-manager.ts 7
src/profiles/loader.ts 7
src/profiles/project-loader.ts 7
src/session-manager.ts 7
src/oauth/gitlab-device-flow.ts 7
... 18 more files ...

Root Cause

Issue #207 introduced the LOG_JSON pattern for access logs:

// src/logging/request-tracker.ts (fixed in #207)
if (LOG_JSON) {
  logger.info({ accessLog: entry }, logLine);
} else {
  logger.info(logLine);  // No object = no multiline
}

But this pattern was only applied to access logs, not to the 144 other logger calls.

Solution

Apply the same pattern from #207 to all logger calls:

Option A: Global Helper Function (Recommended)

Create a helper that handles both modes transparently:

// src/logger.ts
export function logInfo(message: string, data?: Record<string, unknown>): void {
  if (LOG_JSON) {
    logger.info(data ?? {}, message);
  } else if (data && Object.keys(data).length > 0) {
    const pairs = Object.entries(data)
      .map(([k, v]) => `${k}=${typeof v === 'object' ? JSON.stringify(v) : v}`)
      .join(' ');
    logger.info(`${message} ${pairs}`);
  } else {
    logger.info(message);
  }
}

// Similar helpers: logWarn, logError, logDebug

Then update all calls:

// Before
logger.info({ status: 401 }, "Version detection failed");

// After
logInfo("Version detection failed", { status: 401 });

Option B: Conditional Logging at Each Call Site

Use LOG_JSON check at each location (more verbose but explicit):

if (LOG_JSON) {
  logger.info({ status: 401 }, "Version detection failed");
} else {
  logger.info(`Version detection failed status=${401}`);
}

Acceptance Criteria

  • All 144 logger calls updated to use single-line output in plain mode
  • LOG_JSON=false (default): All logs as single-line plain text with key=value format
  • LOG_JSON=true: Full structured objects preserved in JSON output
  • No multiline output visible in journalctl -u gitlab-mcp
  • Helper functions created and exported from src/logger.ts
  • Existing tests still pass

Testing

# Deploy to staging or run locally
yarn dev:sse

# Check for multiline output (should find NONE)
journalctl -u gitlab-mcp --since "1 minute ago" | grep -E "^\s{4}\w+:"

# Verify JSON mode still works
LOG_JSON=true yarn dev:sse 2>&1 | head -5 | jq .

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions