Skip to main content
Glama
logs.ts9.13 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { loadEnv } from '../types/env.js'; import { Logger } from '../utils/logger.js'; import { promises as fs } from 'fs'; import path from 'path'; type ReadParams = { filterCategory?: string[] filterLevel?: 'Error' | 'Warning' | 'Log' | 'Verbose' | 'VeryVerbose' | 'All' lines?: number logPath?: string includePrefixes?: string[] excludeCategories?: string[] } type Entry = { timestamp?: string category?: string level?: string message: string } export class LogTools { private env = loadEnv(); private log = new Logger('LogTools'); private cachedLogPath?: string; constructor(private bridge: UnrealBridge) {} async readOutputLog(params: ReadParams) { const target = await this.resolveLogPath(params.logPath); if (!target) { return { success: false, error: 'Log file not found' }; } const maxLines = typeof params.lines === 'number' && params.lines > 0 ? Math.min(params.lines, 2000) : 200; let text = ''; try { text = await this.tailFile(target, maxLines); } catch (err: any) { return { success: false, error: String(err?.message || err) }; } const rawLines = text.split(/\r?\n/).filter(l => l.length > 0); const parsed: Entry[] = rawLines.map(l => this.parseLine(l)); const mappedLevel = params.filterLevel || 'All'; const includeCats = Array.isArray(params.filterCategory) && params.filterCategory.length ? new Set(params.filterCategory) : undefined; const includePrefixes = Array.isArray(params.includePrefixes) && params.includePrefixes.length ? params.includePrefixes : undefined; const excludeCats = Array.isArray(params.excludeCategories) && params.excludeCategories.length ? new Set(params.excludeCategories) : undefined; const filtered = parsed.filter(e => { if (!e) return false; if (mappedLevel && mappedLevel !== 'All') { const lv = (e.level || 'Log'); if (lv === 'Display') { if (mappedLevel !== 'Log') return false; } else if (lv !== mappedLevel) { return false; } } if (includeCats && e.category && !includeCats.has(e.category)) return false; if (includePrefixes && includePrefixes.length && e.category) { if (!includePrefixes.some(p => (e.category ?? '').startsWith(p))) return false; } if (excludeCats && e.category && excludeCats.has(e.category)) return false; return true; }); const includeInternal = Boolean( (includeCats && includeCats.has('LogPython')) || (includePrefixes && includePrefixes.some(p => 'LogPython'.startsWith(p))) ); const sanitized = includeInternal ? filtered : filtered.filter(entry => !this.isInternalLogEntry(entry)); return { success: true, logPath: target.replace(/\\/g, '/'), entries: sanitized, filteredCount: sanitized.length }; } private async resolveLogPath(override?: string): Promise<string | undefined> { if (override && typeof override === 'string' && override.trim()) { try { const st = await fs.stat(override); if (st.isFile()) { return this.cacheLogPath(path.resolve(override)); } } catch {} } if (this.cachedLogPath && (await this.fileExists(this.cachedLogPath))) { return this.cachedLogPath; } const envLog = await this.resolveFromProjectEnv(); if (envLog) { return envLog; } if (this.bridge.isConnected) { try { const script = ` import unreal, json, os paths = [] try: d = unreal.Paths.project_log_dir() if d: paths.append(os.path.abspath(d)) except Exception: pass try: sd = unreal.Paths.project_saved_dir() if sd: p = os.path.join(sd, 'Logs') paths.append(os.path.abspath(p)) except Exception: pass try: pf = unreal.Paths.get_project_file_path() if pf: pd = os.path.dirname(pf) p = os.path.join(pd, 'Saved', 'Logs') paths.append(os.path.abspath(p)) except Exception: pass all_logs = [] for base in paths: try: if os.path.isdir(base): for name in os.listdir(base): if name.lower().endswith('.log'): fp = os.path.join(base, name) try: m = os.path.getmtime(fp) all_logs.append({'p': fp, 'm': m}) except Exception: pass except Exception: pass all_logs.sort(key=lambda x: x['m'], reverse=True) print('RESULT:' + json.dumps({'dirs': paths, 'logs': all_logs})) `.trim(); const res = await this.bridge.executePythonWithResult(script); const logs = Array.isArray(res?.logs) ? res.logs : []; for (const entry of logs) { const p = typeof entry?.p === 'string' ? entry.p : undefined; if (p && p.trim()) return this.cacheLogPath(p); } } catch {} } const fallback = await this.findLatestLogInDir(path.join(process.cwd(), 'Saved', 'Logs')); if (fallback) { return fallback; } return undefined; } private async resolveFromProjectEnv(): Promise<string | undefined> { const projectPath = this.env.UE_PROJECT_PATH; if (projectPath && typeof projectPath === 'string' && projectPath.trim()) { const projectDir = path.dirname(projectPath); const logsDir = path.join(projectDir, 'Saved', 'Logs'); const envLog = await this.findLatestLogInDir(logsDir); if (envLog) { return envLog; } } return undefined; } private async findLatestLogInDir(dir: string): Promise<string | undefined> { if (!dir) return undefined; try { const entries = await fs.readdir(dir); const candidates: { p: string; m: number }[] = []; for (const name of entries) { if (!name.toLowerCase().endsWith('.log')) continue; const fp = path.join(dir, name); try { const st = await fs.stat(fp); candidates.push({ p: fp, m: st.mtimeMs }); } catch {} } if (candidates.length) { candidates.sort((a, b) => b.m - a.m); return this.cacheLogPath(candidates[0].p); } } catch {} return undefined; } private async fileExists(filePath: string): Promise<boolean> { try { const st = await fs.stat(filePath); return st.isFile(); } catch { return false; } } private cacheLogPath(p: string): string { this.cachedLogPath = p; return p; } private async tailFile(filePath: string, maxLines: number): Promise<string> { const handle = await fs.open(filePath, 'r'); try { const stat = await handle.stat(); const chunkSize = 128 * 1024; let position = stat.size; let remaining = ''; const lines: string[] = []; while (position > 0 && lines.length < maxLines) { const readSize = Math.min(chunkSize, position); position -= readSize; const buf = Buffer.alloc(readSize); await handle.read(buf, 0, readSize, position); remaining = buf.toString('utf8') + remaining; const parts = remaining.split(/\r?\n/); remaining = parts.shift() || ''; while (parts.length) { const line = parts.pop() as string; if (line === undefined) break; if (line.length === 0) continue; lines.unshift(line); if (lines.length >= maxLines) break; } } if (lines.length < maxLines && remaining) { lines.unshift(remaining); } return lines.slice(0, maxLines).join('\n'); } finally { try { await handle.close(); } catch {} } } private parseLine(line: string): Entry { const m1 = line.match(/^\[?(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}\.\d{2}:\d+)\]?\s*\[(.*?)\]\s*(.*)$/); if (m1) { const rest = m1[3]; const m2 = rest.match(/^(\w+):\s*(Error|Warning|Display|Log|Verbose|VeryVerbose):\s*(.*)$/); if (m2) { return { timestamp: m1[1], category: m2[1], level: m2[2] === 'Display' ? 'Log' : m2[2], message: m2[3] }; } const m3 = rest.match(/^(\w+):\s*(.*)$/); if (m3) { return { timestamp: m1[1], category: m3[1], level: 'Log', message: m3[2] }; } return { timestamp: m1[1], message: rest }; } const m = line.match(/^(\w+):\s*(Error|Warning|Display|Log|Verbose|VeryVerbose):\s*(.*)$/); if (m) { return { category: m[1], level: m[2] === 'Display' ? 'Log' : m[2], message: m[3] }; } const mAlt = line.match(/^(\w+):\s*(.*)$/); if (mAlt) { return { category: mAlt[1], level: 'Log', message: mAlt[2] }; } return { message: line }; } private isInternalLogEntry(entry: Entry): boolean { if (!entry) return false; const category = entry.category?.toLowerCase() || ''; const message = entry.message?.trim() || ''; if (category === 'logpython' && message.startsWith('RESULT:')) { return true; } if (!entry.category && message.startsWith('[') && message.includes('LogPython: RESULT:')) { return true; } return false; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server