Stream Android and iOS logs without Android Studio or Console.app, with programmatic Node.js API for log analysis.
This is a Wix Incubator fork of the original logkitty project.
npm install logkitten
# or
yarn add logkitten
import { logkitten, Level } from 'logkitten';
// Basic usage - get all Android logs
const emitter = logkitten({
platform: 'android',
});
emitter.on('entry', (entry) => {
// Process the structured log entry
console.log({
ts: new Date(entry.ts),
level: entry.level,
pid: entry.pid,
tid: entry.tid,
message: entry.msg,
});
});
emitter.on('error', (error: Error) => {
console.error('Logging error:', error.message);
});
// When you're done listening to logs, close the stream programmatically
await emitter.close(); // or emitter.close(callback)
The main function that accepts platform-specific options:
// Android
const androidEmitter = logkitten({
platform: 'android',
// Optional: specify custom adb path
adbPath: '/custom/path/to/adb',
// Optional: target specific device
deviceId: 'emulator-5554',
// Optional: filter entries
filter: (entry) => entry.tag === 'MyApp'
});
// iOS
const iosEmitter = logkitten({
platform: 'ios',
// Optional: target specific simulator (defaults to 'booted')
deviceId: 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890',
// Optional: filter entries
filter: (entry) => entry.processImagePath.includes('/MyApp')
});
Each log entry is a structured object with platform-specific extensions:
interface Entry {
msg: string; // Message content
ts: number; // Timestamp in milliseconds
pid: number; // Process ID
tid: number; // Thread ID
level: number; // Log level/priority (Level enum)
}
interface AndroidEntry extends Entry {
tag: string; // Log tag
uid: string; // User ID
}
interface IosEntry extends Entry {
category: string; // iOS category
formatString: string; // Format string used for the log message
processImagePath: string; // Path to the process executable
subsystem: string; // iOS subsystem
userID: number; // User ID
}
The emitter returned by all functions extends Node's EventEmitter
and provides:
-
Events
entry
(arguments:entry: AndroidEntry | IosEntry
) – Emitted when a new log entry passes filters.error
(arguments:error: Error
) – Emitted when parsing fails or the underlying process encounters errors.close
– Emitted once the logging process is terminated via.close()
.
-
Methods
close(cb?)
– Gracefully stops the underlying logging process and returns a Promise that resolves when cleanup is complete.
import { Level } from 'logkitten';
// Available priorities (from lowest to highest):
Level.TRACE // 10 - Trace level (Android verbose)
Level.DEBUG // 20 - Debug level
Level.INFO // 30 - Info level
Level.WARN // 40 - Warning level
Level.ERROR // 50 - Error level
Level.FATAL // 60 - Fatal level
You can provide a custom filter function to control which log entries are emitted:
const emitter = logkitten({
platform: 'android',
filter: (entry) => {
// Only include entries with specific tags
return entry.tag === 'MyApp' || entry.tag === 'ReactNative';
}
});
Filter by tag (Android):
(entry) => entry.tag === 'MyApp'
Filter by process (both platforms):
(entry) => entry.pid === myAppPid
Filter by message content:
filter: (entry) => /error|warning/i.test(entry.msg)
Filter by subsystem (iOS):
filter: (entry) => entry.subsystem === 'com.mycompany.myapp'
Multi-criteria filtering:
(entry) => {
const isMyApp = entry.tag === 'MyApp' || entry.processImagePath?.includes('MyApp');
const isError = entry.level >= Level.ERROR;
const hasKeyword = entry.msg.includes('network');
return isMyApp && (isError || hasKeyword);
}
const emitter = logkitten({
platform: 'android',
});
const errorCount = new Map();
emitter.on('entry', (entry) => {
if (entry.level >= Level.ERROR) {
const count = errorCount.get(entry.tag) || 0;
errorCount.set(entry.tag, count + 1);
}
});
// Log error summary every 10 seconds
setInterval(() => {
console.log('Error counts by tag:', Object.fromEntries(errorCount));
}, 10000);
const emitter = logkitten({
platform: 'ios',
filter: (entry) => entry.processImagePath.includes('/MyApp'),
});
const logs = [];
emitter.on('entry', (entry) => {
// Store structured log data
logs.push({
timestamp: entry.ts,
level: entry.level,
source: entry.subsystem,
content: entry.msg,
});
// Keep only last 1000 entries
if (logs.length > 1000) {
logs.shift();
}
});
This project is a fork and continuation of the original logkitty by Paweł Trysła (@zamotany), maintained by Wix Incubator.
The original project has been inactive since 2020, and this fork continues development with modern dependencies and bug fixes. While we maintain this fork primarily for our own use, we welcome community contributions and will consider issues and feature requests as time permits.