Skip to content

Commit 30e4307

Browse files
committed
feat: out-of-the-box colorful logging, per-event BYOD opt-out, 100% test coverage
1 parent 2ed92e6 commit 30e4307

10 files changed

Lines changed: 467 additions & 238 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,30 @@ agent.on('pool-exhaustion', (event) => {
547547
> [!NOTE]
548548
> `ArgusAgent` calls `setMaxListeners(0)` internally — you can attach as many listeners as needed without triggering Node's memory leak warning.
549549
550+
### Bring Your Own Destination (BYOD)
551+
552+
Argus is designed to be fully extensible. While the built-in console logger formats events beautifully during development, you can easily pipe critical alerts to Slack, PagerDuty, or your own custom logging stack:
553+
554+
```typescript
555+
// 1. Send memory leaks to a Slack webhook
556+
agent.on('leak', async (event) => {
557+
await fetch(process.env.SLACK_WEBHOOK, {
558+
method: 'POST',
559+
body: JSON.stringify({ text: `🚨 Memory leak detected: ${event.handlesCount} active handles` })
560+
});
561+
});
562+
563+
// 2. Send slow queries to your own logging infrastructure
564+
agent.on('slow-query', (record) => {
565+
myCustomLogger.warn('Slow DB Query', {
566+
query: record.sanitizedQuery,
567+
ms: record.durationMs
568+
});
569+
});
570+
```
571+
572+
**Per-Event Opt-Out:** If you are using the default console logger (e.g., in the `dev` profile), the agent detects your custom `.on()` listeners and **gracefully steps aside**. It will skip printing its own default log for that specific event, avoiding duplicate noise while continuing to format everything else perfectly.
573+
550574
---
551575

552576
## Environment Variables

packages/agent/src/argus-agent.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -826,22 +826,38 @@ export class ArgusAgent extends EventEmitter {
826826

827827
/** Steps 7 + 13 — Fire-and-forget static and audit scans. */
828828
private runStartupScans(): void {
829+
// Suppress FS events while startup scans read source files — those reads are
830+
// expected behaviour, not hot-path bugs, and would otherwise flood the output.
831+
if (this.fsTracker) this.fsTracker.suppress();
832+
833+
const scanDone: Promise<unknown>[] = [];
834+
829835
if (this.staticScanDir) {
830-
new StaticScanner(this.staticScanDir)
831-
.scan()
832-
.then((results) => this.emit("scan", results))
833-
.catch((err) => this.emit("error", err));
836+
scanDone.push(
837+
new StaticScanner(this.staticScanDir)
838+
.scan()
839+
.then((results) => this.emit("scan", results))
840+
.catch((err) => this.emit("error", err)),
841+
);
834842
}
835843

836844
if (this.auditScanDir) {
837-
new AuditScanner(this.auditScanDir)
838-
.scan()
839-
.then((result) => {
840-
if (result) this.emit("audit", result);
841-
})
842-
.catch((err) => this.emit("error", err));
845+
scanDone.push(
846+
new AuditScanner(this.auditScanDir)
847+
.scan()
848+
.then((result) => {
849+
if (result) this.emit("audit", result);
850+
})
851+
.catch((err) => this.emit("error", err)),
852+
);
843853
}
844854

855+
Promise.all(scanDone)
856+
.catch((err: Error) => this.emit("error", err))
857+
.finally(() => {
858+
if (this.fsTracker) this.fsTracker.resume();
859+
});
860+
845861
if (this.indexHintsDir) {
846862
new MigrationScanner()
847863
.scan(this.indexHintsDir)

packages/agent/src/instrumentation/dns-monitor.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,20 @@ export class DnsMonitor extends EventEmitter {
7676

7777
const start = performance.now();
7878
const cb = lastArg as (err: Error | null, address: string, family: number) => void;
79-
rest[rest.length - 1] = (err: Error | null, address: string, family: number) => {
79+
rest[rest.length - 1] = (
80+
err: Error | null,
81+
address: string | { address: string; family: number }[],
82+
family?: number,
83+
) => {
8084
const durationMs = performance.now() - start;
81-
this._injectDns(hostname, durationMs, err?.message, address ? [address] : undefined);
82-
cb(err, address, family);
85+
let addresses: string[] | undefined;
86+
if (Array.isArray(address)) {
87+
addresses = address.length > 0 ? address.map((a) => a.address) : undefined;
88+
} else {
89+
addresses = address ? [address] : undefined;
90+
}
91+
this._injectDns(hostname, durationMs, err?.message, addresses);
92+
cb(err, address as string, family!);
8393
};
8494

8595
(origLookup as (...a: unknown[]) => void).call(dns, hostname, ...rest);

packages/agent/src/instrumentation/fs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type FsMethod = (...args: unknown[]) => unknown;
2424
export class FsInstrumentation extends EventEmitter {
2525
private analyzer = new FsAnalyzer();
2626
private active = false;
27+
private suppressed = false;
2728
private getSourceLine: () => string | undefined;
2829
private originalMethods = new Map<string, FsMethod>();
2930

@@ -98,7 +99,11 @@ export class FsInstrumentation extends EventEmitter {
9899
};
99100
}
100101

102+
public suppress(): void { this.suppressed = true; }
103+
public resume(): void { this.suppressed = false; }
104+
101105
private record(method: string, path: string, start: number, sourceLine?: string) {
106+
if (this.suppressed) return;
102107
const durationMs = performance.now() - start;
103108
const suggestions = this.analyzer.analyze(method, path, !!getCurrentContext());
104109

0 commit comments

Comments
 (0)