158 lines
4.8 KiB
TypeScript
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,
|
|
};
|
|
}
|