1
0

feat: 运行时日志系统,Pino + pino-pretty + pino-roll,console/file 双输出,敏感信息 redaction

This commit is contained in:
2026-05-21 12:21:59 +08:00
parent 0d709c7681
commit 007d74934d
26 changed files with 1713 additions and 114 deletions

View File

@@ -2,13 +2,22 @@ import { isNumber, isPlainObject, isString } from "es-toolkit";
import { dirname, resolve } from "node:path";
import type { ConfigValidationIssue } from "./schema/issues";
import type { DefaultsConfig, EngineRuntimeConfig, RawTargetConfig, ResolvedTargetBase } from "./types";
import type {
DefaultsConfig,
EngineRuntimeConfig,
LoggingConfig,
LogLevel,
RawTargetConfig,
ResolvedLoggingConfig,
ResolvedTargetBase,
RotationFrequency,
} from "./types";
import { checkerRegistry } from "./runner";
import { issue, throwConfigIssues } from "./schema/issues";
import { asValidatedConfig, type RawProbeConfig } from "./schema/types";
import { validateProbeConfigContract } from "./schema/validate";
import { parseDuration } from "./utils";
import { parseDuration, parseSize } from "./utils";
import { resolveVariables } from "./variables";
const DEFAULT_HOST = "127.0.0.1";
@@ -18,11 +27,19 @@ const DEFAULT_INTERVAL = "30s";
const DEFAULT_TIMEOUT = "10s";
const DEFAULT_MAX_CONCURRENT_CHECKS = 20;
const DEFAULT_RETENTION = "7d";
const DEFAULT_LOG_LEVEL: LogLevel = "info";
const DEFAULT_ROTATION_SIZE = "50MB";
const DEFAULT_ROTATION_FREQUENCY: RotationFrequency = "daily";
const DEFAULT_ROTATION_MAX_FILES = 14;
const VALID_LOG_LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"];
const VALID_ROTATION_FREQUENCIES: RotationFrequency[] = ["hourly", "daily", "weekly"];
export interface ResolvedConfig {
configDir: string;
dataDir: string;
host: string;
logging: ResolvedLoggingConfig;
maxConcurrentChecks: number;
port: number;
retentionMs: number;
@@ -80,7 +97,10 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
const maxConcurrentChecks = resolveMaxConcurrentChecks(runtime);
const retentionMs = resolveRetention(runtime);
const logging = resolveLogging(validated.logging ?? {}, dataDir, configDir);
const allRuntimeIssues = [...allIssues];
validateLoggingConfig(validated.logging, allRuntimeIssues);
if (allRuntimeIssues.length > 0) {
throwConfigIssues(dedupeIssues(allRuntimeIssues));
}
@@ -92,7 +112,7 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir),
);
return { configDir, dataDir, host, maxConcurrentChecks, port, retentionMs, targets };
return { configDir, dataDir, host, logging, maxConcurrentChecks, port, retentionMs, targets };
}
function canRunSemanticValidation(value: unknown): boolean {
@@ -113,6 +133,45 @@ function dedupeIssues(issues: ConfigValidationIssue[]): ConfigValidationIssue[]
export { parseDuration } from "./utils";
function isAbsolute(p: string): boolean {
return p.startsWith("/") || /^[A-Za-z]:/.test(p);
}
function resolveLogging(logging: LoggingConfig, dataDir: string, configDir: string): ResolvedLoggingConfig {
const globalLevel = resolveLogLevel(logging.level, DEFAULT_LOG_LEVEL);
const consoleLevel = resolveLogLevel(logging.console?.level, globalLevel);
const fileLevel = resolveLogLevel(logging.file?.level, globalLevel);
const rawPath = logging.file?.path;
const filePath = rawPath
? isAbsolute(rawPath)
? rawPath
: resolve(configDir, rawPath)
: resolve(dataDir, "logs/dial.log");
const rotationRaw = logging.file?.rotation;
const rotationSizeRaw = rotationRaw?.size ?? DEFAULT_ROTATION_SIZE;
const rotationSizeBytes = parseSize(rotationSizeRaw);
const rotationFrequency = rotationRaw?.frequency ?? DEFAULT_ROTATION_FREQUENCY;
const rotationMaxFiles = rotationRaw?.maxFiles ?? DEFAULT_ROTATION_MAX_FILES;
return {
consoleLevel,
fileLevel,
filePath,
rotationFrequency,
rotationMaxFiles,
rotationSizeBytes,
rotationSizeRaw,
};
}
function resolveLogLevel(level: unknown, fallback: LogLevel): LogLevel {
if (!isString(level)) return fallback;
if (VALID_LOG_LEVELS.includes(level as LogLevel)) return level as LogLevel;
return fallback;
}
function resolveMaxConcurrentChecks(runtime: EngineRuntimeConfig): number {
if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
if (
@@ -263,3 +322,69 @@ function validateDurationValue(
issues.push(issue("invalid-duration", path, error instanceof Error ? error.message : "时长格式不合法", targetName));
}
}
function validateLoggingConfig(logging: LoggingConfig | undefined, issues: ConfigValidationIssue[]): void {
if (logging === undefined) return;
if (logging.level !== undefined && !VALID_LOG_LEVELS.includes(logging.level)) {
issues.push(
issue("invalid-value", "logging.level", `日志等级非法: "${logging.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`),
);
}
if (logging.console?.level !== undefined && !VALID_LOG_LEVELS.includes(logging.console.level)) {
issues.push(
issue(
"invalid-value",
"logging.console.level",
`日志等级非法: "${logging.console.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
),
);
}
if (logging.file?.level !== undefined && !VALID_LOG_LEVELS.includes(logging.file.level)) {
issues.push(
issue(
"invalid-value",
"logging.file.level",
`日志等级非法: "${logging.file.level}",支持: ${VALID_LOG_LEVELS.join(", ")}`,
),
);
}
if (logging.file?.path !== undefined) {
if (!isString(logging.file.path) || logging.file.path.trim() === "") {
issues.push(issue("invalid-value", "logging.file.path", "日志路径不能为空字符串或空白字符串"));
}
}
const rotation = logging.file?.rotation;
if (rotation?.size !== undefined) {
try {
const bytes = parseSize(rotation.size);
if (bytes <= 0) {
issues.push(issue("invalid-value", "logging.file.rotation.size", "滚动大小必须为正整数字节数"));
}
} catch (error) {
issues.push(
issue("invalid-value", "logging.file.rotation.size", error instanceof Error ? error.message : "size 格式非法"),
);
}
}
if (rotation?.frequency !== undefined && !VALID_ROTATION_FREQUENCIES.includes(rotation.frequency)) {
issues.push(
issue(
"invalid-value",
"logging.file.rotation.frequency",
`滚动频率非法: "${rotation.frequency}",支持: ${VALID_ROTATION_FREQUENCIES.join(", ")}`,
),
);
}
if (rotation?.maxFiles !== undefined) {
if (!isNumber(rotation.maxFiles) || !Number.isInteger(rotation.maxFiles) || rotation.maxFiles <= 0) {
issues.push(issue("invalid-value", "logging.file.rotation.maxFiles", "maxFiles 必须为正整数"));
}
}
}