feat: observe BI data source health
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
export const DATA_SOURCE_IDS = [
|
||||
'oneOsMileage',
|
||||
'hydrogenDatabase',
|
||||
'electricDatabase',
|
||||
'etcDatabase',
|
||||
] as const;
|
||||
|
||||
export type DataSourceId = typeof DATA_SOURCE_IDS[number];
|
||||
|
||||
interface Outcome {
|
||||
success: boolean;
|
||||
at: number;
|
||||
}
|
||||
|
||||
const MAX_OUTCOMES = 100;
|
||||
const outcomes = new Map<DataSourceId, Outcome[]>();
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
function lastMatching(entries: Outcome[], success: boolean): Outcome | undefined {
|
||||
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
||||
if (entries[index].success === success) return entries[index];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function recordDataSourceOutcome(
|
||||
source: DataSourceId,
|
||||
success: boolean,
|
||||
at = Date.now(),
|
||||
): void {
|
||||
const entries = outcomes.get(source) || [];
|
||||
entries.push({ success, at });
|
||||
if (entries.length > MAX_OUTCOMES) entries.splice(0, entries.length - MAX_OUTCOMES);
|
||||
outcomes.set(source, entries);
|
||||
}
|
||||
|
||||
export async function observeDataSource<T>(
|
||||
source: DataSourceId,
|
||||
operation: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const result = await operation();
|
||||
recordDataSourceOutcome(source, true);
|
||||
return result;
|
||||
} catch (error) {
|
||||
recordDataSourceOutcome(source, false);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDataSourceTelemetry(now = new Date()) {
|
||||
return {
|
||||
semantics: 'observed-requests' as const,
|
||||
windowSize: MAX_OUTCOMES,
|
||||
processStartedAt: startedAt,
|
||||
checkedAt: now.toISOString(),
|
||||
sources: DATA_SOURCE_IDS.map(source => {
|
||||
const entries = outcomes.get(source) || [];
|
||||
const failures = entries.filter(entry => !entry.success).length;
|
||||
const last = entries.at(-1);
|
||||
const lastSuccess = lastMatching(entries, true);
|
||||
const lastFailure = lastMatching(entries, false);
|
||||
return {
|
||||
source,
|
||||
state: !last ? 'unobserved' as const : last.success ? 'available' as const : 'failing' as const,
|
||||
attempts: entries.length,
|
||||
failures,
|
||||
failureRate: entries.length > 0 ? Math.round(failures / entries.length * 10_000) / 10_000 : null,
|
||||
lastSuccessAt: lastSuccess ? new Date(lastSuccess.at).toISOString() : null,
|
||||
lastFailureAt: lastFailure ? new Date(lastFailure.at).toISOString() : null,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function clearDataSourceTelemetry(): void {
|
||||
outcomes.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user