diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js index 0920fb8b303..1bd9b8b84fb 100644 --- a/app/api/server/v1/misc.js +++ b/app/api/server/v1/misc.js @@ -13,7 +13,7 @@ import { settings } from '../../../settings/server'; import { API } from '../api'; import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; import { getURL } from '../../../utils/lib/getURL'; -import { StdOut } from '../../../logger/server/streamer'; +import { getLogs } from '../../../../server/stream/stdout'; import { SystemLogger } from '../../../../server/lib/logger/system'; API.v1.addRoute('me', { authRequired: true }, { @@ -195,7 +195,7 @@ API.v1.addRoute('stdout.queue', { authRequired: true }, { if (!hasPermission(this.userId, 'view-logs')) { return API.v1.unauthorized(); } - return API.v1.success({ queue: StdOut.queue }); + return API.v1.success({ queue: getLogs() }); }, }); diff --git a/app/logger/server/index.js b/app/logger/server/index.js index f535468e5d9..5630b3a0aa2 100644 --- a/app/logger/server/index.js +++ b/app/logger/server/index.js @@ -1,7 +1,2 @@ -import { Logger } from '../../../server/lib/logger/Logger'; -import './streamer'; - // TODO there are imports pointing to this file still, ideally we should point everything to "/server/lib/logger/Logger" and remove this file -export { - Logger, -}; +export { Logger } from '../../../server/lib/logger/Logger'; diff --git a/app/logger/server/streamer.js b/app/logger/server/streamer.js deleted file mode 100644 index 2dc5e56d1db..00000000000 --- a/app/logger/server/streamer.js +++ /dev/null @@ -1,71 +0,0 @@ -import { EventEmitter } from 'events'; - -import { Meteor } from 'meteor/meteor'; -import { EJSON } from 'meteor/ejson'; -import { Log } from 'meteor/logging'; - -import { settings } from '../../settings/server'; -import notifications from '../../notifications/server/lib/Notifications'; - -const processString = function(string, date) { - let obj; - try { - if (string[0] === '{') { - obj = EJSON.parse(string); - } else { - obj = { - message: string, - time: date, - level: 'info', - }; - } - return Log.format(obj, { color: true }); - } catch (error) { - return string; - } -}; - -export const StdOut = Object.assign(new EventEmitter(), { - queue: [], -}); - -const { write } = process.stdout; - -const maxInt = 2147483647; -let queueSize = 0; - -process.stdout.write = (...args) => { - write.apply(process.stdout, args); - const date = new Date(); - const string = processString(args[0], date); - const item = { - id: `logid-${ queueSize }`, - string, - ts: date, - }; - StdOut.queue.push(item); - - queueSize = (queueSize + 1) & maxInt; - - const limit = settings.get('Log_View_Limit') || 1000; - if (queueSize > limit) { - StdOut.queue.shift(); - } - - StdOut.emit('write', string, item); -}; - -Meteor.startup(() => { - const handler = (string, item) => { - // TODO having this as 'emitWithoutBroadcast' will not sent this data to ddp-streamer, so this data - // won't be available when using micro services. - notifications.streamStdout.emitWithoutBroadcast('stdout', { - ...item, - }); - }; - - // do not emit to StdOut if moleculer log level set to debug because it creates an infinite loop - if (String(process.env.MOLECULER_LOG_LEVEL).toLowerCase() !== 'debug') { - StdOut.on('write', handler); - } -}); diff --git a/server/lib/logger/getPino.ts b/server/lib/logger/getPino.ts index 37f02f01957..60f90fca547 100644 --- a/server/lib/logger/getPino.ts +++ b/server/lib/logger/getPino.ts @@ -1,6 +1,9 @@ import { pino } from 'pino'; import type { P } from 'pino'; +// make sure log queue is set up, so pino uses the overwritten process.stdout.write +import './logQueue'; + // add support to multiple params on the log commands, i.e.: // logger.info('user', Meteor.user()); // will print: {"level":30,"time":1629814080968,"msg":"user {\"username\": \"foo\"}"} function logMethod(this: P.Logger, args: unknown[], method: any): void { diff --git a/server/lib/logger/logQueue.ts b/server/lib/logger/logQueue.ts new file mode 100644 index 00000000000..b87fa0d0436 --- /dev/null +++ b/server/lib/logger/logQueue.ts @@ -0,0 +1,61 @@ +import EventEmitter from 'events'; + +type LogQueue = { + id: string; + data: string; + ts: Date; +}; + +const queue: LogQueue[] = []; +const maxInt = 2147483647; +let queueLimit = 1000; +let queueSize = 0; + +export function setQueueLimit(limit: number): void { + queueLimit = limit; + + if (queueSize > queueLimit) { + queue.splice(0, queueSize - queueLimit); + } +} + +export function getQueuedLogs(): LogQueue[] { + return queue; +} + +export const logEntries = new EventEmitter(); + +const { write } = process.stdout; + +function queueWrite(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean; +function queueWrite(str: Uint8Array | string, encoding?: string, cb?: (err?: Error) => void): boolean; +function queueWrite(...args: any): boolean { + write.apply(process.stdout, args); + + const [str] = args; + if (typeof str !== 'string') { + return false; + } + + const date = new Date(); + const item = { + id: `logid-${ queueSize }`, + data: str, + ts: date, + }; + queue.push(item); + + queueSize = (queueSize + 1) & maxInt; + + if (queueSize > queueLimit) { + queue.shift(); + } + + logEntries.emit('log', item); + + return true; +} + +if (String(process.env.MOLECULER_LOG_LEVEL).toLowerCase() !== 'debug') { + process.stdout.write = queueWrite; +} diff --git a/server/lib/logger/startup.ts b/server/lib/logger/startup.ts index 1b9c1cd0e8e..a0f57049bae 100644 --- a/server/lib/logger/startup.ts +++ b/server/lib/logger/startup.ts @@ -1,8 +1,15 @@ import { settings } from '../../../app/settings/server'; import { logLevel, LogLevelSetting } from './logLevel'; +import { setQueueLimit } from './logQueue'; settings.get('Log_Level', (_key, value) => { if (value != null) { logLevel.emit('changed', String(value) as LogLevelSetting); } }); + +settings.get('Log_View_Limit', (_key, value) => { + if (typeof value === 'number') { + setQueueLimit(value); + } +}); diff --git a/server/main.js b/server/main.js index 7a37123b9f2..f806c978ef7 100644 --- a/server/main.js +++ b/server/main.js @@ -76,6 +76,7 @@ import './publications/settings'; import './publications/spotlight'; import './publications/subscription'; import './routes/avatar'; +import './stream/stdout'; import './stream/streamBroadcast'; import './features/EmailInbox/index'; diff --git a/server/stream/stdout.ts b/server/stream/stdout.ts new file mode 100644 index 00000000000..c6fefc60b31 --- /dev/null +++ b/server/stream/stdout.ts @@ -0,0 +1,41 @@ +import { EJSON } from 'meteor/ejson'; +import { Log } from 'meteor/logging'; + +import notifications from '../../app/notifications/server/lib/Notifications'; +import { getQueuedLogs, logEntries } from '../lib/logger/logQueue'; + +const processString = function(string: string, date: Date): string { + let obj; + try { + if (string[0] === '{') { + obj = EJSON.parse(string); + } else { + obj = { + message: string, + time: date, + level: 'info', + }; + } + return Log.format(obj, { color: true }); + } catch (error) { + return string; + } +}; + +const transformLog = function(item: any): { id: string; string: string; ts: Date } { + return { + id: item.id, + string: processString(item.data, item.ts), + ts: item.ts, + }; +}; + +logEntries.on('log', (item) => { + // TODO having this as 'emitWithoutBroadcast' will not sent this data to ddp-streamer, so this data + // won't be available when using micro services. + notifications.streamStdout.emitWithoutBroadcast('stdout', transformLog(item)); +}); + +export function getLogs(): { id: string; string: string; ts: Date }[] { + return getQueuedLogs().map(transformLog); +} diff --git a/typings.d.ts b/typings.d.ts index 11f69dff1ce..2f202dc49b0 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -3,6 +3,7 @@ declare module 'meteor/littledata:synced-cron'; declare module 'meteor/promise'; declare module 'meteor/ddp-common'; declare module 'meteor/routepolicy'; +declare module 'meteor/logging'; declare module 'xml-encryption'; declare module 'webdav';