Skip to content

feat: live tail mode#328

Open
erickzhao wants to merge 7 commits into
mainfrom
live
Open

feat: live tail mode#328
erickzhao wants to merge 7 commits into
mainfrom
live

Conversation

@erickzhao
Copy link
Copy Markdown
Member

@erickzhao erickzhao commented Apr 24, 2026

This PR adds a Live Tail mode that peeks at your local Slack.app log folder. Useful for making your local logs human-readable and filterable, especially when you need to inspect across process types.

erickzhao and others added 4 commits April 23, 2026 18:20
Adds a new "live tail" mode that watches Slack's local log directories
and incrementally parses new log lines as they appear. Entry points
include welcome screen buttons and Electron menu items (macOS only).

Key components:
- LineParser: extracted incremental parsing state machine from readLogFile
- LiveTailWatcher: fs.watch-based file/directory monitoring with byte-offset reads
- Renderer live-tail update pipeline with MobX-driven re-renders
- Auto-scroll behavior and Live/Stop UI controls in header

Also fixes pre-existing MobX strict-mode violations in getSuggestions
and LogTimeView onZoomComplete.

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
@erickzhao erickzhao marked this pull request as ready for review April 29, 2026 23:17

private watchedDirs: string[] = [];

private async scanDirectory(): Promise<UnzippedFiles> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above in ipc.ts line 404

we're recursively scanning files here

Comment thread src/main/ipc.ts
);
}
/** Register IPC handlers for starting, stopping, and cleaning up live tail sessions. */
private setupLiveTail() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: This seems unsafe! We should define a file allow list, otherwise we can freely recurse and stream any file content under a given path.

Comment on lines +172 to +194
private onFileChange(fullPath: string) {
if (this.stopped) return;

const watched = this.watchedFiles.get(fullPath);
if (!watched) return;

let newSize: number;
try {
newSize = fs.statSync(fullPath).size;
} catch {
return;
}

if (newSize <= watched.byteOffset) {
if (newSize < watched.byteOffset) {
d('File %s was truncated/rotated, resetting', fullPath);
this.resetFile(watched);
}
return;
}

this.readIncremental(watched, newSize);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private onFileChange(fullPath: string) {
if (this.stopped) return;
const watched = this.watchedFiles.get(fullPath);
if (!watched) return;
let newSize: number;
try {
newSize = fs.statSync(fullPath).size;
} catch {
return;
}
if (newSize <= watched.byteOffset) {
if (newSize < watched.byteOffset) {
d('File %s was truncated/rotated, resetting', fullPath);
this.resetFile(watched);
}
return;
}
this.readIncremental(watched, newSize);
}
private onFileChange(fullPath: string) {
if (this.stopped || !this.watchedFiles.has(fullPath)) return;
this.dirtyFiles.add(fullPath);
if (!this.readTimer) {
this.readTimer = setTimeout(() => this.drainDirty(), 20);
}
}
private drainDirty() {
this.readTimer = null;
if (this.stopped) return;
for (const fullPath of this.dirtyFiles) {
const watched = this.watchedFiles.get(fullPath);
if (!watched) continue;
let newSize: number;
try {
newSize = fs.statSync(fullPath).size;
} catch {
continue;
}
if (newSize <= watched.byteOffset) {
if (newSize < watched.byteOffset) {
d('File %s was truncated/rotated, resetting', fullPath);
this.resetFile(watched);
}
continue;
}
this.readIncremental(watched, newSize);
}
this.dirtyFiles.clear();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can add a 20ms debounce, or even a 50tbh so we aren't creating a new read stream and readline scaffolding on each line, if we're bursting logs we can batch read and write.

private directoryWatchers: fs.FSWatcher[] = [];
private watchedFiles = new Map<string, WatchedFile>();
private pendingUpdates: LiveTailUpdate[] = [];
private flushTimer: ReturnType<typeof setTimeout> | null = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private flushTimer: ReturnType<typeof setTimeout> | null = null;
private flushTimer: ReturnType<typeof setTimeout> | null = null;
private dirtyFiles = new Set<string>();
private readTimer: ReturnType<typeof setTimeout> | null = null;
private stopped = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment about batching read and writes across a debounce below

}
this.watchedFiles.clear();

if (this.flushTimer) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (this.flushTimer) {
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
if (this.readTimer) {
clearTimeout(this.readTimer);
this.readTimer = null;
}
this.dirtyFiles.clear();
this.pendingUpdates = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cleanup for batching read and writes

Comment thread src/renderer/live-tail.ts
for (const logType of affectedTypes) {
if (logType === LogType.ALL) continue;

const existingMerged = state.mergedLogFiles![logType];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd like to change this into a guard instead, makes for better error logs

* Watches a Slack logs directory for changes, incrementally parses new lines,
* and sends batched updates to the renderer via IPC every `FLUSH_DEBOUNCE_MS`.
*/
export class LiveTailWatcher {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow-up: all of our parsing work runs in the main thread, if CE uses these on non-dev machines, i'm not sure if all the regex matching can cause sleuth to lag, I think its a much better architectural decision to treat the file stream like a message queue and move all the reading, decoding and regex matching to a node worker thread instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants