feat(daemon): split console/file logging defaults with rotation (#88)
This commit is contained in:
@@ -26,7 +26,7 @@ async function main() {
|
||||
try {
|
||||
paseoHome = resolvePaseoHome();
|
||||
const persistedConfig = loadPersistedConfig(paseoHome);
|
||||
logger = createRootLogger(persistedConfig);
|
||||
logger = createRootLogger(persistedConfig, { paseoHome });
|
||||
config = loadConfig(paseoHome);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
||||
@@ -1,108 +1,232 @@
|
||||
import path from "node:path";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { resolveLogConfig } from "./logger.js";
|
||||
import type { PersistedConfig } from "./persisted-config.js";
|
||||
|
||||
describe("resolveLogConfig", () => {
|
||||
const originalEnv = process.env;
|
||||
const paseoHome = "/tmp/paseo-logger-tests";
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.PASEO_LOG;
|
||||
delete process.env.PASEO_LOG_FORMAT;
|
||||
delete process.env.PASEO_LOG_CONSOLE_LEVEL;
|
||||
delete process.env.PASEO_LOG_FILE_LEVEL;
|
||||
delete process.env.PASEO_LOG_FILE_PATH;
|
||||
delete process.env.PASEO_LOG_FILE_ROTATE_SIZE;
|
||||
delete process.env.PASEO_LOG_FILE_ROTATE_COUNT;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("returns defaults when no config or env vars", () => {
|
||||
const result = resolveLogConfig(undefined);
|
||||
expect(result).toEqual({ level: "debug", format: "pretty" });
|
||||
});
|
||||
|
||||
it("uses config.json values over defaults", () => {
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
level: "debug",
|
||||
format: "json",
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "debug", format: "json" });
|
||||
});
|
||||
|
||||
it("uses env PASEO_LOG over config.json level", () => {
|
||||
process.env.PASEO_LOG = "warn";
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
level: "debug",
|
||||
format: "json",
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "warn", format: "json" });
|
||||
});
|
||||
|
||||
it("uses env PASEO_LOG_FORMAT over config.json format", () => {
|
||||
process.env.PASEO_LOG_FORMAT = "pretty";
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
level: "debug",
|
||||
format: "json",
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "debug", format: "pretty" });
|
||||
});
|
||||
|
||||
it("env vars override both config.json and defaults", () => {
|
||||
process.env.PASEO_LOG = "error";
|
||||
process.env.PASEO_LOG_FORMAT = "json";
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
it("returns dual-sink defaults when no config or env vars", () => {
|
||||
const result = resolveLogConfig(undefined, { paseoHome });
|
||||
expect(result).toEqual({
|
||||
level: "trace",
|
||||
console: {
|
||||
level: "info",
|
||||
format: "pretty",
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "error", format: "json" });
|
||||
file: {
|
||||
level: "trace",
|
||||
path: path.join(paseoHome, "daemon.log"),
|
||||
rotate: {
|
||||
maxSize: "10m",
|
||||
maxFiles: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles partial config - level only", () => {
|
||||
it("uses config.json destination-specific values over defaults", () => {
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
console: {
|
||||
level: "warn",
|
||||
format: "json",
|
||||
},
|
||||
file: {
|
||||
level: "debug",
|
||||
path: "/tmp/custom.log",
|
||||
rotate: {
|
||||
maxSize: "25m",
|
||||
maxFiles: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config, { paseoHome });
|
||||
|
||||
expect(result).toEqual({
|
||||
level: "debug",
|
||||
console: {
|
||||
level: "warn",
|
||||
format: "json",
|
||||
},
|
||||
file: {
|
||||
level: "debug",
|
||||
path: "/tmp/custom.log",
|
||||
rotate: {
|
||||
maxSize: "25m",
|
||||
maxFiles: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses env vars over config.json values", () => {
|
||||
process.env.PASEO_LOG_CONSOLE_LEVEL = "error";
|
||||
process.env.PASEO_LOG_FILE_LEVEL = "fatal";
|
||||
process.env.PASEO_LOG_FORMAT = "json";
|
||||
process.env.PASEO_LOG_FILE_PATH = "logs/daemon-custom.log";
|
||||
process.env.PASEO_LOG_FILE_ROTATE_SIZE = "15m";
|
||||
process.env.PASEO_LOG_FILE_ROTATE_COUNT = "4";
|
||||
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
console: {
|
||||
level: "info",
|
||||
format: "pretty",
|
||||
},
|
||||
file: {
|
||||
level: "trace",
|
||||
path: "/tmp/will-be-overridden.log",
|
||||
rotate: {
|
||||
maxSize: "30m",
|
||||
maxFiles: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolveLogConfig(config, { paseoHome });
|
||||
expect(result).toEqual({
|
||||
level: "error",
|
||||
console: {
|
||||
level: "error",
|
||||
format: "json",
|
||||
},
|
||||
file: {
|
||||
level: "fatal",
|
||||
path: path.resolve(paseoHome, "logs/daemon-custom.log"),
|
||||
rotate: {
|
||||
maxSize: "15m",
|
||||
maxFiles: 4,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps backwards compatibility for legacy log.level and log.format", () => {
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
level: "warn",
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "warn", format: "pretty" });
|
||||
});
|
||||
|
||||
it("handles partial config - format only", () => {
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
format: "json",
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "debug", format: "json" });
|
||||
|
||||
const result = resolveLogConfig(config, { paseoHome });
|
||||
expect(result).toEqual({
|
||||
level: "warn",
|
||||
console: {
|
||||
level: "warn",
|
||||
format: "json",
|
||||
},
|
||||
file: {
|
||||
level: "warn",
|
||||
path: path.join(paseoHome, "daemon.log"),
|
||||
rotate: {
|
||||
maxSize: "10m",
|
||||
maxFiles: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("handles empty log object in config", () => {
|
||||
it("keeps backwards compatibility for legacy env vars", () => {
|
||||
process.env.PASEO_LOG = "error";
|
||||
process.env.PASEO_LOG_FORMAT = "json";
|
||||
|
||||
const result = resolveLogConfig(undefined, { paseoHome });
|
||||
expect(result).toEqual({
|
||||
level: "error",
|
||||
console: {
|
||||
level: "error",
|
||||
format: "json",
|
||||
},
|
||||
file: {
|
||||
level: "error",
|
||||
path: path.join(paseoHome, "daemon.log"),
|
||||
rotate: {
|
||||
maxSize: "10m",
|
||||
maxFiles: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("supports partial destination config and retains defaults", () => {
|
||||
const config: PersistedConfig = {
|
||||
log: {},
|
||||
log: {
|
||||
console: {
|
||||
level: "warn",
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = resolveLogConfig(config);
|
||||
expect(result).toEqual({ level: "debug", format: "pretty" });
|
||||
|
||||
const result = resolveLogConfig(config, { paseoHome });
|
||||
expect(result).toEqual({
|
||||
level: "trace",
|
||||
console: {
|
||||
level: "warn",
|
||||
format: "pretty",
|
||||
},
|
||||
file: {
|
||||
level: "trace",
|
||||
path: path.join(paseoHome, "daemon.log"),
|
||||
rotate: {
|
||||
maxSize: "10m",
|
||||
maxFiles: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("supports all log levels", () => {
|
||||
const levels: Array<"trace" | "debug" | "info" | "warn" | "error" | "fatal"> =
|
||||
["trace", "debug", "info", "warn", "error", "fatal"];
|
||||
it("ignores invalid rotate count env var and falls back to config value", () => {
|
||||
process.env.PASEO_LOG_FILE_ROTATE_COUNT = "0";
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
file: {
|
||||
rotate: {
|
||||
maxFiles: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolveLogConfig(config, { paseoHome });
|
||||
expect(result.file.rotate.maxFiles).toBe(7);
|
||||
});
|
||||
|
||||
it("supports all log levels for destination-specific env vars", () => {
|
||||
const levels: Array<"trace" | "debug" | "info" | "warn" | "error" | "fatal"> = [
|
||||
"trace",
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
"fatal",
|
||||
];
|
||||
|
||||
for (const level of levels) {
|
||||
process.env.PASEO_LOG = level;
|
||||
const result = resolveLogConfig(undefined);
|
||||
process.env.PASEO_LOG_CONSOLE_LEVEL = level;
|
||||
process.env.PASEO_LOG_FILE_LEVEL = level;
|
||||
const result = resolveLogConfig(undefined, { paseoHome });
|
||||
expect(result.console.level).toBe(level);
|
||||
expect(result.file.level).toBe(level);
|
||||
expect(result.level).toBe(level);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,49 +1,248 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import pretty from "pino-pretty";
|
||||
import { createStream as createRotatingFileStream } from "rotating-file-stream";
|
||||
import type { PersistedConfig } from "./persisted-config.js";
|
||||
import { resolvePaseoHome } from "./paseo-home.js";
|
||||
|
||||
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
export type LogFormat = "pretty" | "json";
|
||||
|
||||
export interface ResolvedLogConfig {
|
||||
level: LogLevel;
|
||||
format: LogFormat;
|
||||
console: {
|
||||
level: LogLevel;
|
||||
format: LogFormat;
|
||||
};
|
||||
file: {
|
||||
level: LogLevel;
|
||||
path: string;
|
||||
rotate: {
|
||||
maxSize: string;
|
||||
maxFiles: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type LegacyLogConfig = {
|
||||
level?: LogLevel;
|
||||
format?: LogFormat;
|
||||
};
|
||||
|
||||
type LoggerConfigInput = PersistedConfig | LegacyLogConfig | undefined;
|
||||
|
||||
type ResolveLogConfigOptions = {
|
||||
paseoHome?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
const LOG_LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"];
|
||||
const LOG_FORMATS: LogFormat[] = ["pretty", "json"];
|
||||
const LOG_LEVEL_PRIORITIES: Record<LogLevel, number> = {
|
||||
trace: 10,
|
||||
debug: 20,
|
||||
info: 30,
|
||||
warn: 40,
|
||||
error: 50,
|
||||
fatal: 60,
|
||||
};
|
||||
|
||||
const DEFAULT_CONSOLE_LEVEL: LogLevel = "info";
|
||||
const DEFAULT_CONSOLE_FORMAT: LogFormat = "pretty";
|
||||
const DEFAULT_FILE_LEVEL: LogLevel = "trace";
|
||||
const DEFAULT_FILE_ROTATE_SIZE = "10m";
|
||||
const DEFAULT_FILE_ROTATE_MAX_FILES = 2;
|
||||
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
|
||||
|
||||
function parseLogLevel(value: string | undefined): LogLevel | undefined {
|
||||
if (!value || !LOG_LEVELS.includes(value as LogLevel)) {
|
||||
return undefined;
|
||||
}
|
||||
return value as LogLevel;
|
||||
}
|
||||
|
||||
function parseLogFormat(value: string | undefined): LogFormat | undefined {
|
||||
if (!value || !LOG_FORMATS.includes(value as LogFormat)) {
|
||||
return undefined;
|
||||
}
|
||||
return value as LogFormat;
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: string | undefined): number | undefined {
|
||||
if (!value || value.trim().length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resolveFilePath(paseoHome: string, configuredPath: string | undefined): string {
|
||||
const fallback = path.join(paseoHome, DEFAULT_DAEMON_LOG_FILENAME);
|
||||
if (!configuredPath) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (path.isAbsolute(configuredPath)) {
|
||||
return configuredPath;
|
||||
}
|
||||
|
||||
return path.resolve(paseoHome, configuredPath);
|
||||
}
|
||||
|
||||
function minLogLevel(levels: LogLevel[]): LogLevel {
|
||||
let minLevel = levels[0];
|
||||
|
||||
for (const level of levels) {
|
||||
if (LOG_LEVEL_PRIORITIES[level] < LOG_LEVEL_PRIORITIES[minLevel]) {
|
||||
minLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
return minLevel;
|
||||
}
|
||||
|
||||
function resolveConfiguredPaseoHome(
|
||||
options: ResolveLogConfigOptions | undefined
|
||||
): string {
|
||||
if (options?.paseoHome) {
|
||||
return options.paseoHome;
|
||||
}
|
||||
return resolvePaseoHome(options?.env ?? process.env);
|
||||
}
|
||||
|
||||
function normalizeLoggerConfigInput(config: LoggerConfigInput): PersistedConfig | undefined {
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ("log" in config) {
|
||||
return config as PersistedConfig;
|
||||
}
|
||||
|
||||
if ("level" in config || "format" in config) {
|
||||
const legacy = config as LegacyLogConfig;
|
||||
return {
|
||||
log: {
|
||||
...(legacy.level ? { level: legacy.level } : {}),
|
||||
...(legacy.format ? { format: legacy.format } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return config as PersistedConfig;
|
||||
}
|
||||
|
||||
function toRotatingFileStreamSize(size: string): string {
|
||||
const trimmed = size.trim();
|
||||
const match = trimmed.match(/^(\d+)\s*([bBkKmMgG])?$/);
|
||||
if (!match) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const value = match[1];
|
||||
const unit = (match[2] ?? "M").toUpperCase();
|
||||
return `${value}${unit}`;
|
||||
}
|
||||
|
||||
export function resolveLogConfig(
|
||||
persistedConfig: PersistedConfig | undefined
|
||||
configInput: LoggerConfigInput,
|
||||
options?: ResolveLogConfigOptions
|
||||
): ResolvedLogConfig {
|
||||
const envLevel = process.env.PASEO_LOG as LogLevel | undefined;
|
||||
const envFormat = process.env.PASEO_LOG_FORMAT as LogFormat | undefined;
|
||||
const persistedConfig = normalizeLoggerConfigInput(configInput);
|
||||
const env = options?.env ?? process.env;
|
||||
const paseoHome = resolveConfiguredPaseoHome(options);
|
||||
const persistedLog = persistedConfig?.log;
|
||||
|
||||
const level: LogLevel =
|
||||
envLevel ?? persistedConfig?.log?.level ?? "debug";
|
||||
const format: LogFormat =
|
||||
envFormat ?? persistedConfig?.log?.format ?? "pretty";
|
||||
const envGlobalLevel = parseLogLevel(env.PASEO_LOG);
|
||||
const persistedGlobalLevel = persistedLog?.level;
|
||||
const consoleLevel: LogLevel =
|
||||
parseLogLevel(env.PASEO_LOG_CONSOLE_LEVEL) ??
|
||||
envGlobalLevel ??
|
||||
persistedLog?.console?.level ??
|
||||
persistedGlobalLevel ??
|
||||
DEFAULT_CONSOLE_LEVEL;
|
||||
|
||||
return { level, format };
|
||||
const fileLevel: LogLevel =
|
||||
parseLogLevel(env.PASEO_LOG_FILE_LEVEL) ??
|
||||
envGlobalLevel ??
|
||||
persistedLog?.file?.level ??
|
||||
persistedGlobalLevel ??
|
||||
DEFAULT_FILE_LEVEL;
|
||||
|
||||
const consoleFormat: LogFormat =
|
||||
parseLogFormat(env.PASEO_LOG_FORMAT) ??
|
||||
persistedLog?.console?.format ??
|
||||
persistedLog?.format ??
|
||||
DEFAULT_CONSOLE_FORMAT;
|
||||
|
||||
const filePath = resolveFilePath(
|
||||
paseoHome,
|
||||
env.PASEO_LOG_FILE_PATH ?? persistedLog?.file?.path
|
||||
);
|
||||
|
||||
const rotateMaxSize =
|
||||
env.PASEO_LOG_FILE_ROTATE_SIZE?.trim() ||
|
||||
persistedLog?.file?.rotate?.maxSize ||
|
||||
DEFAULT_FILE_ROTATE_SIZE;
|
||||
|
||||
const rotateMaxFiles =
|
||||
parsePositiveInteger(env.PASEO_LOG_FILE_ROTATE_COUNT) ??
|
||||
persistedLog?.file?.rotate?.maxFiles ??
|
||||
DEFAULT_FILE_ROTATE_MAX_FILES;
|
||||
|
||||
return {
|
||||
level: minLogLevel([consoleLevel, fileLevel]),
|
||||
console: {
|
||||
level: consoleLevel,
|
||||
format: consoleFormat,
|
||||
},
|
||||
file: {
|
||||
level: fileLevel,
|
||||
path: filePath,
|
||||
rotate: {
|
||||
maxSize: rotateMaxSize,
|
||||
maxFiles: rotateMaxFiles,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createRootLogger(
|
||||
persistedConfig: PersistedConfig | undefined
|
||||
configInput: LoggerConfigInput,
|
||||
options?: ResolveLogConfigOptions
|
||||
): pino.Logger {
|
||||
const config = resolveLogConfig(persistedConfig);
|
||||
const config = resolveLogConfig(configInput, options);
|
||||
|
||||
const transport =
|
||||
config.format === "pretty"
|
||||
? {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
singleLine: true,
|
||||
ignore: "pid,hostname",
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
mkdirSync(path.dirname(config.file.path), { recursive: true });
|
||||
|
||||
return pino({
|
||||
level: config.level,
|
||||
transport,
|
||||
const consoleStream =
|
||||
config.console.format === "pretty"
|
||||
? pretty({
|
||||
colorize: true,
|
||||
singleLine: true,
|
||||
ignore: "pid,hostname",
|
||||
})
|
||||
: pino.destination({ dest: 1, sync: false });
|
||||
|
||||
const fileStream = createRotatingFileStream(path.basename(config.file.path), {
|
||||
path: path.dirname(config.file.path),
|
||||
size: toRotatingFileStreamSize(config.file.rotate.maxSize),
|
||||
maxFiles: config.file.rotate.maxFiles,
|
||||
});
|
||||
|
||||
return pino(
|
||||
{ level: config.level },
|
||||
pino.multistream([
|
||||
{ level: config.console.level, stream: consoleStream },
|
||||
{ level: config.file.level, stream: fileStream },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export function createChildLogger(parent: pino.Logger, name: string): pino.Logger {
|
||||
|
||||
@@ -57,3 +57,53 @@ describe("PersistedConfigSchema agent provider runtime settings", () => {
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PersistedConfigSchema logging config", () => {
|
||||
test("accepts destination-specific logging config", () => {
|
||||
const parsed = PersistedConfigSchema.parse({
|
||||
log: {
|
||||
console: {
|
||||
level: "info",
|
||||
format: "pretty",
|
||||
},
|
||||
file: {
|
||||
level: "trace",
|
||||
path: "daemon.log",
|
||||
rotate: {
|
||||
maxSize: "10m",
|
||||
maxFiles: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.log?.console?.level).toBe("info");
|
||||
expect(parsed.log?.file?.level).toBe("trace");
|
||||
expect(parsed.log?.file?.rotate?.maxFiles).toBe(2);
|
||||
});
|
||||
|
||||
test("accepts legacy logging config fields", () => {
|
||||
const parsed = PersistedConfigSchema.parse({
|
||||
log: {
|
||||
level: "debug",
|
||||
format: "json",
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.log?.level).toBe("debug");
|
||||
expect(parsed.log?.format).toBe("json");
|
||||
});
|
||||
|
||||
test("rejects unknown logging config fields", () => {
|
||||
const result = PersistedConfigSchema.safeParse({
|
||||
log: {
|
||||
console: {
|
||||
level: "info",
|
||||
color: "red",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,37 @@ import { z } from "zod";
|
||||
import { AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
|
||||
import { AgentProviderRuntimeSettingsMapSchema } from "./agent/provider-launch-config.js";
|
||||
|
||||
const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
||||
const LogFormatSchema = z.enum(["pretty", "json"]);
|
||||
|
||||
const LogConfigSchema = z
|
||||
.object({
|
||||
level: z
|
||||
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
||||
// Legacy global log settings (kept for backwards compatibility).
|
||||
level: LogLevelSchema.optional(),
|
||||
format: LogFormatSchema.optional(),
|
||||
|
||||
console: z
|
||||
.object({
|
||||
level: LogLevelSchema.optional(),
|
||||
format: LogFormatSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
file: z
|
||||
.object({
|
||||
level: LogLevelSchema.optional(),
|
||||
path: z.string().min(1).optional(),
|
||||
rotate: z
|
||||
.object({
|
||||
maxSize: z.string().min(1).optional(),
|
||||
maxFiles: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
format: z.enum(["pretty", "json"]).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user