Skip to content

Commit 41480e8

Browse files
committed
fix(showcase): add comprehensive logging to harness probe execution pipeline
Probe runs were a black box in Railway logs — results only visible via PocketBase API. Add structured INFO-level logging at every lifecycle boundary so operators can diagnose probe outcomes from logs alone. probe-invoker.ts: - tick-start: probe ID, kind, discovered target count, trigger source - target-start/complete: key, state (green/red/error), duration - tick-complete: passed/failed counts, total duration - run-summary: single structured line with every service's outcome e2e-deep.ts (D5 driver): - service-start: slug, feature count, backend URL - feature-complete: slug, feature type, pass/fail, error if failed, duration - service-complete: slug, passed/failed/skipped counts, duration browser-pool.ts: - acquire/release: available/inUse counts after operation - recycle: slot index, context count, recycle threshold orchestrator.ts: - Thread logger into BrowserPool constructor
1 parent 2781b7d commit 41480e8

4 files changed

Lines changed: 118 additions & 2 deletions

File tree

showcase/harness/src/orchestrator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export async function boot(opts: BootOptions = {}): Promise<{
272272
const poolSize = process.env.BROWSER_POOL_SIZE
273273
? parseInt(process.env.BROWSER_POOL_SIZE, 10) || 4
274274
: 4;
275-
const browserPool = new BrowserPool(poolSize);
275+
const browserPool = new BrowserPool(poolSize, undefined, logger);
276276
let browserPoolReady = false;
277277
try {
278278
await browserPool.init();

showcase/harness/src/probes/drivers/e2e-deep.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,13 @@ export function createE2eDeepDriver(
636636
});
637637
}
638638

639+
const serviceStart = Date.now();
640+
ctx.logger.info("probe.e2e-deep.service-start", {
641+
slug,
642+
featureCount: requestedFeatures.length,
643+
backendUrl,
644+
});
645+
639646
// Partition features into "registered" (script available) vs
640647
// "skipped" (no script). The skipped set short-circuits before
641648
// chromium so we don't pay for a launch per slug when Wave 2b
@@ -799,6 +806,7 @@ export function createE2eDeepDriver(
799806
const url = `${backendUrl}${route}`;
800807

801808
await sem.acquire();
809+
const featureStart = Date.now();
802810
try {
803811
if (abort.signal.aborted) {
804812
await sideEmit(ctx, {
@@ -817,6 +825,15 @@ export function createE2eDeepDriver(
817825
},
818826
observedAt: ctx.now().toISOString(),
819827
});
828+
ctx.logger.info("probe.e2e-deep.feature-complete", {
829+
slug,
830+
featureType: ft,
831+
pass: false,
832+
errorDesc: timedOut
833+
? `timeout after ${timeoutMs}ms`
834+
: "aborted",
835+
durationMs: Date.now() - featureStart,
836+
});
820837
return {
821838
ft,
822839
ok: false as const,
@@ -856,6 +873,12 @@ export function createE2eDeepDriver(
856873
},
857874
observedAt: ctx.now().toISOString(),
858875
});
876+
ctx.logger.info("probe.e2e-deep.feature-complete", {
877+
slug,
878+
featureType: ft,
879+
pass: true,
880+
durationMs: Date.now() - featureStart,
881+
});
859882
return { ft, ok: true as const };
860883
} else {
861884
await sideEmit(ctx, {
@@ -878,6 +901,13 @@ export function createE2eDeepDriver(
878901
},
879902
observedAt: ctx.now().toISOString(),
880903
});
904+
ctx.logger.info("probe.e2e-deep.feature-complete", {
905+
slug,
906+
featureType: ft,
907+
pass: false,
908+
errorDesc: featureResult.errorDesc,
909+
durationMs: Date.now() - featureStart,
910+
});
881911
return {
882912
ft,
883913
ok: false as const,
@@ -944,6 +974,15 @@ export function createE2eDeepDriver(
944974
}
945975

946976
const aggregateGreen = failed.length === 0;
977+
ctx.logger.info("probe.e2e-deep.service-complete", {
978+
slug,
979+
passed,
980+
failed: failed.length,
981+
skipped: skipped.length,
982+
total: requestedFeatures.length,
983+
state: aggregateGreen ? "green" : "red",
984+
durationMs: Date.now() - serviceStart,
985+
});
947986
return {
948987
key: input.key,
949988
state: aggregateGreen ? "green" : "red",

showcase/harness/src/probes/helpers/browser-pool.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ export interface BrowserPoolStats {
1717
totalRecycles: number;
1818
}
1919

20+
/**
21+
* Minimal logger surface the pool uses for lifecycle events. Matches the
22+
* harness-wide `Logger` interface but only the `info` method is required.
23+
* Optional so existing callers (tests, legacy boot paths) don't break.
24+
*/
25+
interface PoolLogger {
26+
info(msg: string, meta?: Record<string, unknown>): void;
27+
}
28+
2029
export class BrowserPool {
2130
private readonly poolSize: number;
2231
private readonly recycleAfter: number;
@@ -26,13 +35,19 @@ export class BrowserPool {
2635
private totalRecycles = 0;
2736
private inFlightRecycles = new Set<Promise<void>>();
2837
private isShutdown = false;
38+
private readonly logger?: PoolLogger;
2939

3040
// Tracks which Browser instance maps to which Slot, so release() can find
3141
// the slot in O(1) even after the slot was removed from `available`.
3242
private browserToSlot = new Map<Browser, Slot>();
3343

34-
constructor(size = 4, recycleAfter?: number) {
44+
constructor(
45+
size = 4,
46+
recycleAfter?: number,
47+
logger?: PoolLogger,
48+
) {
3549
this.poolSize = size;
50+
this.logger = logger;
3651
const envRecycle = process.env.BROWSER_POOL_RECYCLE_AFTER
3752
? parseInt(process.env.BROWSER_POOL_RECYCLE_AFTER, 10)
3853
: undefined;
@@ -71,6 +86,10 @@ export class BrowserPool {
7186

7287
const slot = this.available.shift();
7388
if (slot) {
89+
this.logger?.info("browser-pool.acquire", {
90+
available: this.available.length,
91+
inUse: this.slots.length - this.available.length,
92+
});
7493
return slot.browser;
7594
}
7695

@@ -119,6 +138,11 @@ export class BrowserPool {
119138
} else {
120139
this.available.push(slot);
121140
}
141+
const s = this.stats();
142+
this.logger?.info("browser-pool.release", {
143+
available: s.available,
144+
inUse: s.inUse,
145+
});
122146
}
123147

124148
async shutdown(): Promise<void> {
@@ -158,6 +182,13 @@ export class BrowserPool {
158182

159183
private recycleSlot(slot: Slot): void {
160184
this.totalRecycles++;
185+
const slotIdx = this.slots.indexOf(slot);
186+
this.logger?.info("browser-pool.recycle", {
187+
slotIndex: slotIdx,
188+
contextCount: slot.contextCount,
189+
recycleAfter: this.recycleAfter,
190+
totalRecycles: this.totalRecycles,
191+
});
161192

162193
// Remove the old browser from the lookup map immediately so a
163194
// double-release of the stale reference is a no-op.

showcase/harness/src/probes/loader/probe-invoker.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export function buildProbeInvoker(
208208
// produce zero workers and drop every input on the floor.
209209
const concurrency = Math.max(cfg.max_concurrency, 1);
210210
const timeoutMs = "timeout_ms" in cfg ? cfg.timeout_ms : undefined;
211+
const tickStart = Date.now();
211212

212213
// B7: tracker registration. Read the slot's `triggeredRun` flag — set
213214
// by scheduler.trigger() before the handler runs — so the snapshot the
@@ -290,6 +291,14 @@ export function buildProbeInvoker(
290291
// fan out across.
291292
const allInputs: ResolvedInput[] = resolved.ok ? resolved.inputs : [];
292293

294+
logger.info("probe.tick-start", {
295+
probeId: cfg.id,
296+
kind: cfg.kind,
297+
discoveredTargets: allInputs.length,
298+
discoveryOk: resolved.ok,
299+
triggered,
300+
});
301+
293302
// CR-A1.1: thread the trigger's slug filter end-to-end. Discover the
294303
// FULL roster (so logs/diagnostics still see what the source returned)
295304
// but only enqueue + run the slugs the operator asked for. Empty
@@ -516,6 +525,8 @@ export function buildProbeInvoker(
516525
// a dead defensive guard.
517526
const idx = cursor++;
518527
const { input, key, preError } = inputs[idx]!;
528+
const targetStart = Date.now();
529+
logger.info("probe.target-start", { probeId: cfg.id, key });
519530
// B7: mark running just before handing the input to the driver.
520531
tracker.start(key);
521532
// CR-A1.2: short-circuit on inputs the resolver pre-flagged as
@@ -562,6 +573,12 @@ export function buildProbeInvoker(
562573
tracker.complete(key, "red");
563574
failed++;
564575
}
576+
logger.info("probe.target-complete", {
577+
probeId: cfg.id,
578+
key,
579+
state: result.state,
580+
durationMs: Date.now() - targetStart,
581+
});
565582
try {
566583
await writer.write(result);
567584
} catch (err) {
@@ -689,6 +706,35 @@ export function buildProbeInvoker(
689706
});
690707
}
691708
}
709+
logger.info("probe.tick-complete", {
710+
probeId: cfg.id,
711+
kind: cfg.kind,
712+
total: summary.total,
713+
passed: summary.passed,
714+
failed: summary.failed,
715+
durationMs: Date.now() - tickStart,
716+
discoveryFailed: summary.discoveryFailed ?? false,
717+
});
718+
// Structured per-service run summary — a single log line carrying
719+
// every target's outcome so Railway logs give a complete picture
720+
// without needing PocketBase.
721+
const summarySnap = tracker.snapshot();
722+
if (summarySnap.services.length > 0) {
723+
logger.info("probe.run-summary", {
724+
probeId: cfg.id,
725+
kind: cfg.kind,
726+
total: summary.total,
727+
passed: summary.passed,
728+
failed: summary.failed,
729+
durationMs: Date.now() - tickStart,
730+
services: summarySnap.services.map((s) => ({
731+
slug: s.slug,
732+
state: s.state,
733+
result: s.result,
734+
...(s.error ? { error: s.error } : {}),
735+
})),
736+
});
737+
}
692738
// B7: clear the tracker so the next snapshot reports no inflight run.
693739
// Done in `finally` so even an unexpected throw still leaves the
694740
// scheduler's view clean. R3-A.2: same prefixed id as registration.

0 commit comments

Comments
 (0)