Skip to main content
Glama
logcat.ts9.21 kB
/** * Android Logcat Capture * Captures and streams logcat output from Android devices */ import { executeShell } from '../../utils/shell.js'; import { LogEntry, LogFilter, LogLevel, parseLogcatLine, filterLogEntries, } from '../../models/log-entry.js'; /** * Options for logcat capture */ export interface LogcatOptions { /** Device ID */ deviceId?: string; /** App package name to filter */ packageName?: string; /** Maximum number of lines to capture */ maxLines?: number; /** Log filter */ filter?: LogFilter; /** Include crash/ANR logs */ includeCrashes?: boolean; /** Timeout in milliseconds */ timeoutMs?: number; /** Clear logcat buffer before capture */ clear?: boolean; } /** * Capture logcat output */ export async function captureLogcat( options: LogcatOptions = {} ): Promise<LogEntry[]> { const { deviceId, packageName, maxLines = 500, filter, includeCrashes = true, timeoutMs = 30000, clear = false, } = options; // Clear logcat if requested if (clear) { await clearLogcat(deviceId); } const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('logcat', '-d'); // Use threadtime format for complete information args.push('-v', 'threadtime'); // Add line limit args.push('-t', String(maxLines)); // If package name provided, get PID and filter let targetPid: number | undefined; if (packageName) { targetPid = await getAppPid(packageName, deviceId); if (targetPid) { args.push('--pid', String(targetPid)); } } try { const result = await executeShell('adb', args, { timeoutMs }); if (result.exitCode !== 0 && !result.stdout) { return []; } // Parse log lines const entries: LogEntry[] = []; const lines = result.stdout.split('\n'); for (const line of lines) { if (!line.trim()) continue; const entry = parseLogcatLine(line); if (entry) { entries.push(entry); } } // Apply filters let filtered = entries; if (filter) { filtered = filterLogEntries(entries, filter); } // Include crash logs if requested if (includeCrashes && packageName) { // Get crash buffer logs const crashes = await getCrashLogs(packageName, deviceId, timeoutMs); // Also get AndroidRuntime logs directly (not filtered by PID) // These contain FATAL EXCEPTION and stack traces const runtimeLogs = await getAndroidRuntimeLogs(deviceId, timeoutMs); // Merge and deduplicate by timestamp + message const seen = new Set(filtered.map(e => `${e.timestamp.getTime()}-${e.message}`)); for (const entry of [...crashes, ...runtimeLogs]) { const key = `${entry.timestamp.getTime()}-${entry.message}`; if (!seen.has(key)) { filtered.push(entry); seen.add(key); } } } return filtered; } catch { return []; } } /** * Get app PID */ async function getAppPid( packageName: string, deviceId?: string ): Promise<number | undefined> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('shell', 'pidof', packageName); try { const result = await executeShell('adb', args, { timeoutMs: 5000 }); if (result.exitCode === 0 && result.stdout.trim()) { const pids = result.stdout.trim().split(/\s+/); return parseInt(pids[0], 10); } return undefined; } catch { return undefined; } } /** * Clear logcat buffer */ export async function clearLogcat(deviceId?: string): Promise<boolean> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('logcat', '-c'); try { const result = await executeShell('adb', args, { timeoutMs: 5000 }); return result.exitCode === 0; } catch { return false; } } /** * Get crash/ANR logs * Note: We don't filter by package name in the crash buffer because: * 1. Crash logs use the old PID which may not match the current running app * 2. AndroidRuntime tag doesn't include package name in log line format * 3. The crash buffer is specifically for fatal crashes, so all entries are relevant */ async function getCrashLogs( packageName: string, deviceId?: string, timeoutMs: number = 10000 ): Promise<LogEntry[]> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } // Read from crash buffer - get more entries to catch recent crashes args.push('logcat', '-b', 'crash', '-d', '-v', 'threadtime', '-t', '200'); try { const result = await executeShell('adb', args, { timeoutMs }); if (result.exitCode !== 0) { return []; } const entries: LogEntry[] = []; const lines = result.stdout.split('\n'); for (const line of lines) { if (!line.trim()) continue; const entry = parseLogcatLine(line); if (entry) { // Include if: // 1. Line contains package name (process crashed) // 2. Tag is AndroidRuntime (fatal exceptions) // 3. Message contains common crash indicators const isRelevant = line.includes(packageName) || entry.tag === 'AndroidRuntime' || entry.tag === 'DEBUG' || entry.message.includes('FATAL EXCEPTION') || entry.message.includes('Process:') || /\b(Exception|Error)\b/.test(entry.message); if (isRelevant) { entries.push(entry); } } } return entries; } catch { return []; } } /** * Get AndroidRuntime logs (FATAL EXCEPTION, stack traces) * These are not filtered by PID to catch crashes from any process */ async function getAndroidRuntimeLogs( deviceId?: string, timeoutMs: number = 10000 ): Promise<LogEntry[]> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } // Get logs specifically from AndroidRuntime tag (fatal exceptions) args.push('logcat', '-d', '-v', 'threadtime', '-t', '100'); args.push('AndroidRuntime:E', '*:S'); // Only AndroidRuntime errors, silence others try { const result = await executeShell('adb', args, { timeoutMs }); if (result.exitCode !== 0) { return []; } const entries: LogEntry[] = []; const lines = result.stdout.split('\n'); for (const line of lines) { if (!line.trim()) continue; const entry = parseLogcatLine(line); if (entry) { entries.push(entry); } } return entries; } catch { return []; } } /** * Get logs by tag */ export async function getLogsByTag( tags: string[], options: Omit<LogcatOptions, 'filter'> = {} ): Promise<LogEntry[]> { const { deviceId, maxLines = 200, timeoutMs = 15000 } = options; const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('logcat', '-d', '-v', 'threadtime', '-t', String(maxLines)); // Add tag filters (TAG:LEVEL format, *:S silences everything else) for (const tag of tags) { args.push(`${tag}:V`); } args.push('*:S'); try { const result = await executeShell('adb', args, { timeoutMs }); if (result.exitCode !== 0) { return []; } const entries: LogEntry[] = []; const lines = result.stdout.split('\n'); for (const line of lines) { if (!line.trim()) continue; const entry = parseLogcatLine(line); if (entry) { entries.push(entry); } } return entries; } catch { return []; } } /** * Get logs by level */ export async function getLogsByLevel( minLevel: LogLevel, options: Omit<LogcatOptions, 'filter'> = {} ): Promise<LogEntry[]> { const logs = await captureLogcat(options); return filterLogEntries(logs, { minLevel }); } /** * Search logs by pattern */ export async function searchLogs( pattern: string, options: Omit<LogcatOptions, 'filter'> = {} ): Promise<LogEntry[]> { const logs = await captureLogcat(options); return filterLogEntries(logs, { pattern, ignoreCase: true }); } /** * Get logcat buffer statistics */ export async function getLogcatStats( deviceId?: string ): Promise<{ main: number; system: number; crash: number; events: number } | null> { const args: string[] = []; if (deviceId) { args.push('-s', deviceId); } args.push('logcat', '-g'); try { const result = await executeShell('adb', args, { timeoutMs: 5000 }); if (result.exitCode !== 0) { return null; } const stats = { main: 0, system: 0, crash: 0, events: 0 }; const lines = result.stdout.split('\n'); for (const line of lines) { const match = line.match(/(\w+):\s*ring buffer is\s*([\d.]+)\s*([KMG]?B)/i); if (match) { const [, buffer, size, unit] = match; let bytes = parseFloat(size); if (unit === 'KB' || unit === 'K') bytes *= 1024; if (unit === 'MB' || unit === 'M') bytes *= 1024 * 1024; if (unit === 'GB' || unit === 'G') bytes *= 1024 * 1024 * 1024; if (buffer.toLowerCase() in stats) { stats[buffer.toLowerCase() as keyof typeof stats] = bytes; } } } return stats; } catch { return null; } }

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/abd3lraouf/specter-mcp'

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