@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
122 lines (121 loc) • 4.82 kB
JavaScript
import { ErrorCode, ServiceUnavailableError } from '@directus/errors';
import { useBus } from '../../bus/index.js';
import emitter from '../../emitter.js';
import { useLogger } from '../../logger/index.js';
import { getAllowedLogLevels } from '../../utils/get-allowed-log-levels.js';
import { getLogsController, LogsController } from '../controllers/index.js';
import { handleWebSocketError, WebSocketError } from '../errors.js';
import { WebSocketLogsMessage } from '../messages.js';
import { fmtMessage, getMessageType } from '../utils/message.js';
const logger = useLogger();
export class LogsHandler {
controller;
messenger;
availableLogLevels;
logLevelValueMap;
subscriptions;
constructor(controller) {
controller = controller ?? getLogsController();
if (!controller) {
throw new ServiceUnavailableError({ service: 'ws', reason: 'WebSocket server is not initialized' });
}
this.controller = controller;
this.messenger = useBus();
this.availableLogLevels = Object.keys(logger.levels.values);
this.logLevelValueMap = Object.fromEntries(Object.entries(logger.levels.values).map(([key, value]) => [value, key]));
this.subscriptions = this.availableLogLevels.reduce((acc, logLevel) => {
acc[logLevel] = new Set();
return acc;
}, {});
this.bindWebSocket();
this.messenger.subscribe('logs', (message) => {
const { log, nodeId } = JSON.parse(message);
const logLevel = this.logLevelValueMap[log['level']];
if (logLevel) {
this.subscriptions[logLevel]?.forEach((subscription) => subscription.send(fmtMessage('logs', { data: log }, nodeId)));
}
});
}
/**
* Hook into websocket client lifecycle events
*/
bindWebSocket() {
// listen to incoming messages on the connected websockets
emitter.onAction('websocket.logs', ({ client, message }) => {
if (!['subscribe', 'unsubscribe'].includes(getMessageType(message)))
return;
try {
const parsedMessage = WebSocketLogsMessage.parse(message);
this.onMessage(client, parsedMessage).catch((error) => {
// this catch is required because the async onMessage function is not awaited
handleWebSocketError(client, error, 'logs');
});
}
catch (error) {
handleWebSocketError(client, error, 'logs');
}
});
// unsubscribe when a connection drops
emitter.onAction('websocket.error', ({ client }) => this.unsubscribe(client));
emitter.onAction('websocket.close', ({ client }) => this.unsubscribe(client));
}
/**
* Register a logs subscription
* @param logLevel
* @param client
*/
subscribe(logLevel, client) {
let allowedLogLevelNames = [];
try {
allowedLogLevelNames = Object.keys(getAllowedLogLevels(logLevel));
}
catch (error) {
throw new WebSocketError('logs', ErrorCode.InvalidPayload, error.message);
}
for (const availableLogLevel of this.availableLogLevels) {
if (allowedLogLevelNames.includes(availableLogLevel)) {
this.subscriptions[availableLogLevel]?.add(client);
}
else {
this.subscriptions[availableLogLevel]?.delete(client);
}
}
}
/**
* Remove a logs subscription
* @param client WebSocketClient
*/
unsubscribe(client) {
for (const availableLogLevel of this.availableLogLevels) {
this.subscriptions[availableLogLevel]?.delete(client);
}
}
/**
* Handle incoming (un)subscribe requests
*/
async onMessage(client, message) {
const accountability = client.accountability;
if (!accountability?.admin) {
throw new WebSocketError('logs', ErrorCode.Forbidden, `You don't have permission to access this.`);
}
if (message.type === 'subscribe') {
try {
const logLevel = message.log_level;
this.subscribe(logLevel, client);
client.send(fmtMessage('logs', { event: 'subscribe', log_level: logLevel }));
}
catch (err) {
handleWebSocketError(client, err, 'subscribe');
}
}
else if (message.type === 'unsubscribe') {
try {
this.unsubscribe(client);
client.send(fmtMessage('logs', { event: 'unsubscribe' }));
}
catch (err) {
handleWebSocketError(client, err, 'unsubscribe');
}
}
}
}