UNPKG

nats-micro

Version:

NATS micro compatible extra-lightweight microservice library

345 lines (287 loc) 9.54 kB
import EventEmitter from 'events'; import { Broker } from './broker.js'; import { debug } from './debug.js'; import { Message, MicroserviceInfo, MicroserviceRegistration, MicroserviceRegistrationSubject, Request, Response, } from './types/index.js'; import { wrapMethod, attachThreadContext } from './utils/index.js'; export type MonitorDiscoveryOptions = { doNotClear: boolean; }; type ServerPingOptions = { // no options yet }; type ServerPingResponse = { server: { name: string, host: string; id: string, ver: string, // jetstream: boolean, // flags: number, // seq: number, // time: string, }; }; type ServerConnzOptions = { // no options yet auth: boolean; }; type ServerConnzItem = { cid: number; ip: string; start: string; account: string; authorized_user: string; }; type ServerConnz = { data: { connections: ServerConnzItem[]; }; }; type UserConnectEvent = { id?: string; server: { name: string; host: string; id: string; ver: string; }; client: { start: string; host: string; id: number; acc?: string; user?: string; }; }; type UserDisconnectEvent = UserConnectEvent; export type DiscoveredMicroservice = MicroserviceInfo & { firstFoundAt: Date; lastFoundAt: Date; connection: UserConnectEvent | undefined; }; export type MonitorOptions = { discoveryTimeout: number, }; export class Monitor { private readonly options: MonitorOptions; public readonly services: DiscoveredMicroservice[] = []; private discoveryInterval: NodeJS.Timer | undefined; private readonly connections: Record<string, UserConnectEvent> = {}; private readonly ee = new EventEmitter(); constructor( private readonly broker: Broker, private readonly systemBroker?: Broker, options: Partial<MonitorOptions> = {}, ) { this.options = { discoveryTimeout: 5000, ...options, }; const handleServiceRegistration = wrapMethod( this.broker, attachThreadContext('monitor', this.handleServiceRegistration.bind(this)), { method: 'handleServiceStatus', }, ); broker.on(MicroserviceRegistrationSubject, handleServiceRegistration); if (systemBroker) { systemBroker.on('$SYS.ACCOUNT.*.CONNECT', this.handleAccountConnect.bind(this)); systemBroker.on('$SYS.ACCOUNT.*.DISCONNECT', this.handleAccountDisconnect.bind(this)); } else { debug.monitor.error('Connection established/dropped monitoring disabled: no system broker'); } this.discoverConnections(); this.discover(this.options.discoveryTimeout); } private async discoverConnections(): Promise<void> { if (!this.systemBroker) { debug.monitor.error('Failed to discover current connections: no system broker'); return; } for await (const { data: server } of this.systemBroker.requestMany< ServerPingOptions, ServerPingResponse >( '$SYS.REQ.SERVER.PING', {}, { timeout: 3000, }, )) { debug.monitor.info(`Found server ${server.server.id}`); const { data: connz } = await this.systemBroker.request<ServerConnzOptions, ServerConnz>( `$SYS.REQ.SERVER.${server.server.id}.CONNZ`, { auth: true }, ); if (!connz) { debug.monitor.error(`Server ${server.server.id} did not response CONNZ request`); return; } debug.monitor.debug(`Server ${server.server.id} connections: ${connz.data.connections.map((c) => c.cid)}`); for (const connection of connz.data.connections) { this.connections[connection.cid] = { client: { id: connection.cid, host: connection.ip, start: connection.start, acc: connection.account, user: connection.authorized_user, }, server: { name: server.server.name, id: server.server.id, host: server.server.host, ver: server.server.ver, }, }; } for (const service of this.services) { const clientId = this.getServiceClientId(service); if (clientId) { const conn = this.connections[clientId]; if (conn) { service.connection = conn; debug.monitor.debug(`Updated microservice ${service.name}.${service.id} to client ${clientId}'s connection`); this.emit('added', service); } } } } } private handleServiceRegistration( req: Request<MicroserviceRegistration>, res: Response<void>, ): void { if (req.data.state === 'down') this.removeService(req.data.info); else this.saveService(req.data.info); res.sendNoResponse(); } private async handleAccountConnect(msg: Message<UserConnectEvent>): Promise<void> { const connection = msg.data; this.connections[connection.client.id] = msg.data; } private async handleAccountDisconnect(msg: Message<UserDisconnectEvent>): Promise<void> { const connection = msg.data; const clientId = connection.client.id; let count = 0; let idx = 0; while (idx < this.services.length) { const service = this.services[idx]; if (this.getServiceConnectionInfo(service)?.client.id === clientId) { const removed = this.services.splice(idx, 1); this.emit('removed', removed[0]); count++; } else idx++; } debug.monitor.debug(`Client ${clientId} disconnected, removing ${count} microservices`); delete (this.connections[connection.client.id]); this.emit('change', this.services); } private emit(event: 'added' | 'removed', service: MicroserviceInfo): void; private emit(event: 'change', services: MicroserviceInfo[]): void; // eslint-disable-next-line @typescript-eslint/no-explicit-any private emit(event: string, ...args: any[]): void { this.ee.emit(event, ...args); } public on(event: 'added' | 'removed', listener: (service: MicroserviceInfo) => void): void; public on(event: 'change', listener: (services: MicroserviceInfo[]) => void): void; // eslint-disable-next-line @typescript-eslint/no-explicit-any public on(event: string, listener: (...args: any[]) => void): void { this.ee.on(event, listener); } private getServiceClientId(service: MicroserviceInfo): number | undefined { const clientId = Number(service.metadata['_nats.client.id']); return Number.isNaN(clientId) ? undefined : clientId; } private getServiceConnectionInfo(service: MicroserviceInfo): UserConnectEvent | undefined { const clientId = this.getServiceClientId(service); return clientId ? this.connections[clientId] : undefined; } private saveService(service: MicroserviceInfo): void { const idx = this.services.findIndex((svc) => svc.id === service.id); const clientId = this.getServiceClientId(service); const connection = this.getServiceConnectionInfo(service); debug.monitor.debug(`${idx >= 0 ? 'Updated' : 'New'} microservice ${service.name}.${service.id} on client ${clientId}${connection ? '' : ' (unknown connection)'}`); if (idx >= 0) { this.services[idx] = { ...this.services[idx], ...service, lastFoundAt: new Date(), }; } else this.services.push({ firstFoundAt: new Date(), lastFoundAt: new Date(), connection: connection, ...service, }); this.emit('added', service); this.emit('change', this.services); } private removeService(service: MicroserviceInfo): void { const idx = this.services.findIndex((svc) => svc.id === service.id); if (idx >= 0) { debug.monitor.debug(`Removing microservice ${service.name}.${service.id}`); this.services.splice(idx, 1); this.emit('removed', service); this.emit('change', this.services); } } public async discover( timeout?: number, options?: Partial<MonitorDiscoveryOptions>, ): Promise<void> { const servicesSnapshot = [...this.services]; const servicesIterator = this.broker.requestMany<string, MicroserviceInfo>( '$SRV.INFO', '', { limit: -1, timeout: timeout ?? this.options.discoveryTimeout, }, ); const services: MicroserviceInfo[] = []; for await (const service of servicesIterator) { services.push(service.data); this.saveService(service.data); } if (!options?.doNotClear) { // compare to this.services BEFORE requestMany was made // as there can be new additions after $SRV.INFO sent and // before we get to this point const servicesToForget = servicesSnapshot .filter((oldSvc) => !services.some((newSvc) => newSvc.id === oldSvc.id)); if (servicesToForget.length > 0) { debug.monitor.info(`Removing microservices ${servicesToForget.map((svc) => `${svc.name}.${svc.id}`)} that have not responded during discovery`); for (const serviceToForger of servicesToForget) this.removeService(serviceToForger); } } } public startPeriodicDiscovery( interval: number, discoveryTimeout?: number, ) { this.stopPeriodicDiscovery(); this.discoveryInterval = setInterval( () => this.discover(discoveryTimeout), interval, ); } public stopPeriodicDiscovery() { if (this.discoveryInterval) { clearInterval(this.discoveryInterval); this.discoveryInterval = undefined; } } }