mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
* fix(minimax): switch auth from x-api-key to Authorization Bearer (#1076) Integrated into release/v3.5.6 — MiniMax auth fix with authHeader consistency normalization * feat(CI,i18n): autogenerate language files + Add missing strings (#1071) Integrated into release/v3.5.6 — i18n translations for memory, skills, and missing keys across 31 languages * fix(ci): restore i18n continue-on-error, remove auto-commit race condition * fix(husky): load nvm in hooks for VS Code compatibility * fix(husky): gracefully skip hooks when npm is not in PATH * fix: convert OpenAI function tool_choice to Claude tool format (#1072) * fix: prevent EPIPE feedback loop filling logs at GB/s (#1006) * fix: fallback to native fetch when undici dispatcher fails (#1054) * fix: improve Qoder PAT validation with actionable error messages (#966) - Add QODER_PERSONAL_ACCESS_TOKEN env var fallback for both validation and execution - Pre-flight ping check to diagnose connectivity issues (Docker/proxy) - Detect encrypted auth blobs from ~/.qoder/.auth/user and guide to website PAT - Clear error messages for auth failures with link to integrations page - Treat non-auth 4xx as auth-pass (request format issue, not token issue) - Update tests to cover new validation paths (23 tests, all passing) * feat: Improve the Chinese translation (#1079) Integrated into release/v3.5.6 * chore(release): v3.5.6 — i18n updates and credential security fixes * fix(ci): resolve e2e and docs-sync pipeline failures * fix(security): bump next to 16.2.3 to resolve SNYK-JS-NEXT-15954202 * fix: guard Memory/Cache UI against null toLocaleString crash (#1083) * fix: translate OpenAI tool_choice type 'function' to Claude 'tool' format (#1072) * fix: pass custom baseUrl in provider API key validation (#1078) * docs: update CHANGELOG with v3.5.6 bug fixes and security patches * docs: rewrite implement-features workflow with 5-phase harvest-research-report-plan-execute pipeline * docs: organize _ideia/ into viable/defer/notfit + add Phase 2.5 auto-response workflow * docs: implementation plans for #1025, #750, #960, #1046 + close already-implemented #833, #973, #982 * feat: mask email addresses in dashboard for privacy (#1025) * feat: add OpenRouter and GitHub to embedding/image provider registries (#960) * feat: add model visibility toggle and search filter to provider page (#750) * docs: move implemented features to notfit, update task plans status * chore: untrack _ideia/ and _tasks/ from git — private/internal only * chore(release): bump to v3.5.6 — changelog, docs, version sync & any-budget fix * fix: remove explicit .ts extension in qoderCli import that caused 500 error in production build --------- Co-authored-by: Jean Brito <jeanfbrito@gmail.com> Co-authored-by: zenobit <zenobit@disroot.org> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Ethan Hunt <136065060+only4copilot@users.noreply.github.com>
193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
/**
|
|
* Structured Logger — FASE-05 Code Quality
|
|
*
|
|
* Lightweight structured logging wrapper with JSON output for production
|
|
* and human-readable output for development. Replaces scattered console.log
|
|
* calls with consistent, parseable log entries.
|
|
*
|
|
* When APP_LOG_TO_FILE is enabled, log entries are also appended as JSON lines
|
|
* to the application log file for the Console Log Viewer.
|
|
*
|
|
* @module shared/utils/structuredLogger
|
|
*/
|
|
|
|
import { getCorrelationId } from "../middleware/correlationId";
|
|
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
|
import { dirname, resolve } from "path";
|
|
import { getAppLogFilePath, getAppLogLevel, getAppLogToFile } from "@/lib/logEnv";
|
|
|
|
const LOG_LEVELS: Record<string, number> = {
|
|
debug: 10,
|
|
info: 20,
|
|
warn: 30,
|
|
error: 40,
|
|
fatal: 50,
|
|
};
|
|
|
|
const currentLevel = LOG_LEVELS[getAppLogLevel("info").toLowerCase() || ""] || LOG_LEVELS.info;
|
|
const isProduction = process.env.NODE_ENV === "production";
|
|
|
|
// File logging configuration
|
|
const logToFile = getAppLogToFile();
|
|
const logFilePath = resolve(getAppLogFilePath());
|
|
|
|
// Ensure log directory exists once at module load
|
|
if (logToFile) {
|
|
try {
|
|
const dir = dirname(logFilePath);
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
} catch {
|
|
// silently ignore — will retry on each write
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append a JSON log line to the log file (non-blocking best-effort).
|
|
*/
|
|
function writeToFile(entry: Record<string, unknown>) {
|
|
if (!logToFile) return;
|
|
try {
|
|
appendFileSync(logFilePath, JSON.stringify(entry) + "\n");
|
|
} catch {
|
|
// Silently fail — file logging should never break the app
|
|
}
|
|
}
|
|
|
|
function formatEntry(
|
|
level: string,
|
|
component: string,
|
|
message: string,
|
|
meta?: Record<string, unknown>
|
|
) {
|
|
const entry: Record<string, unknown> = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
component,
|
|
message,
|
|
...meta,
|
|
};
|
|
|
|
// Add correlation ID if available
|
|
const correlationId = getCorrelationId() as string | undefined;
|
|
if (correlationId) {
|
|
entry.correlationId = correlationId;
|
|
}
|
|
|
|
if (isProduction) {
|
|
return JSON.stringify(entry);
|
|
}
|
|
|
|
// Human-readable for development
|
|
const metaStr = meta && Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
|
|
const corrStr = correlationId ? ` [${correlationId.slice(0, 8)}]` : "";
|
|
return `[${entry.timestamp}] ${level.toUpperCase().padEnd(5)} [${component}]${corrStr} ${message}${metaStr}`;
|
|
}
|
|
|
|
function buildEntry(
|
|
level: string,
|
|
component: string,
|
|
message: string,
|
|
meta?: Record<string, unknown>
|
|
) {
|
|
const entry: Record<string, unknown> = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
component,
|
|
message,
|
|
...meta,
|
|
};
|
|
const correlationId = getCorrelationId() as string | undefined;
|
|
if (correlationId) {
|
|
entry.correlationId = correlationId;
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
// EPIPE-safe error deduplication + rate limiting (#1006)
|
|
const _recentErrors = new Map<string, { count: number; firstSeen: number }>();
|
|
const DEDUP_WINDOW_MS = 5_000;
|
|
const MAX_WRITES_PER_SECOND = 50;
|
|
let _writeCount = 0;
|
|
let _writeWindowStart = Date.now();
|
|
|
|
function shouldSuppressError(message: string): boolean {
|
|
const now = Date.now();
|
|
|
|
// Rate limit: max writes per second
|
|
if (now - _writeWindowStart > 1000) {
|
|
_writeCount = 0;
|
|
_writeWindowStart = now;
|
|
}
|
|
if (_writeCount >= MAX_WRITES_PER_SECOND) return true;
|
|
|
|
// Dedup: suppress identical messages within window
|
|
const existing = _recentErrors.get(message);
|
|
if (existing && now - existing.firstSeen < DEDUP_WINDOW_MS) {
|
|
existing.count++;
|
|
return true;
|
|
}
|
|
|
|
// Cleanup old entries
|
|
if (_recentErrors.size > 100) {
|
|
for (const [key, entry] of _recentErrors) {
|
|
if (now - entry.firstSeen > DEDUP_WINDOW_MS) _recentErrors.delete(key);
|
|
}
|
|
}
|
|
|
|
_recentErrors.set(message, { count: 1, firstSeen: now });
|
|
_writeCount++;
|
|
return false;
|
|
}
|
|
|
|
export function createLogger(component: string) {
|
|
return {
|
|
debug(message: string, meta?: Record<string, unknown>) {
|
|
if (currentLevel <= LOG_LEVELS.debug) {
|
|
const entry = buildEntry("debug", component, message, meta);
|
|
console.debug(formatEntry("debug", component, message, meta));
|
|
writeToFile(entry);
|
|
}
|
|
},
|
|
info(message: string, meta?: Record<string, unknown>) {
|
|
if (currentLevel <= LOG_LEVELS.info) {
|
|
const entry = buildEntry("info", component, message, meta);
|
|
console.info(formatEntry("info", component, message, meta));
|
|
writeToFile(entry);
|
|
}
|
|
},
|
|
warn(message: string, meta?: Record<string, unknown>) {
|
|
if (currentLevel <= LOG_LEVELS.warn) {
|
|
const entry = buildEntry("warn", component, message, meta);
|
|
console.warn(formatEntry("warn", component, message, meta));
|
|
writeToFile(entry);
|
|
}
|
|
},
|
|
error(message: string, meta?: Record<string, unknown>) {
|
|
if (currentLevel <= LOG_LEVELS.error) {
|
|
if (shouldSuppressError(message)) return;
|
|
const entry = buildEntry("error", component, message, meta);
|
|
// Use stderr.write to avoid Next.js console patching that triggers EPIPE loops
|
|
try {
|
|
process.stderr.write(formatEntry("error", component, message, meta) + "\n");
|
|
} catch {}
|
|
writeToFile(entry);
|
|
}
|
|
},
|
|
fatal(message: string, meta?: Record<string, unknown>) {
|
|
if (shouldSuppressError(message)) return;
|
|
const entry = buildEntry("fatal", component, message, meta);
|
|
try {
|
|
process.stderr.write(formatEntry("fatal", component, message, meta) + "\n");
|
|
} catch {}
|
|
writeToFile(entry);
|
|
},
|
|
child(defaultMeta: Record<string, unknown>) {
|
|
return createLogger(component);
|
|
},
|
|
};
|
|
}
|
|
|
|
export { LOG_LEVELS };
|