import {authStateSizes} from '../auth' import type { CloudProvider, CloudServiceDescriptor, CloudServiceType, EmulatedServiceStatus, RuntimeContainer, RuntimeDiagnostics, } from '../cloud-spi/types' import type {CloudProxyService} from './CloudProxyService' import {awsEndpoint} from './runtimeProbe' const DIAGNOSTICS_TIMEOUT_MS = 3_000 const processStartedAt = new Date() /** * Floci core service ids mapped to the console services they back. Only the * runtime's own ids are listed; whether a console page is actually available is * still decided by the adapter registry through `services()`. */ const AWS_CONSOLE_SERVICES: Record = { apigateway: ['apigateway'], appconfig: ['configuration'], cloudformation: ['iac'], codebuild: ['codebuild'], codedeploy: ['codedeploy'], codepipeline: ['codepipeline'], dynamodb: ['nosql'], ec2: ['compute', 'networking'], ecr: ['ecr'], ecs: ['ecs', 'fargate'], eks: ['k8s'], elasticache: ['elasticache'], elasticloadbalancing: ['loadbalancing', 'targetgroups'], email: ['email'], events: ['events'], iam: ['identity'], kafka: ['msk'], kafkaconnect: ['msk-connect'], kinesis: ['kinesis'], kms: ['kms'], lambda: ['serverless'], lightsail: ['lightsail'], logs: ['logs'], rds: ['database'], 'rds-data': ['database'], s3: ['storage'], secretsmanager: ['secrets'], sns: ['notifications'], sqs: ['messaging'], ssm: ['parameters'], states: ['workflows'], } type FlociHealth = {services?: Record; version?: unknown; edition?: unknown} type FlociDiagnose = {ec2?: {instances?: Array<{instanceId?: unknown; containerStatus?: unknown; sshHost?: unknown; sshPort?: unknown}>}} export type DiagnosticsDependencies = { fetch?: typeof fetch now?: () => Date startedAt?: Date authMode?: 'local' | 'oidc' activeSessions?: () => number } /** * Everything the console can learn about a runtime in one call. Each source is * optional: a runtime that is down, or one that does not publish a service list * or container diagnostics, still yields a partial report rather than an error. */ export async function runtimeDiagnostics( cloud: CloudProvider, proxy: Pick, dependencies: DiagnosticsDependencies = {}, ): Promise { const fetcher = dependencies.fetch ?? globalThis.fetch const now = dependencies.now?.() ?? new Date() const startedAt = dependencies.startedAt ?? processStartedAt const flociCore = cloud === 'aws' const [status, health, diagnose] = await Promise.all([ proxy.status(cloud, {includeServices: true}), flociCore ? fetchJson(fetcher, `${awsEndpoint()}/_floci/health`) : null, flociCore ? fetchJson(fetcher, `${awsEndpoint()}/_floci/diagnose`) : null, ]) return { cloud, checkedAt: now.toISOString(), runtime: { status: status.runtime, endpoint: status.endpoint, latencyMs: health?.latencyMs ?? null, error: status.error, version: stringOrNull(health?.body.version), edition: stringOrNull(health?.body.edition), }, emulatedServices: emulatedServices(health?.body.services, proxy.services(cloud)), consoleServices: status.services ?? [], containers: containers(diagnose?.body), console: { startedAt: startedAt.toISOString(), uptimeSeconds: Math.max(0, Math.floor((now.getTime() - startedAt.getTime()) / 1000)), authMode: dependencies.authMode ?? (process.env.FLOCI_UI_AUTH_MODE === 'oidc' ? 'oidc' : 'local'), activeSessions: (dependencies.activeSessions ?? (() => authStateSizes().sessions))(), }, } } function emulatedServices(reported: Record | undefined, descriptors: CloudServiceDescriptor[]): EmulatedServiceStatus[] | null { if (!reported || typeof reported !== 'object') return null const available = new Map(descriptors.filter((descriptor) => descriptor.availability === 'available').map((descriptor) => [descriptor.service, descriptor])) return Object.entries(reported) .map(([name, state]) => ({ name, state: typeof state === 'string' ? state : 'unknown', consolePages: (AWS_CONSOLE_SERVICES[name] ?? []) .map((service) => available.get(service)) .filter((descriptor): descriptor is CloudServiceDescriptor => Boolean(descriptor)) .map(({service, displayName, route}) => ({service, displayName, route})), })) .sort((left, right) => left.name.localeCompare(right.name)) } function containers(diagnose: FlociDiagnose | undefined): RuntimeContainer[] | null { const instances = diagnose?.ec2?.instances if (!Array.isArray(instances)) return null return instances .filter((instance) => typeof instance.instanceId === 'string') .map((instance) => ({ kind: 'ec2-instance' as const, resourceId: instance.instanceId as string, status: stringOrNull(instance.containerStatus), sshHost: stringOrNull(instance.sshHost), sshPort: typeof instance.sshPort === 'number' ? instance.sshPort : null, })) } async function fetchJson(fetcher: typeof fetch, url: string): Promise<{body: T; latencyMs: number} | null> { const startedAt = performance.now() try { const response = await fetcher(url, {signal: AbortSignal.timeout(DIAGNOSTICS_TIMEOUT_MS)}) if (!response.ok) return null return {body: await response.json() as T, latencyMs: Math.round(performance.now() - startedAt)} } catch { return null } } function stringOrNull(value: unknown): string | null { return typeof value === 'string' && value ? value : null }