learning_ai_notes/backend/src/lib/dependency-readiness.ts

158 lines
4.8 KiB
TypeScript

import { checkService, type ServiceCheck, type ServiceTarget } from '@bytelyst/monitoring';
import { getNotesMcpToolsForRegistration } from '../mcp/register-note-tools.js';
import { config } from './config.js';
import { initDatastore } from './datastore.js';
import { getEncryptionReadiness } from './field-encrypt.js';
export type DependencyStatus = ServiceCheck['status'];
export type DependencyCheck = ServiceCheck & {
details?: Record<string, unknown>;
};
export interface DependencyReadinessReport {
overall: 'ready' | 'degraded' | 'down';
timestamp: string;
dependencies: DependencyCheck[];
summary: {
healthy: number;
unhealthy: number;
unreachable: number;
total: number;
};
}
type ServiceChecker = (target: ServiceTarget, opts?: { timeoutMs?: number }) => Promise<ServiceCheck>;
function elapsedSince(start: number): number {
return Math.round(performance.now() - start);
}
function normalizeServiceBaseUrl(url: string): string {
const trimmed = url.replace(/\/+$/, '');
return trimmed.endsWith('/api') ? trimmed.slice(0, -4) : trimmed;
}
function summarize(checks: DependencyCheck[]): DependencyReadinessReport['summary'] {
return {
healthy: checks.filter(check => check.status === 'healthy').length,
unhealthy: checks.filter(check => check.status === 'unhealthy').length,
unreachable: checks.filter(check => check.status === 'unreachable').length,
total: checks.length,
};
}
function overallFrom(summary: DependencyReadinessReport['summary']): DependencyReadinessReport['overall'] {
if (summary.healthy === summary.total) {
return 'ready';
}
if (summary.healthy === 0) {
return 'down';
}
return 'degraded';
}
export async function checkDatastoreReadiness(): Promise<DependencyCheck> {
const start = performance.now();
try {
const healthy = await initDatastore().isHealthy();
return {
name: 'datastore',
url: config.DB_PROVIDER,
status: healthy ? 'healthy' : 'unhealthy',
responseTimeMs: elapsedSince(start),
details: { provider: config.DB_PROVIDER },
...(healthy ? {} : { error: 'Datastore provider reported unhealthy' }),
};
} catch (error) {
return {
name: 'datastore',
url: config.DB_PROVIDER,
status: 'unreachable',
responseTimeMs: elapsedSince(start),
error: error instanceof Error ? error.message : String(error),
details: { provider: config.DB_PROVIDER },
};
}
}
export async function checkEncryptionReadiness(): Promise<DependencyCheck> {
const start = performance.now();
const readiness = getEncryptionReadiness();
return {
name: 'encryption',
url: readiness.keyProvider,
status: readiness.ready ? 'healthy' : 'unhealthy',
responseTimeMs: elapsedSince(start),
details: {
enabled: readiness.enabled,
keyProvider: readiness.keyProvider,
},
...(readiness.ready ? {} : { error: 'Field encryption is not ready' }),
};
}
async function checkHttpDependency(
checker: ServiceChecker,
target: ServiceTarget,
timeoutMs: number,
): Promise<DependencyCheck> {
return checker(target, { timeoutMs });
}
async function checkMcpReadiness(
checker: ServiceChecker,
timeoutMs: number,
): Promise<DependencyCheck> {
const baseUrl = normalizeServiceBaseUrl(config.MCP_SERVER_URL);
const tools = getNotesMcpToolsForRegistration();
const serviceCheck = await checker({ name: 'mcp-server', url: baseUrl, path: '/health' }, { timeoutMs });
const hasTools = tools.length > 0;
const status: DependencyStatus =
serviceCheck.status === 'healthy' && hasTools ? 'healthy' : serviceCheck.status;
return {
...serviceCheck,
name: 'mcp',
status: hasTools ? status : 'unhealthy',
details: {
...(serviceCheck.details ?? {}),
registeredToolCount: tools.length,
registeredToolsHealthy: hasTools,
},
...(!hasTools ? { error: 'No NoteLett MCP tools are registered locally' } : {}),
};
}
export async function getDependencyReadiness(options: {
timeoutMs?: number;
serviceChecker?: ServiceChecker;
} = {}): Promise<DependencyReadinessReport> {
const timeoutMs = options.timeoutMs ?? 1500;
const serviceChecker = options.serviceChecker ?? checkService;
const dependencies = await Promise.all([
checkDatastoreReadiness(),
checkEncryptionReadiness(),
checkHttpDependency(
serviceChecker,
{ name: 'platform-service', url: normalizeServiceBaseUrl(config.PLATFORM_SERVICE_URL), path: '/health' },
timeoutMs,
),
checkHttpDependency(
serviceChecker,
{ name: 'extraction-service', url: normalizeServiceBaseUrl(config.EXTRACTION_SERVICE_URL), path: '/health' },
timeoutMs,
),
checkMcpReadiness(serviceChecker, timeoutMs),
]);
const summary = summarize(dependencies);
return {
overall: overallFrom(summary),
timestamp: new Date().toISOString(),
dependencies,
summary,
};
}