|
@@ -1,6 +1,10 @@
|
|
|
import winston, { Logger, LoggerOptions } from 'winston'
|
|
|
import escFormat from '@elastic/ecs-winston-format'
|
|
|
+import { ElasticsearchTransport } from 'winston-elasticsearch'
|
|
|
import { ReadonlyConfig } from '../../types'
|
|
|
+import { blake2AsHex } from '@polkadot/util-crypto'
|
|
|
+import { Format } from 'logform'
|
|
|
+import NodeCache from 'node-cache'
|
|
|
|
|
|
const cliColors = {
|
|
|
error: 'red',
|
|
@@ -12,6 +16,26 @@ const cliColors = {
|
|
|
|
|
|
winston.addColors(cliColors)
|
|
|
|
|
|
+const pausedLogs = new NodeCache({
|
|
|
+ deleteOnExpire: true,
|
|
|
+})
|
|
|
+
|
|
|
+// Pause log for a specified time period
|
|
|
+const pauseFormat: (opts: { id: string }) => Format = winston.format((info, opts: { id: string }) => {
|
|
|
+ if (info['@pauseFor']) {
|
|
|
+ const messageHash = blake2AsHex(`${opts.id}:${info.level}:${info.message}`)
|
|
|
+ if (!pausedLogs.has(messageHash)) {
|
|
|
+ pausedLogs.set(messageHash, null, info['@pauseFor'])
|
|
|
+ info.message += ` (this log message will be skipped for the next ${info['@pauseFor']}s)`
|
|
|
+ delete info['@pauseFor']
|
|
|
+ return info
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return info
|
|
|
+})
|
|
|
+
|
|
|
const cliFormat = winston.format.combine(
|
|
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
|
|
winston.format.metadata({ fillExcept: ['label', 'level', 'timestamp', 'message'] }),
|
|
@@ -25,30 +49,47 @@ const cliFormat = winston.format.combine(
|
|
|
|
|
|
export class LoggingService {
|
|
|
private rootLogger: Logger
|
|
|
+ private esTransport: ElasticsearchTransport | undefined
|
|
|
|
|
|
- private constructor(options: LoggerOptions) {
|
|
|
+ private constructor(options: LoggerOptions, esTransport?: ElasticsearchTransport) {
|
|
|
+ this.esTransport = esTransport
|
|
|
this.rootLogger = winston.createLogger(options)
|
|
|
}
|
|
|
|
|
|
public static withAppConfig(config: ReadonlyConfig): LoggingService {
|
|
|
- const transports: winston.LoggerOptions['transports'] = [
|
|
|
- new winston.transports.File({
|
|
|
- filename: `${config.directories.logs}/logs.json`,
|
|
|
- level: config.log?.file || 'debug',
|
|
|
- format: escFormat(),
|
|
|
- }),
|
|
|
- ]
|
|
|
+ const esTransport = new ElasticsearchTransport({
|
|
|
+ level: config.log?.elastic || 'warn',
|
|
|
+ format: winston.format.combine(pauseFormat({ id: 'es' }), escFormat()),
|
|
|
+ flushInterval: 5000,
|
|
|
+ source: config.id,
|
|
|
+ clientOpts: {
|
|
|
+ node: {
|
|
|
+ url: new URL(config.endpoints.elasticSearch),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ const fileTransport = new winston.transports.File({
|
|
|
+ filename: `${config.directories.logs}/logs.json`,
|
|
|
+ level: config.log?.file || 'debug',
|
|
|
+ format: winston.format.combine(pauseFormat({ id: 'file' }), escFormat()),
|
|
|
+ })
|
|
|
+
|
|
|
+ const transports: winston.LoggerOptions['transports'] = [esTransport, fileTransport]
|
|
|
+
|
|
|
if (config.log?.console) {
|
|
|
- transports.push(
|
|
|
- new winston.transports.Console({
|
|
|
- level: config.log.console,
|
|
|
- format: cliFormat,
|
|
|
- })
|
|
|
- )
|
|
|
+ const consoleTransport = new winston.transports.Console({
|
|
|
+ level: config.log.console,
|
|
|
+ format: winston.format.combine(pauseFormat({ id: 'cli' }), cliFormat),
|
|
|
+ })
|
|
|
+ transports.push(consoleTransport)
|
|
|
}
|
|
|
- return new LoggingService({
|
|
|
- transports,
|
|
|
- })
|
|
|
+ return new LoggingService(
|
|
|
+ {
|
|
|
+ transports,
|
|
|
+ },
|
|
|
+ esTransport
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
public static withCLIConfig(): LoggingService {
|
|
@@ -56,7 +97,7 @@ export class LoggingService {
|
|
|
transports: new winston.transports.Console({
|
|
|
// Log everything to stderr, only the command output value will be written to stdout
|
|
|
stderrLevels: Object.keys(winston.config.npm.levels),
|
|
|
- format: cliFormat,
|
|
|
+ format: winston.format.combine(pauseFormat({ id: 'cli' }), cliFormat),
|
|
|
}),
|
|
|
})
|
|
|
}
|
|
@@ -65,7 +106,11 @@ export class LoggingService {
|
|
|
return this.rootLogger.child({ label, ...meta })
|
|
|
}
|
|
|
|
|
|
- public end(): void {
|
|
|
+ public async end(): Promise<void> {
|
|
|
+ if (this.esTransport) {
|
|
|
+ await this.esTransport.flush()
|
|
|
+ }
|
|
|
this.rootLogger.end()
|
|
|
+ await Promise.all(this.rootLogger.transports.map((t) => new Promise((resolve) => t.on('finish', resolve))))
|
|
|
}
|
|
|
}
|