nats-micro
Version:
NATS micro compatible extra-lightweight microservice library
345 lines (287 loc) • 9.54 kB
text/typescript
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;
}
}
}