refactor: centralize restart log conventions

This commit is contained in:
Peter Steinberger
2026-04-18 19:05:21 +01:00
parent a7e029fde9
commit 28be124cc1
12 changed files with 175 additions and 32 deletions

View File

@@ -28,11 +28,13 @@ vi.mock("../../daemon/inspect.js", () => ({
renderGatewayServiceCleanupHints: () => [],
}));
vi.mock("../../daemon/launchd.js", () => ({
vi.mock("../../daemon/restart-logs.js", () => ({
resolveGatewayLogPaths: () => ({
logDir: "/tmp",
stdoutPath: "/tmp/gateway.out.log",
stderrPath: "/tmp/gateway.err.log",
}),
resolveGatewayRestartLogPath: () => "/tmp/gateway-restart.log",
}));
vi.mock("../../daemon/systemd-hints.js", () => ({

View File

@@ -4,7 +4,7 @@ import {
resolveGatewaySystemdServiceName,
} from "../../daemon/constants.js";
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
import {
isSystemdUnavailableDetail,
renderSystemdUnavailableHints,
@@ -288,20 +288,23 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
defaultRuntime.error(
errorText(`Gateway port ${status.port.port} is not listening (service appears running).`),
);
const serviceEnv = { ...process.env, ...service.command?.environment };
if (status.lastError) {
defaultRuntime.error(`${errorText("Last gateway error:")} ${status.lastError}`);
}
if (process.platform === "linux") {
const env = service.command?.environment ?? process.env;
const unit = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
const unit = resolveGatewaySystemdServiceName(serviceEnv.OPENCLAW_PROFILE);
defaultRuntime.error(
errorText(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`),
);
} else if (process.platform === "darwin") {
const logs = resolveGatewayLogPaths(service.command?.environment ?? process.env);
const logs = resolveGatewayLogPaths(serviceEnv);
defaultRuntime.error(`${errorText("Logs:")} ${shortenHomePath(logs.stdoutPath)}`);
defaultRuntime.error(`${errorText("Errors:")} ${shortenHomePath(logs.stderrPath)}`);
}
defaultRuntime.error(
`${errorText("Restart log:")} ${shortenHomePath(resolveGatewayRestartLogPath(serviceEnv))}`,
);
spacer();
}

View File

@@ -14,6 +14,7 @@ import {
import { formatConfigIssueLines } from "../../config/issue-format.js";
import { asResolvedSourceConfig, asRuntimeConfig } from "../../config/materialize.js";
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
import { resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { nodeVersionSatisfiesEngine } from "../../infra/runtime-guard.js";
import {
@@ -715,6 +716,9 @@ async function maybeRestartService(params: {
for (const line of renderRestartDiagnostics(health)) {
defaultRuntime.log(theme.muted(line));
}
defaultRuntime.log(
theme.muted(`Restart log: ${resolveGatewayRestartLogPath(process.env)}`),
);
defaultRuntime.log(
theme.muted(
`Run \`${replaceCliName(formatCliCommand("openclaw gateway status --deep"), CLI_NAME)}\` for details.`,

View File

@@ -1,10 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import type { ProgressReporter } from "../../cli/progress.js";
vi.mock("../../daemon/launchd.js", () => ({
vi.mock("../../daemon/restart-logs.js", () => ({
resolveGatewayLogPaths: () => {
throw new Error("skip log tail");
},
resolveGatewayRestartLogPath: () => "/tmp/gateway-restart.log",
}));
vi.mock("./gateway.js", () => ({

View File

@@ -1,6 +1,6 @@
import type { ProgressReporter } from "../../cli/progress.js";
import { formatConfigIssueLine } from "../../config/issue-format.js";
import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
import {
formatPortDiagnostics,
isDualStackLoopbackGatewayListeners,
@@ -218,9 +218,11 @@ export async function appendStatusAllDiagnosis(params: {
})();
if (logPaths) {
params.progress.setLabel("Reading logs…");
const [stderrTail, stdoutTail] = await Promise.all([
const restartLogPath = resolveGatewayRestartLogPath(process.env);
const [stderrTail, stdoutTail, restartTail] = await Promise.all([
readFileTailLines(logPaths.stderrPath, 40).catch(() => []),
readFileTailLines(logPaths.stdoutPath, 40).catch(() => []),
readFileTailLines(restartLogPath, 30).catch(() => []),
]);
if (stderrTail.length > 0 || stdoutTail.length > 0) {
lines.push("");
@@ -234,6 +236,13 @@ export async function appendStatusAllDiagnosis(params: {
lines.push(` ${muted(line)}`);
}
}
if (restartTail.length > 0) {
lines.push("");
lines.push(muted(`Gateway restart attempts (tail): ${restartLogPath}`));
for (const line of summarizeLogTail(restartTail, { maxLines: 16 }).map(redactSecrets)) {
lines.push(` ${muted(line)}`);
}
}
}
params.progress.tick();

View File

@@ -1,5 +1,5 @@
import fs from "node:fs/promises";
import { resolveGatewayLogPaths } from "./launchd.js";
import { resolveGatewayLogPaths } from "./restart-logs.js";
const GATEWAY_LOG_ERROR_PATTERNS = [
/refusing to bind gateway/i,

View File

@@ -20,7 +20,8 @@ import {
scheduleDetachedLaunchdRestartHandoff,
} from "./launchd-restart-handoff.js";
import { formatLine, toPosixPath, writeFormattedLines } from "./output.js";
import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js";
import { resolveHomeDir } from "./paths.js";
import { resolveGatewayLogPaths } from "./restart-logs.js";
import { parseKeyValueOutput } from "./runtime-parse.js";
import type { GatewayServiceRuntime } from "./service-runtime.js";
import type {
@@ -65,21 +66,6 @@ export function resolveLaunchAgentPlistPath(env: GatewayServiceEnv): string {
return resolveLaunchAgentPlistPathForLabel(env, label);
}
export function resolveGatewayLogPaths(env: GatewayServiceEnv): {
logDir: string;
stdoutPath: string;
stderrPath: string;
} {
const stateDir = resolveGatewayStateDir(env);
const logDir = path.join(stateDir, "logs");
const prefix = env.OPENCLAW_LOG_PREFIX?.trim() || "gateway";
return {
logDir,
stdoutPath: path.join(logDir, `${prefix}.log`),
stderrPath: path.join(logDir, `${prefix}.err.log`),
};
}
export async function readLaunchAgentProgramArguments(
env: GatewayServiceEnv,
): Promise<GatewayServiceCommandConfig | null> {

View File

@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import {
GATEWAY_RESTART_LOG_FILENAME,
renderCmdRestartLogSetup,
renderPosixRestartLogSetup,
resolveGatewayLogPaths,
resolveGatewayRestartLogPath,
} from "./restart-logs.js";
describe("restart log conventions", () => {
it("resolves profile-aware gateway logs and restart attempts together", () => {
const env = {
HOME: "/Users/test",
OPENCLAW_PROFILE: "work",
};
expect(resolveGatewayLogPaths(env)).toEqual({
logDir: "/Users/test/.openclaw-work/logs",
stdoutPath: "/Users/test/.openclaw-work/logs/gateway.log",
stderrPath: "/Users/test/.openclaw-work/logs/gateway.err.log",
});
expect(resolveGatewayRestartLogPath(env)).toBe(
`/Users/test/.openclaw-work/logs/${GATEWAY_RESTART_LOG_FILENAME}`,
);
});
it("honors OPENCLAW_STATE_DIR for restart attempts", () => {
const env = {
HOME: "/Users/test",
OPENCLAW_STATE_DIR: "/tmp/openclaw-state",
};
expect(resolveGatewayRestartLogPath(env)).toBe(
`/tmp/openclaw-state/logs/${GATEWAY_RESTART_LOG_FILENAME}`,
);
});
it("renders best-effort POSIX log setup with escaped paths", () => {
const setup = renderPosixRestartLogSetup({
HOME: "/Users/test's",
});
expect(setup).toContain("mkdir -p '/Users/test'\\''s/.openclaw/logs' 2>/dev/null || true");
expect(setup).toContain(
"exec >>'/Users/test'\\''s/.openclaw/logs/gateway-restart.log' 2>&1 || true",
);
});
it("renders CMD log setup with quoted paths", () => {
const setup = renderCmdRestartLogSetup({
USERPROFILE: "C:\\Users\\Test User",
});
expect(setup.quotedLogPath).toBe('"C:\\Users\\Test User/.openclaw/logs/gateway-restart.log"');
expect(setup.lines).toContain(
'if not exist "C:\\Users\\Test User/.openclaw/logs" mkdir "C:\\Users\\Test User/.openclaw/logs" >nul 2>&1',
);
});
});

View File

@@ -0,0 +1,53 @@
import path from "node:path";
import { quoteCmdScriptArg } from "./cmd-argv.js";
import { resolveGatewayStateDir } from "./paths.js";
import type { GatewayServiceEnv } from "./service-types.js";
export const GATEWAY_RESTART_LOG_FILENAME = "gateway-restart.log";
export function resolveGatewayLogPaths(env: GatewayServiceEnv): {
logDir: string;
stdoutPath: string;
stderrPath: string;
} {
const stateDir = resolveGatewayStateDir(env);
const logDir = path.join(stateDir, "logs");
const prefix = env.OPENCLAW_LOG_PREFIX?.trim() || "gateway";
return {
logDir,
stdoutPath: path.join(logDir, `${prefix}.log`),
stderrPath: path.join(logDir, `${prefix}.err.log`),
};
}
export function resolveGatewayRestartLogPath(env: GatewayServiceEnv): string {
return path.join(resolveGatewayLogPaths(env).logDir, GATEWAY_RESTART_LOG_FILENAME);
}
export function shellEscapeRestartLogValue(value: string): string {
return value.replace(/'/g, "'\\''");
}
export function renderPosixRestartLogSetup(env: GatewayServiceEnv): string {
const logDir = path.dirname(resolveGatewayRestartLogPath(env));
const logPath = resolveGatewayRestartLogPath(env);
return `mkdir -p '${shellEscapeRestartLogValue(logDir)}' 2>/dev/null || true
exec >>'${shellEscapeRestartLogValue(logPath)}' 2>&1 || true`;
}
export function renderCmdRestartLogSetup(env: GatewayServiceEnv): {
lines: string[];
quotedLogPath: string;
} {
const logPath = resolveGatewayRestartLogPath(env);
const logDir = path.dirname(logPath);
const quotedLogDir = quoteCmdScriptArg(logDir);
const quotedLogPath = quoteCmdScriptArg(logPath);
return {
quotedLogPath,
lines: [
`if not exist ${quotedLogDir} mkdir ${quotedLogDir} >nul 2>&1`,
`>> ${quotedLogPath} 2>&1 echo [%DATE% %TIME%] openclaw restart log initialized`,
],
};
}

View File

@@ -16,6 +16,7 @@ describe("buildPlatformRuntimeLogHints", () => {
).toEqual([
"Launchd stdout (if installed): /tmp/openclaw-state/logs/gateway.log",
"Launchd stderr (if installed): /tmp/openclaw-state/logs/gateway.err.log",
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
]);
});
@@ -23,17 +24,29 @@ describe("buildPlatformRuntimeLogHints", () => {
expect(
buildPlatformRuntimeLogHints({
platform: "linux",
env: {
OPENCLAW_STATE_DIR: "/tmp/openclaw-state",
},
systemdServiceName: "openclaw-gateway",
windowsTaskName: "OpenClaw Gateway",
}),
).toEqual(["Logs: journalctl --user -u openclaw-gateway.service -n 200 --no-pager"]);
).toEqual([
"Logs: journalctl --user -u openclaw-gateway.service -n 200 --no-pager",
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
]);
expect(
buildPlatformRuntimeLogHints({
platform: "win32",
env: {
OPENCLAW_STATE_DIR: "/tmp/openclaw-state",
},
systemdServiceName: "openclaw-gateway",
windowsTaskName: "OpenClaw Gateway",
}),
).toEqual(['Logs: schtasks /Query /TN "OpenClaw Gateway" /V /FO LIST']);
).toEqual([
'Logs: schtasks /Query /TN "OpenClaw Gateway" /V /FO LIST',
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
]);
});
});

View File

@@ -1,5 +1,5 @@
import { resolveGatewayLogPaths } from "./launchd.js";
import { toPosixPath } from "./output.js";
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "./restart-logs.js";
function toDarwinDisplayPath(value: string): string {
return toPosixPath(value).replace(/^[A-Za-z]:/, "");
@@ -12,19 +12,26 @@ export function buildPlatformRuntimeLogHints(params: {
windowsTaskName: string;
}): string[] {
const platform = params.platform ?? process.platform;
const env = params.env ?? process.env;
const env = { ...process.env, ...params.env };
if (platform === "darwin") {
const logs = resolveGatewayLogPaths(env);
return [
`Launchd stdout (if installed): ${toDarwinDisplayPath(logs.stdoutPath)}`,
`Launchd stderr (if installed): ${toDarwinDisplayPath(logs.stderrPath)}`,
`Restart attempts: ${toDarwinDisplayPath(resolveGatewayRestartLogPath(env))}`,
];
}
if (platform === "linux") {
return [`Logs: journalctl --user -u ${params.systemdServiceName}.service -n 200 --no-pager`];
return [
`Logs: journalctl --user -u ${params.systemdServiceName}.service -n 200 --no-pager`,
`Restart attempts: ${resolveGatewayRestartLogPath(env)}`,
];
}
if (platform === "win32") {
return [`Logs: schtasks /Query /TN "${params.windowsTaskName}" /V /FO LIST`];
return [
`Logs: schtasks /Query /TN "${params.windowsTaskName}" /V /FO LIST`,
`Restart attempts: ${resolveGatewayRestartLogPath(env)}`,
];
}
return [];
}

View File

@@ -1,12 +1,17 @@
import { beforeAll, describe, expect, it, vi } from "vitest";
const resolveGatewayLogPathsMock = vi.fn(() => ({
logDir: "C:\\tmp\\openclaw-state\\logs",
stdoutPath: "C:\\tmp\\openclaw-state\\logs\\gateway.log",
stderrPath: "C:\\tmp\\openclaw-state\\logs\\gateway.err.log",
}));
const resolveGatewayRestartLogPathMock = vi.fn(
() => "C:\\tmp\\openclaw-state\\logs\\gateway-restart.log",
);
vi.mock("./launchd.js", () => ({
vi.mock("./restart-logs.js", () => ({
resolveGatewayLogPaths: resolveGatewayLogPathsMock,
resolveGatewayRestartLogPath: resolveGatewayRestartLogPathMock,
}));
let buildPlatformRuntimeLogHints: typeof import("./runtime-hints.js").buildPlatformRuntimeLogHints;
@@ -26,6 +31,7 @@ describe("buildPlatformRuntimeLogHints", () => {
).toEqual([
"Launchd stdout (if installed): /tmp/openclaw-state/logs/gateway.log",
"Launchd stderr (if installed): /tmp/openclaw-state/logs/gateway.err.log",
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
]);
});
});