refactor: migrate to winston and enable daily rotated app logs (#71)
This commit is contained in:
17 files changed
+414
-92
No files matched your search
+1
-1
@@ -15,7 +15,7 @@ function getQuickConfig() {
|
||||
return config.error_reporting_enabled;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to read config for Sentry init:', e);
|
||||
logger.error('Failed to read config for Sentry init:', e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from './handler';
|
||||
import { CloudAccountSchema } from '../../types/cloudAccount';
|
||||
import { CloudAccountRepo } from '../database/cloudHandler';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
export const cloudRouter = os.router({
|
||||
addGoogleAccount: os
|
||||
@@ -72,7 +73,7 @@ export const cloudRouter = os.router({
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('[ORPC] syncLocalAccount error:', error.message, error.stack);
|
||||
logger.error('[ORPC] syncLocalAccount error:', error.message, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { bootstrapNestServer, stopNestServer, getNestServerStatus } from '../../server/main';
|
||||
import { ConfigManager } from '../config/manager';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/**
|
||||
* Start the gateway server (NestJS)
|
||||
@@ -20,7 +21,7 @@ export const startGateway = async (port: number): Promise<boolean> => {
|
||||
|
||||
return await bootstrapNestServer(proxyConfig);
|
||||
} catch (e) {
|
||||
console.error('Failed to start gateway:', e);
|
||||
logger.error('Failed to start gateway:', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -32,7 +33,7 @@ export const stopGateway = async (): Promise<boolean> => {
|
||||
try {
|
||||
return await stopNestServer();
|
||||
} catch (e) {
|
||||
console.error('Failed to stop gateway:', e);
|
||||
logger.error('Failed to stop gateway:', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ try {
|
||||
logger.info(`Database router check: ${!!db}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error logging router:', e);
|
||||
logger.error('Error logging router:', e);
|
||||
}
|
||||
|
||||
export const rpcHandler: RPCHandler<Record<never, never>> = new RPCHandler(router);
|
||||
+2
-1
@@ -11,6 +11,7 @@ import { os } from '@orpc/server';
|
||||
import { z } from 'zod';
|
||||
import { isProcessRunning, closeAntigravity, startAntigravity } from './process/handler';
|
||||
import { systemHandler } from './system/handler';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Log middleware setup
|
||||
const logMiddleware = os.middleware(async (opts: any) => {
|
||||
@@ -21,7 +22,7 @@ const logMiddleware = os.middleware(async (opts: any) => {
|
||||
const result = await next({});
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`[ORPC] Error in handler for ${JSON.stringify(requestPath)}:`, err);
|
||||
logger.error(`[ORPC] Error in handler for ${JSON.stringify(requestPath)}:`, err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { mapClaudeModelToGemini } from './ModelMapping';
|
||||
import { cleanJsonSchema } from './JsonSchemaUtils';
|
||||
import { SignatureStore } from './SignatureStore';
|
||||
import { logger } from '../../utils/logger';
|
||||
import {
|
||||
ClaudeRequest,
|
||||
Message,
|
||||
@@ -378,7 +379,7 @@ function injectGoogleSearchTool(body: { tools?: GeminiToolDeclaration[] }) {
|
||||
|
||||
const hasFunctions = toolsArr.some((t) => t.functionDeclarations);
|
||||
if (hasFunctions) {
|
||||
console.info('Skipping googleSearch injection due to existing functionDeclarations');
|
||||
logger.info('Skipping googleSearch injection due to existing functionDeclarations');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GeminiPart, Usage, UsageMetadata } from './types';
|
||||
import { SignatureStore } from './SignatureStore';
|
||||
import { decodeSignature } from './signature-utils';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
type BlockType = 'None' | 'Text' | 'Thinking' | 'Function';
|
||||
|
||||
@@ -249,7 +250,7 @@ export class StreamingState {
|
||||
const chunks: string[] = [];
|
||||
this.parseErrorCount++;
|
||||
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[SSE-Parser] Parse error #${this.parseErrorCount}. Raw data length: ${rawData.length}`,
|
||||
);
|
||||
|
||||
@@ -260,7 +261,7 @@ export class StreamingState {
|
||||
|
||||
// Emit error event if too many errors
|
||||
if (this.parseErrorCount > 3) {
|
||||
console.error(
|
||||
logger.error(
|
||||
`[SSE-Parser] High error rate (${this.parseErrorCount} errors). Stream may be corrupted.`,
|
||||
);
|
||||
chunks.push(
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
const logger = {
|
||||
log: (msg: string) => console.log(msg),
|
||||
warn: (msg: string) => console.warn(msg),
|
||||
};
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const CLAUDE_TO_GEMINI: Record<string, string> = {
|
||||
// Directly supported models
|
||||
@@ -78,7 +75,7 @@ export function resolveModelRoute(
|
||||
): string {
|
||||
// 1. Check custom exact mapping (Highest priority)
|
||||
if (customMapping[originalModel]) {
|
||||
logger.log(
|
||||
logger.info(
|
||||
`[Router] Using custom exact mapping: ${originalModel} -> ${customMapping[originalModel]}`,
|
||||
);
|
||||
return customMapping[originalModel];
|
||||
@@ -98,7 +95,7 @@ export function resolveModelRoute(
|
||||
lowerModel === 'gpt-4'
|
||||
) {
|
||||
if (openaiMapping['gpt-4-series']) {
|
||||
logger.log(
|
||||
logger.info(
|
||||
`[Router] Using GPT-4 series mapping: ${originalModel} -> ${openaiMapping['gpt-4-series']}`,
|
||||
);
|
||||
return openaiMapping['gpt-4-series'];
|
||||
@@ -113,7 +110,7 @@ export function resolveModelRoute(
|
||||
lowerModel.includes('turbo')
|
||||
) {
|
||||
if (openaiMapping['gpt-4o-series']) {
|
||||
logger.log(
|
||||
logger.info(
|
||||
`[Router] Using GPT-4o/3.5 series mapping: ${originalModel} -> ${openaiMapping['gpt-4o-series']}`,
|
||||
);
|
||||
return openaiMapping['gpt-4o-series'];
|
||||
@@ -124,13 +121,13 @@ export function resolveModelRoute(
|
||||
if (lowerModel.startsWith('gpt-5')) {
|
||||
// Prefer gpt-5-series mapping, fallback to gpt-4-series if missing
|
||||
if (openaiMapping['gpt-5-series']) {
|
||||
logger.log(
|
||||
logger.info(
|
||||
`[Router] Using GPT-5 series mapping: ${originalModel} -> ${openaiMapping['gpt-5-series']}`,
|
||||
);
|
||||
return openaiMapping['gpt-5-series'];
|
||||
}
|
||||
if (openaiMapping['gpt-4-series']) {
|
||||
logger.log(
|
||||
logger.info(
|
||||
`[Router] Using GPT-4 series mapping (GPT-5 fallback): ${originalModel} -> ${openaiMapping['gpt-4-series']}`,
|
||||
);
|
||||
return openaiMapping['gpt-4-series'];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios, { AxiosError, AxiosInstance } from 'axios';
|
||||
import { QuotaData, Tier, LoadProjectResponse, QuotaApiResponse } from './types';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
// Constants
|
||||
const QUOTA_API_URL = 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels';
|
||||
@@ -48,15 +49,15 @@ export class QuotaService {
|
||||
const subscriptionTier = data.paidTier?.id || data.currentTier?.id;
|
||||
|
||||
if (subscriptionTier) {
|
||||
console.log(`📊 [${email}] Subscription Identified: ${subscriptionTier}`);
|
||||
logger.info(`📊 [${email}] Subscription Identified: ${subscriptionTier}`);
|
||||
}
|
||||
|
||||
return [projectId, subscriptionTier];
|
||||
} else {
|
||||
console.warn(`⚠️ [${email}] loadCodeAssist failed: Status: ${res.status}`);
|
||||
logger.warn(`⚠️ [${email}] loadCodeAssist failed: Status: ${res.status}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ [${email}] loadCodeAssist Network Error: ${error.message}`);
|
||||
logger.error(`❌ [${email}] loadCodeAssist Network Error: ${error.message}`);
|
||||
}
|
||||
|
||||
return [undefined, undefined];
|
||||
@@ -86,7 +87,7 @@ export class QuotaService {
|
||||
const url = QUOTA_API_URL;
|
||||
const maxRetries = 3;
|
||||
|
||||
console.log(`Sending quota request to ${url}`);
|
||||
logger.info(`Sending quota request to ${url}`);
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
@@ -104,11 +105,11 @@ export class QuotaService {
|
||||
subscriptionTier: subscriptionTier,
|
||||
};
|
||||
|
||||
console.log(`Quota API returned ${Object.keys(quotaResponse.models || {}).length} models:`);
|
||||
logger.info(`Quota API returned ${Object.keys(quotaResponse.models || {}).length} models:`);
|
||||
|
||||
if (quotaResponse.models) {
|
||||
for (const [name, info] of Object.entries(quotaResponse.models)) {
|
||||
console.log(` - ${name}`);
|
||||
logger.info(` - ${name}`);
|
||||
if (info.quotaInfo) {
|
||||
const fraction = info.quotaInfo.remainingFraction ?? 0;
|
||||
const percentage = Math.floor(fraction * 100);
|
||||
@@ -135,7 +136,7 @@ export class QuotaService {
|
||||
|
||||
// ✅ Handle 403 Forbidden specifically - return immediately, do not retry
|
||||
if (status === 403) {
|
||||
console.warn(`Account no permission (403 Forbidden), marked as forbidden`);
|
||||
logger.warn(`Account no permission (403 Forbidden), marked as forbidden`);
|
||||
return {
|
||||
quotaData: {
|
||||
models: {},
|
||||
@@ -146,9 +147,9 @@ export class QuotaService {
|
||||
};
|
||||
}
|
||||
|
||||
console.warn(`API Error: ${status} - ${text} (Attempt ${attempt}/${maxRetries})`);
|
||||
logger.warn(`API Error: ${status} - ${text} (Attempt ${attempt}/${maxRetries})`);
|
||||
} else {
|
||||
console.warn(`Request Failed: ${error.message} (Attempt ${attempt}/${maxRetries})`);
|
||||
logger.warn(`Request Failed: ${error.message} (Attempt ${attempt}/${maxRetries})`);
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Global thought_signature storage shared by all endpoints
|
||||
* Used to capture and replay signatures for Gemini 3+ function calls when clients don't pass them back.
|
||||
*/
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
class SignatureStoreImpl {
|
||||
private static instance: SignatureStoreImpl;
|
||||
@@ -28,12 +29,12 @@ class SignatureStoreImpl {
|
||||
const newLen = sig.length;
|
||||
|
||||
if (newLen > existingLen) {
|
||||
console.log(
|
||||
logger.info(
|
||||
`[ThoughtSig] Storing new signature (length: ${newLen}, replacing old: ${existingLen})`,
|
||||
);
|
||||
this.signature = sig;
|
||||
} else {
|
||||
console.debug(
|
||||
logger.debug(
|
||||
`[ThoughtSig] Skipping shorter signature (new length: ${newLen}, existing: ${existingLen})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* Problem: Invalid signatures cause thinking blocks to be dropped, losing content
|
||||
* Solution: Validate and convert invalid blocks to text instead of dropping
|
||||
*/
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
/** Minimum length for a valid thought signature */
|
||||
const MIN_SIGNATURE_LENGTH = 10;
|
||||
@@ -82,7 +83,7 @@ export function gracefulDegradeThinking(block: ContentBlock): ContentBlock | nul
|
||||
// Invalid signature - check if we have content to preserve
|
||||
if (thinkingBlock.thinking && thinkingBlock.thinking.length > 0) {
|
||||
// Convert to text block to preserve content
|
||||
console.info(
|
||||
logger.info(
|
||||
`[Thinking-Utils] Converting invalid thinking block to text. Content length: ${thinkingBlock.thinking.length}`,
|
||||
);
|
||||
return {
|
||||
@@ -92,7 +93,7 @@ export function gracefulDegradeThinking(block: ContentBlock): ContentBlock | nul
|
||||
}
|
||||
|
||||
// Empty content + invalid signature = drop
|
||||
console.debug('[Thinking-Utils] Dropping empty thinking block with invalid signature');
|
||||
logger.debug('[Thinking-Utils] Dropping empty thinking block with invalid signature');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ function logPacket(data: any) {
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(e.message);
|
||||
logger.error('Failed to append ORPC packet log', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ async function setupORPC() {
|
||||
|
||||
logPacket(data);
|
||||
} catch (e) {
|
||||
console.log('[RAW ORPC MSG] (unparseable)', msgEvent.data);
|
||||
logger.debug('[RAW ORPC MSG] (unparseable)', msgEvent.data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ConfigManager } from '../ipc/config/manager';
|
||||
import { ProxyAgent } from 'undici';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// --- Constants & Config ---
|
||||
|
||||
@@ -85,7 +86,7 @@ export class GoogleAPIService {
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback or log if config load fails (shouldn't happen usually)
|
||||
console.warn('[GoogleAPIService] Failed to load proxy config', e);
|
||||
logger.warn('[GoogleAPIService] Failed to load proxy config', e);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -246,7 +247,7 @@ export class GoogleAPIService {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[GoogleAPIService] Failed to fetch project ID (Attempt ${i + 1}):`, e);
|
||||
logger.warn(`[GoogleAPIService] Failed to fetch project ID (Attempt ${i + 1}):`, e);
|
||||
await new Promise((r) => setTimeout(r, 500)); // Sleep 500ms
|
||||
}
|
||||
}
|
||||
@@ -296,7 +297,7 @@ export class GoogleAPIService {
|
||||
}
|
||||
|
||||
const errorMsg = `HTTP ${status} - ${text}`;
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[GoogleAPIService] API Error: ${errorMsg} (Attempt ${attempt}/${maxRetries})`,
|
||||
);
|
||||
|
||||
@@ -327,7 +328,7 @@ export class GoogleAPIService {
|
||||
return result;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
`[GoogleAPIService] Request failed: ${e.message} (Attempt ${attempt}/${maxRetries})`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { logger } from '../../utils/logger';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -12,40 +12,65 @@ vi.mock('../../utils/paths', async () => {
|
||||
|
||||
describe('Logger Utilities', () => {
|
||||
const testLogDir = path.join(process.cwd(), 'temp_test_logs');
|
||||
const testLogFile = path.join(testLogDir, 'app.log');
|
||||
|
||||
const getLatestLogFile = () => {
|
||||
const files = fs
|
||||
.readdirSync(testLogDir)
|
||||
.filter((file) => /^app-\d{4}-\d{2}-\d{2}(\.\d+)?\.log$/.test(file))
|
||||
.sort();
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return path.join(testLogDir, files[files.length - 1]);
|
||||
};
|
||||
|
||||
const waitForLogContains = async (text: string) => {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const filePath = getLatestLogFile();
|
||||
if (filePath && fs.existsSync(filePath)) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
if (content.includes(text)) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
console.log('Test Log Dir:', testLogDir);
|
||||
if (fs.existsSync(testLogDir)) {
|
||||
fs.rmSync(testLogDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(testLogDir, { recursive: true });
|
||||
console.log('Dir created:', fs.existsSync(testLogDir));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fs.existsSync(testLogDir)) {
|
||||
fs.rmSync(testLogDir, { recursive: true, force: true });
|
||||
if (!fs.existsSync(testLogDir)) {
|
||||
fs.mkdirSync(testLogDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should create log file if it does not exist', () => {
|
||||
logger.info('Test message');
|
||||
expect(fs.existsSync(testLogFile)).toBe(true);
|
||||
it('should create rotated log file', async () => {
|
||||
const message = 'Test message';
|
||||
logger.info(message);
|
||||
const filePath = await waitForLogContains(message);
|
||||
expect(filePath).not.toBeNull();
|
||||
expect(fs.existsSync(filePath as string)).toBe(true);
|
||||
});
|
||||
|
||||
it('should write formatted message to file', () => {
|
||||
it('should write formatted message to file', async () => {
|
||||
const message = 'Test info message';
|
||||
logger.info(message);
|
||||
const content = fs.readFileSync(testLogFile, 'utf-8');
|
||||
const filePath = await waitForLogContains(message);
|
||||
expect(filePath).not.toBeNull();
|
||||
const content = fs.readFileSync(filePath as string, 'utf-8');
|
||||
expect(content).toContain('[INFO]');
|
||||
expect(content).toContain(message);
|
||||
});
|
||||
|
||||
it('should log error messages', () => {
|
||||
it('should log error messages', async () => {
|
||||
const message = 'Test error message';
|
||||
logger.error(message);
|
||||
const content = fs.readFileSync(testLogFile, 'utf-8');
|
||||
const filePath = await waitForLogContains(message);
|
||||
expect(filePath).not.toBeNull();
|
||||
const content = fs.readFileSync(filePath as string, 'utf-8');
|
||||
expect(content).toContain('[ERROR]');
|
||||
expect(content).toContain(message);
|
||||
});
|
||||
|
||||
+62
-32
@@ -1,11 +1,15 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import winston from 'winston';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { getAgentDir } from './paths';
|
||||
|
||||
export type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
||||
|
||||
const LOG_WINDOW_MS = 30_000;
|
||||
const MAX_LOG_ENTRIES = 200;
|
||||
const LOG_RETENTION = '30d';
|
||||
const LOG_MAX_SIZE = '10m';
|
||||
|
||||
interface LogEntry {
|
||||
timestamp: number;
|
||||
@@ -49,13 +53,14 @@ function safeStringify(obj: unknown): string {
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private logFilePath: string;
|
||||
private winstonLogger: winston.Logger;
|
||||
private recentLogs: LogEntry[] = [];
|
||||
private sentryReporter: SentryReporter | null = null;
|
||||
private sentryEnabled = false;
|
||||
|
||||
constructor() {
|
||||
const agentDir = getAgentDir();
|
||||
|
||||
if (!fs.existsSync(agentDir)) {
|
||||
try {
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
@@ -63,36 +68,50 @@ class Logger {
|
||||
console.error('Failed to create agent directory for logs', e);
|
||||
}
|
||||
}
|
||||
this.logFilePath = path.join(agentDir, 'app.log');
|
||||
}
|
||||
|
||||
private formatMessage(level: LogLevel, message: string, ...args: unknown[]): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
const formattedArgs = args
|
||||
.map((arg) => (typeof arg === 'object' ? safeStringify(arg) : String(arg)))
|
||||
.join(' ');
|
||||
return `[${timestamp}] [${level.toUpperCase()}] ${message} ${formattedArgs}`;
|
||||
}
|
||||
const fileFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.printf(({ timestamp, level, message }) => {
|
||||
return `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
||||
}),
|
||||
);
|
||||
|
||||
private writeToFile(formattedMessage: string) {
|
||||
try {
|
||||
fs.appendFileSync(this.logFilePath, formattedMessage + '\n');
|
||||
} catch (e) {
|
||||
console.error('Failed to write to log file', e);
|
||||
}
|
||||
}
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(({ level, message }) => {
|
||||
return `[${level.toUpperCase()}] ${message}`;
|
||||
}),
|
||||
);
|
||||
|
||||
private writeToConsole(level: LogLevel, message: string, ...args: unknown[]) {
|
||||
const colorMap: Record<LogLevel, string> = {
|
||||
info: '\x1b[36m', // Cyan
|
||||
warn: '\x1b[33m', // Yellow
|
||||
error: '\x1b[31m', // Red
|
||||
debug: '\x1b[90m', // Gray
|
||||
};
|
||||
const reset = '\x1b[0m';
|
||||
const color = colorMap[level];
|
||||
const rotateTransport = new DailyRotateFile({
|
||||
filename: path.join(agentDir, 'app-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: LOG_MAX_SIZE,
|
||||
maxFiles: LOG_RETENTION,
|
||||
zippedArchive: false,
|
||||
auditFile: path.join(agentDir, '.app-log-audit.json'),
|
||||
level: 'debug',
|
||||
format: fileFormat,
|
||||
});
|
||||
|
||||
console.log(`${color}[${level.toUpperCase()}]${reset} ${message}`, ...args);
|
||||
rotateTransport.on('error', (error) => {
|
||||
console.error('DailyRotateFile transport error', error);
|
||||
});
|
||||
|
||||
const consoleTransport = new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: consoleFormat,
|
||||
});
|
||||
|
||||
consoleTransport.on('error', (error) => {
|
||||
console.error('Console transport error', error);
|
||||
});
|
||||
|
||||
this.winstonLogger = winston.createLogger({
|
||||
level: 'debug',
|
||||
transports: [consoleTransport, rotateTransport],
|
||||
exitOnError: false,
|
||||
});
|
||||
}
|
||||
|
||||
private pruneLogs(now: number) {
|
||||
@@ -122,24 +141,35 @@ class Logger {
|
||||
this.sentryEnabled = enabled;
|
||||
}
|
||||
|
||||
private formatArgs(args: unknown[]): string {
|
||||
return args
|
||||
.map((arg) => (typeof arg === 'object' ? safeStringify(arg) : String(arg)))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
log(level: LogLevel, message: string, ...args: unknown[]) {
|
||||
const formattedMessage = this.formatMessage(level, message, ...args);
|
||||
const formattedArgs = this.formatArgs(args);
|
||||
const mergedMessage = formattedArgs ? `${message} ${formattedArgs}` : message;
|
||||
const now = Date.now();
|
||||
const formattedMessage = `[${new Date(now).toISOString()}] [${level.toUpperCase()}] ${mergedMessage}`;
|
||||
|
||||
this.recentLogs.push({
|
||||
timestamp: now,
|
||||
level,
|
||||
message,
|
||||
message: mergedMessage,
|
||||
formatted: formattedMessage,
|
||||
});
|
||||
this.pruneLogs(now);
|
||||
|
||||
this.writeToConsole(level, message, ...args);
|
||||
this.writeToFile(formattedMessage);
|
||||
this.winstonLogger.log({
|
||||
level,
|
||||
message: mergedMessage,
|
||||
});
|
||||
|
||||
if (level === 'error' && this.sentryEnabled && this.sentryReporter) {
|
||||
this.sentryReporter({
|
||||
level,
|
||||
message,
|
||||
message: mergedMessage,
|
||||
error: this.extractError(args),
|
||||
logs: [...this.recentLogs],
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user