Skip to content

Commit e3ec262

Browse files
ozgesolidkeyclaude
andcommitted
Add SQLite baseline store for log comparison and anomaly detection
Captures a rich fingerprint of log analysis (level distribution, crashes, components, timestamp density, sample lines) into ~/.logan/baselines.db. Users can save baselines from known-good logs and compare new logs against them to detect level shifts, new crashes, error rate spikes, missing components, and activity pattern changes. Includes UI in analysis panel, 4 HTTP API endpoints, 4 MCP tools, and 45 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8099407 commit e3ec262

13 files changed

Lines changed: 1968 additions & 22 deletions

File tree

package-lock.json

Lines changed: 209 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
},
3232
"devDependencies": {
3333
"@electron/rebuild": "^3.7.2",
34+
"@types/better-sqlite3": "^7.6.13",
3435
"@types/diff": "^7.0.2",
3536
"@types/node": "^20.10.0",
3637
"@types/ssh2": "^1.15.5",
@@ -41,6 +42,7 @@
4142
},
4243
"dependencies": {
4344
"@modelcontextprotocol/sdk": "^1.26.0",
45+
"better-sqlite3": "^12.6.2",
4446
"diff": "^8.0.3",
4547
"marked": "^17.0.1",
4648
"node-pty": "^1.1.0",

src/main/api-server.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as os from 'os';
55
import { BrowserWindow } from 'electron';
66
import { SearchOptions, Bookmark, Highlight } from '../shared/types';
77
import { FileHandler } from './fileHandler';
8+
import { BaselineStore, buildFingerprint } from './baselineStore';
9+
import { AnalysisResult } from './analyzers/types';
810

911
const API_PORT = 19532;
1012
const PORT_FILE = path.join(os.homedir(), '.logan', 'mcp-port');
@@ -27,6 +29,8 @@ export interface ApiContext {
2729
addHighlight(highlight: Highlight): any;
2830
detectTimeGaps(options: any): Promise<any>;
2931
navigateToLine(lineNumber: number): void;
32+
getBaselineStore(): BaselineStore;
33+
getAnalysisResult(): AnalysisResult | null;
3034
}
3135

3236
let server: http.Server | null = null;
@@ -121,6 +125,12 @@ export function startApiServer(ctx: ApiContext): void {
121125
return;
122126
}
123127

128+
if (url === '/api/baselines') {
129+
const baselines = ctx.getBaselineStore().list();
130+
sendJson(res, { success: true, baselines });
131+
return;
132+
}
133+
124134
sendError(res, 'Not found', 404);
125135
return;
126136
}
@@ -228,6 +238,44 @@ export function startApiServer(ctx: ApiContext): void {
228238
return;
229239
}
230240

241+
if (url === '/api/baseline-save') {
242+
const filePath = ctx.getCurrentFilePath();
243+
const handler = ctx.getFileHandler();
244+
const analysisResult = ctx.getAnalysisResult();
245+
if (!filePath || !handler) return sendError(res, 'No file open');
246+
if (!analysisResult) return sendError(res, 'Run analysis first');
247+
const fp = buildFingerprint(filePath, analysisResult, handler);
248+
const id = ctx.getBaselineStore().save(
249+
body.name || 'Unnamed baseline',
250+
body.description || '',
251+
body.tags || [],
252+
fp
253+
);
254+
sendJson(res, { success: true, id });
255+
return;
256+
}
257+
258+
if (url === '/api/baseline-compare') {
259+
const filePath = ctx.getCurrentFilePath();
260+
const handler = ctx.getFileHandler();
261+
const analysisResult = ctx.getAnalysisResult();
262+
if (!filePath || !handler) return sendError(res, 'No file open');
263+
if (!analysisResult) return sendError(res, 'Run analysis first');
264+
if (!body.baselineId) return sendError(res, 'baselineId required');
265+
const fp = buildFingerprint(filePath, analysisResult, handler);
266+
const report = ctx.getBaselineStore().compare(fp, body.baselineId);
267+
if (!report) return sendError(res, 'Baseline not found');
268+
sendJson(res, { success: true, report });
269+
return;
270+
}
271+
272+
if (url === '/api/baseline-delete') {
273+
if (!body.baselineId) return sendError(res, 'baselineId required');
274+
const ok = ctx.getBaselineStore().delete(body.baselineId);
275+
sendJson(res, { success: ok, error: ok ? undefined : 'Baseline not found' });
276+
return;
277+
}
278+
231279
sendError(res, 'Not found', 404);
232280
return;
233281
}

0 commit comments

Comments
 (0)