/**
* 로깅 서비스
*
* 모든 Gemini 요청/응답을 로그로 기록
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// ============================================================
// Configuration
// ============================================================
const LOG_LEVEL = process.env.GEMINI_LOG_LEVEL || 'info'; // debug, info, warn, error, none
const LOG_FILE = process.env.GEMINI_LOG_FILE; // Optional: path to log file
const LOG_DIR = process.env.GEMINI_LOG_DIR || path.join(os.homedir(), '.gemini-mcp', 'logs');
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
const LOG_LEVELS: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
none: 4,
};
// ============================================================
// Log Entry Types
// ============================================================
export interface RequestLog {
timestamp: string;
tool: string;
provider: 'api' | 'cli';
model: string;
prompt: string;
context?: string;
source?: string;
code?: string;
}
export interface ResponseLog {
timestamp: string;
tool: string;
provider: 'api' | 'cli';
model: string;
success: boolean;
response?: string;
responseLength?: number;
tokenUsage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
durationMs: number;
error?: string;
}
// ============================================================
// Logger Implementation
// ============================================================
let logFileStream: fs.WriteStream | null = null;
let errorLogStream: fs.WriteStream | null = null;
function shouldLog(level: LogLevel): boolean {
return LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL as LogLevel];
}
function ensureLogDir(): void {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
}
function getLogFilePath(isError = false): string {
if (!isError && LOG_FILE) return LOG_FILE;
const date = new Date().toISOString().split('T')[0];
const suffix = isError ? '-error' : '';
return path.join(LOG_DIR, `gemini-mcp-${date}${suffix}.log`);
}
function getLogStream(): fs.WriteStream {
if (!logFileStream) {
ensureLogDir();
const logPath = getLogFilePath(false);
logFileStream = fs.createWriteStream(logPath, { flags: 'a' });
}
return logFileStream;
}
function getErrorLogStream(): fs.WriteStream {
if (!errorLogStream) {
ensureLogDir();
const logPath = getLogFilePath(true);
errorLogStream = fs.createWriteStream(logPath, { flags: 'a' });
}
return errorLogStream;
}
function formatTimestamp(): string {
return new Date().toISOString();
}
function formatLogEntry(entry: Record<string, any>): string {
const separator = '─'.repeat(60);
const timestamp = entry.timestamp || new Date().toISOString();
const type = (entry.type || 'log').toUpperCase();
const header = `[${timestamp}] [${type}]`;
// response를 맨 아래로 이동
const { response, ...rest } = entry;
const orderedEntry = response !== undefined ? { ...rest, response } : rest;
const prettyJson = JSON.stringify(orderedEntry, null, 2);
return `${separator}\n${header}\n${prettyJson}\n`;
}
function writeToFile(entry: object, isError = false): void {
if (LOG_LEVEL === 'none') return;
try {
const formatted = formatLogEntry(entry);
const stream = getLogStream();
stream.write(formatted);
// 에러는 별도 파일에도 기록
if (isError) {
const errorStream = getErrorLogStream();
errorStream.write(formatted);
}
} catch (error) {
// Silently fail file writes
}
}
function writeToStderr(level: LogLevel, message: string): void {
if (!shouldLog(level)) return;
const prefix = `[gemini-mcp:${level.toUpperCase()}]`;
console.error(`${prefix} ${message}`);
}
// ============================================================
// Public API
// ============================================================
export function logRequest(log: Omit<RequestLog, 'timestamp'>): void {
const entry: RequestLog = {
timestamp: formatTimestamp(),
...log,
};
writeToFile({ type: 'request', ...entry });
if (shouldLog('debug')) {
const promptPreview = log.prompt.length > 100
? log.prompt.substring(0, 100) + '...'
: log.prompt;
writeToStderr('debug', `[${log.tool}] Request: ${promptPreview}`);
}
if (shouldLog('info')) {
writeToStderr('info', `[${log.tool}] ${log.provider.toUpperCase()} request (${log.model})`);
}
}
export function logResponse(log: Omit<ResponseLog, 'timestamp'>): void {
const entry: ResponseLog = {
timestamp: formatTimestamp(),
...log,
};
writeToFile({ type: 'response', ...entry }, !log.success);
if (log.success) {
if (shouldLog('info')) {
const tokens = log.tokenUsage
? ` | ${log.tokenUsage.totalTokens} tokens`
: '';
writeToStderr('info', `[${log.tool}] Success (${log.durationMs}ms${tokens})`);
}
} else {
if (shouldLog('error')) {
writeToStderr('error', `[${log.tool}] Error: ${log.error}`);
}
}
}
export function logDebug(tool: string, message: string): void {
if (shouldLog('debug')) {
writeToStderr('debug', `[${tool}] ${message}`);
}
}
export function logInfo(tool: string, message: string): void {
if (shouldLog('info')) {
writeToStderr('info', `[${tool}] ${message}`);
}
}
export function logWarn(tool: string, message: string): void {
if (shouldLog('warn')) {
writeToStderr('warn', `[${tool}] ${message}`);
}
}
export function logError(tool: string, message: string): void {
if (shouldLog('error')) {
writeToStderr('error', `[${tool}] ${message}`);
}
}
// ============================================================
// Cleanup
// ============================================================
export function closeLogger(): void {
if (logFileStream) {
logFileStream.end();
logFileStream = null;
}
if (errorLogStream) {
errorLogStream.end();
errorLogStream = null;
}
}
// Log startup
if (shouldLog('info')) {
console.error(`[gemini-mcp:INFO] 로그 레벨: ${LOG_LEVEL}`);
if (LOG_LEVEL !== 'none') {
console.error(`[gemini-mcp:INFO] 로그 디렉토리: ${LOG_DIR}`);
}
}