2017-11-03 02:09:34 +02:00
const moment = require ( 'moment' ) ;
2020-12-28 17:15:30 +02:00
const { sprintf } = require ( 'sprintf-js' ) ;
2021-01-20 17:49:02 +02:00
const Mutex = require ( 'async-mutex' ) . Mutex ;
const writeToFileMutex_ = new Mutex ( ) ;
2017-06-23 23:32:24 +02:00
2020-10-09 19:35:46 +02:00
export enum TargetType {
Database = 'database' ,
File = 'file' ,
Console = 'console' ,
}
2021-05-03 12:55:38 +02:00
export enum LogLevel {
2020-10-09 19:35:46 +02:00
None = 0 ,
Error = 10 ,
Warn = 20 ,
Info = 30 ,
Debug = 40 ,
}
2023-08-28 15:30:56 +02:00
type FormatFunction = ( level : LogLevel , targetPrefix? : string ) = > string ;
2021-10-15 13:24:22 +02:00
interface TargetOptions {
2020-11-12 21:29:22 +02:00
level? : LogLevel ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
database? : any ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-12 21:29:22 +02:00
console? : any ;
prefix? : string ;
path? : string ;
source? : string ;
2020-12-28 17:15:30 +02:00
// Default message format
2023-08-28 15:30:56 +02:00
format? : string | FormatFunction ;
2020-10-09 19:35:46 +02:00
}
2021-10-15 13:24:22 +02:00
interface Target extends TargetOptions {
type : TargetType ;
}
2024-01-18 13:26:32 +02:00
interface LastEntriesOptions {
levels? : LogLevel [ ] ;
filter? : string ;
}
2020-11-25 11:40:54 +02:00
export interface LoggerWrapper {
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
debug : Function ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
info : Function ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
warn : Function ;
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
error : Function ;
}
2023-07-27 17:05:56 +02:00
interface FsDriver {
appendFile : ( path : string , content : string , encoding : string ) = > Promise < void > ;
}
const dummyFsDriver : FsDriver = {
appendFile : async ( _path : string , _content : string , _encoding : string ) = > { } ,
} ;
2017-06-23 23:32:24 +02:00
class Logger {
2020-10-09 19:35:46 +02:00
// For backward compatibility
public static LEVEL_NONE = LogLevel . None ;
public static LEVEL_ERROR = LogLevel . Error ;
public static LEVEL_WARN = LogLevel . Warn ;
public static LEVEL_INFO = LogLevel . Info ;
public static LEVEL_DEBUG = LogLevel . Debug ;
2023-07-27 17:05:56 +02:00
public static fsDriver_ : FsDriver | null = null ;
private static globalLogger_ : Logger | null = null ;
2020-10-09 19:35:46 +02:00
2020-11-12 21:13:28 +02:00
private targets_ : Target [ ] = [ ] ;
private level_ : LogLevel = LogLevel . Info ;
2023-07-27 17:05:56 +02:00
private lastDbCleanup_ : number = Date . now ( ) ;
2023-06-30 10:07:03 +02:00
private enabled_ = true ;
2017-06-23 23:32:24 +02:00
2023-03-06 16:22:01 +02:00
public static fsDriver() {
2023-07-27 17:05:56 +02:00
if ( ! Logger . fsDriver_ ) Logger . fsDriver_ = dummyFsDriver ;
2017-07-05 23:52:31 +02:00
return Logger . fsDriver_ ;
}
2021-02-09 19:54:29 +02:00
public get enabled ( ) : boolean {
return this . enabled_ ;
}
public set enabled ( v : boolean ) {
this . enabled_ = v ;
}
2023-10-19 18:11:20 +02:00
public status ( ) : string {
const output : string [ ] = [ ] ;
output . push ( ` Enabled: ${ this . enabled } ` ) ;
output . push ( ` Level: ${ this . level ( ) } ` ) ;
output . push ( ` Targets: ${ this . targets ( ) . map ( t = > t . type ) . join ( ', ' ) } ` ) ;
return output . join ( '\n' ) ;
}
2020-11-19 17:25:02 +02:00
public static initializeGlobalLogger ( logger : Logger ) {
this . globalLogger_ = logger ;
}
2024-04-26 17:07:16 +02:00
public logFilePath() {
const fileTarget = this . targets ( ) . find ( t = > t . type === TargetType . File ) ;
if ( ! fileTarget ) return '' ;
return fileTarget . path || '' ;
}
2021-02-09 19:54:29 +02:00
public static get globalLogger ( ) : Logger {
2023-02-20 18:05:00 +02:00
if ( ! this . globalLogger_ ) {
// The global logger normally is initialized early, so we shouldn't
// end up here. However due to early event handlers, it might happen
// and in this case we want to know about it. So we print this
// warning, and also flag the log statements using `[UNINITIALIZED
// GLOBAL LOGGER]` so that we know from where the incorrect log
// statement comes from.
console . warn ( 'Logger: Trying to access globalLogger, but it has not been initialized. Make sure that initializeGlobalLogger() has been called before logging. Will use the console as fallback.' ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-02-20 18:05:00 +02:00
const output : any = {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-02-20 18:05:00 +02:00
log : ( level : LogLevel , prefix : string , . . . object : any [ ] ) = > {
// eslint-disable-next-line no-console
console . info ( ` [UNINITIALIZED GLOBAL LOGGER] ${ this . levelIdToString ( level ) } : ${ prefix } : ` , object ) ;
} ,
} ;
return output ;
// throw new Error('Global logger has not been initialized!!');
}
2020-11-19 17:25:02 +02:00
return this . globalLogger_ ;
}
2023-03-06 16:22:01 +02:00
public static create ( prefix : string ) : LoggerWrapper {
2020-11-19 17:25:02 +02:00
return {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
debug : ( . . . object : any [ ] ) = > this . globalLogger . log ( LogLevel . Debug , prefix , . . . object ) ,
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
info : ( . . . object : any [ ] ) = > this . globalLogger . log ( LogLevel . Info , prefix , . . . object ) ,
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
warn : ( . . . object : any [ ] ) = > this . globalLogger . log ( LogLevel . Warn , prefix , . . . object ) ,
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-11-19 17:25:02 +02:00
error : ( . . . object : any [ ] ) = > this . globalLogger . log ( LogLevel . Error , prefix , . . . object ) ,
} ;
}
2021-12-20 16:47:50 +02:00
public setLevel ( level : LogLevel ) {
const previous = this . level_ ;
2017-06-23 23:32:24 +02:00
this . level_ = level ;
2021-12-20 16:47:50 +02:00
return previous ;
2017-06-23 23:32:24 +02:00
}
2023-03-06 16:22:01 +02:00
public level() {
2017-06-23 23:32:24 +02:00
return this . level_ ;
}
2023-03-06 16:22:01 +02:00
public targets() {
2018-01-31 00:35:50 +02:00
return this . targets_ ;
}
2023-07-27 17:05:56 +02:00
public addTarget ( type : TargetType , options : TargetOptions | null = null ) {
2020-03-14 01:46:14 +02:00
const target = { type : type } ;
for ( const n in options ) {
2017-06-23 23:32:24 +02:00
if ( ! options . hasOwnProperty ( n ) ) continue ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2021-10-15 13:24:22 +02:00
( target as any ) [ n ] = ( options as any ) [ n ] ;
2017-06-23 23:32:24 +02:00
}
this . targets_ . push ( target ) ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public objectToString ( object : any ) {
2017-07-06 21:48:17 +02:00
let output = '' ;
if ( typeof object === 'object' ) {
if ( object instanceof Error ) {
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-10-09 19:35:46 +02:00
object = object as any ;
2017-07-06 21:48:17 +02:00
output = object . toString ( ) ;
2019-09-19 23:51:18 +02:00
if ( object . code ) output += ` \ nCode: ${ object . code } ` ;
if ( object . headers ) output += ` \ nHeader: ${ JSON . stringify ( object . headers ) } ` ;
if ( object . request ) output += ` \ nRequest: ${ object . request . substr ? object . request . substr ( 0 , 1024 ) : '' } ` ;
if ( object . stack ) output += ` \ n ${ object . stack } ` ;
2017-07-06 21:48:17 +02:00
} else {
output = JSON . stringify ( object ) ;
}
} else {
output = object ;
}
2019-07-29 15:43:53 +02:00
return output ;
2017-07-07 19:19:24 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public objectsToString ( . . . object : any [ ] ) {
2020-03-14 01:46:14 +02:00
const output = [ ] ;
2024-03-09 12:33:05 +02:00
if ( object . length === 1 ) {
// Quoting when there is only one argument can make the log more difficult to read,
// particularly when formatting is handled elsewhere.
output . push ( this . objectToString ( object [ 0 ] ) ) ;
} else {
for ( let i = 0 ; i < object . length ; i ++ ) {
output . push ( ` " ${ this . objectToString ( object [ i ] ) } " ` ) ;
}
2017-07-07 19:19:24 +02:00
}
return output . join ( ', ' ) ;
2017-07-06 21:48:17 +02:00
}
2023-03-06 16:22:01 +02:00
public static databaseCreateTableSql() {
2020-03-14 01:46:14 +02:00
const output = `
2017-07-07 19:19:24 +02:00
CREATE TABLE IF NOT EXISTS logs (
2017-07-06 21:48:17 +02:00
id INTEGER PRIMARY KEY ,
source TEXT ,
level INT NOT NULL ,
message TEXT NOT NULL ,
\ ` timestamp \` INT NOT NULL
) ;
` ;
2019-07-29 15:43:53 +02:00
return output . split ( '\n' ) . join ( ' ' ) ;
2017-07-06 21:48:17 +02:00
}
2017-07-07 19:19:24 +02:00
// Only for database at the moment
2024-01-18 13:26:32 +02:00
public async lastEntries ( limit = 100 , options : LastEntriesOptions | null = null ) {
2018-01-31 21:51:29 +02:00
if ( options === null ) options = { } ;
2020-10-09 19:35:46 +02:00
if ( ! options . levels ) options . levels = [ LogLevel . Debug , LogLevel . Info , LogLevel . Warn , LogLevel . Error ] ;
2018-01-31 21:51:29 +02:00
if ( ! options . levels . length ) return [ ] ;
2017-07-07 19:19:24 +02:00
for ( let i = 0 ; i < this . targets_ . length ; i ++ ) {
const target = this . targets_ [ i ] ;
2022-07-23 09:31:32 +02:00
if ( target . type === 'database' ) {
2024-01-18 13:26:32 +02:00
const sql = [ ` SELECT * FROM logs WHERE level IN ( ${ options . levels . join ( ',' ) } ) ` ] ;
const sqlParams = [ ] ;
if ( options . filter ) {
sql . push ( 'AND message LIKE ?' ) ;
sqlParams . push ( ` % ${ options . filter } % ` ) ;
}
sql . push ( 'ORDER BY timestamp DESC' ) ;
if ( limit !== null ) {
sql . push ( 'LIMIT ?' ) ;
sqlParams . push ( limit ) ;
}
return await target . database . selectAll ( sql . join ( ' ' ) , sqlParams ) ;
2017-07-07 19:19:24 +02:00
}
}
return [ ] ;
}
2023-07-27 17:05:56 +02:00
public targetLevel ( target : Target ) : LogLevel {
if ( 'level' in target ) return target . level as LogLevel ;
2019-07-29 15:43:53 +02:00
return this . level ( ) ;
2019-05-11 18:53:56 +02:00
}
2024-04-26 16:08:37 +02:00
private logInfoToString ( level : LogLevel , prefix : string | null , . . . object : unknown [ ] ) {
const timestamp = moment ( ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ;
const line = [ timestamp ] ;
if ( prefix ) line . push ( prefix ) ;
const levelString = [ LogLevel . Error , LogLevel . Warn ] . includes ( level ) ? ` [ ${ Logger . levelIdToString ( level ) } ] ` : '' ;
line . push ( ( levelString ? levelString : '' ) + this . objectsToString ( . . . object ) ) ;
return ` ${ line . join ( ': ' ) } ` ;
}
public log ( level : LogLevel , prefix : string | null , . . . object : unknown [ ] ) {
2021-02-09 19:54:29 +02:00
if ( ! this . targets_ . length || ! this . enabled ) return ;
2017-06-23 23:32:24 +02:00
2024-04-26 16:08:37 +02:00
let logLine = '' ;
2017-06-23 23:32:24 +02:00
for ( let i = 0 ; i < this . targets_ . length ; i ++ ) {
2020-03-14 01:46:14 +02:00
const target = this . targets_ [ i ] ;
2024-04-26 16:08:37 +02:00
const targetPrefix = prefix ? prefix : ( target . prefix || '' ) ;
2019-07-29 15:43:53 +02:00
2019-05-11 18:53:56 +02:00
if ( this . targetLevel ( target ) < level ) continue ;
2022-07-23 09:31:32 +02:00
if ( target . type === 'console' ) {
2017-07-11 20:41:18 +02:00
let fn = 'log' ;
2022-07-23 09:31:32 +02:00
if ( level === LogLevel . Error ) fn = 'error' ;
if ( level === LogLevel . Warn ) fn = 'warn' ;
if ( level === LogLevel . Info ) fn = 'info' ;
2019-09-08 18:16:45 +02:00
const consoleObj = target . console ? target.console : console ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2020-12-28 19:26:15 +02:00
let items : any [ ] = [ ] ;
2020-12-28 17:15:30 +02:00
if ( target . format ) {
2023-08-28 15:30:56 +02:00
const format = typeof target . format === 'string' ? target.format : target.format ( level , targetPrefix ) ;
2020-12-28 17:15:30 +02:00
const s = sprintf ( format , {
date_time : moment ( ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ,
level : Logger.levelIdToString ( level ) ,
prefix : targetPrefix || '' ,
message : '' ,
} ) ;
items = [ s . trim ( ) ] . concat ( . . . object ) ;
} else {
const prefixItems = [ moment ( ) . format ( 'HH:mm:ss' ) ] ;
if ( targetPrefix ) prefixItems . push ( targetPrefix ) ;
2024-04-26 16:08:37 +02:00
items = [ ` ${ prefixItems . join ( ': ' ) } : ` as unknown ] . concat ( . . . object ) ;
2020-12-28 17:15:30 +02:00
}
2020-03-10 01:24:57 +02:00
consoleObj [ fn ] ( . . . items ) ;
2022-07-23 09:31:32 +02:00
} else if ( target . type === 'file' ) {
2024-04-26 16:08:37 +02:00
logLine = this . logInfoToString ( level , targetPrefix , . . . object ) ;
2021-01-20 17:49:02 +02:00
// Write to file using a mutex so that log entries appear in the
// correct order (otherwise, since the async call is not awaited
// by caller, multiple log call in a row are not guaranteed to
// appear in the right order). We also can't use a sync call
// because that would slow down the main process, especially
// when many log operations are being done (eg. during sync in
// dev mode).
2023-06-30 11:30:29 +02:00
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
2023-07-27 17:05:56 +02:00
let release : Function | null = null ;
2023-06-30 11:30:29 +02:00
/* eslint-disable-next-line promise/prefer-await-to-then, @typescript-eslint/ban-types -- Old code before rule was applied, Old code before rule was applied */
2021-01-20 17:49:02 +02:00
writeToFileMutex_ . acquire ( ) . then ( ( r : Function ) = > {
release = r ;
2024-04-26 16:08:37 +02:00
return Logger . fsDriver ( ) . appendFile ( target . path as string , ` ${ logLine } \ n ` , 'utf8' ) ;
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line promise/prefer-await-to-then, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
2021-01-20 17:49:02 +02:00
} ) . catch ( ( error : any ) = > {
2019-10-14 10:35:04 +02:00
console . error ( 'Cannot write to log file:' , error ) ;
2022-09-30 18:23:14 +02:00
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
2021-01-20 17:49:02 +02:00
} ) . finally ( ( ) = > {
if ( release ) release ( ) ;
} ) ;
2022-07-23 09:31:32 +02:00
} else if ( target . type === 'database' ) {
2020-11-19 17:25:02 +02:00
const msg = [ ] ;
if ( targetPrefix ) msg . push ( targetPrefix ) ;
msg . push ( this . objectsToString ( . . . object ) ) ;
2017-07-16 18:20:25 +02:00
2020-03-14 01:46:14 +02:00
const queries = [
2019-07-29 15:43:53 +02:00
{
sql : 'INSERT INTO logs (`source`, `level`, `message`, `timestamp`) VALUES (?, ?, ?, ?)' ,
2023-07-27 17:05:56 +02:00
params : [ target . source , level , msg . join ( ': ' ) , Date . now ( ) ] ,
2019-07-29 15:43:53 +02:00
} ,
] ;
2017-07-16 18:20:25 +02:00
2023-07-27 17:05:56 +02:00
const now = Date . now ( ) ;
2017-07-16 18:20:25 +02:00
if ( now - this . lastDbCleanup_ > 1000 * 60 * 60 ) {
this . lastDbCleanup_ = now ;
const dayKeep = 14 ;
queries . push ( {
sql : 'DELETE FROM logs WHERE `timestamp` < ?' ,
params : [ now - 1000 * 60 * 60 * 24 * dayKeep ] ,
} ) ;
}
target . database . transactionExecBatch ( queries ) ;
2017-06-23 23:32:24 +02:00
}
}
}
2024-03-14 20:34:11 +02:00
// For tests
public async waitForFileWritesToComplete_() {
const release = await writeToFileMutex_ . acquire ( ) ;
release ( ) ;
return ;
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public error ( . . . object : any [ ] ) {
2020-11-19 17:25:02 +02:00
return this . log ( LogLevel . Error , null , . . . object ) ;
2019-07-29 15:43:53 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public warn ( . . . object : any [ ] ) {
2020-11-19 17:25:02 +02:00
return this . log ( LogLevel . Warn , null , . . . object ) ;
2019-07-29 15:43:53 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public info ( . . . object : any [ ] ) {
2020-11-19 17:25:02 +02:00
return this . log ( LogLevel . Info , null , . . . object ) ;
2019-07-29 15:43:53 +02:00
}
2024-04-05 13:16:49 +02:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
2023-03-06 16:22:01 +02:00
public debug ( . . . object : any [ ] ) {
2020-11-19 17:25:02 +02:00
return this . log ( LogLevel . Debug , null , . . . object ) ;
2019-07-29 15:43:53 +02:00
}
2017-06-23 23:32:24 +02:00
2023-03-06 16:22:01 +02:00
public static levelStringToId ( s : string ) {
2022-07-23 09:31:32 +02:00
if ( s === 'none' ) return LogLevel . None ;
if ( s === 'error' ) return LogLevel . Error ;
if ( s === 'warn' ) return LogLevel . Warn ;
if ( s === 'info' ) return LogLevel . Info ;
if ( s === 'debug' ) return LogLevel . Debug ;
2020-10-09 19:35:46 +02:00
throw new Error ( ` Unknown log level: ${ s } ` ) ;
2017-08-20 22:11:32 +02:00
}
2023-03-06 16:22:01 +02:00
public static levelIdToString ( id : LogLevel ) {
2022-07-23 09:31:32 +02:00
if ( id === LogLevel . None ) return 'none' ;
if ( id === LogLevel . Error ) return 'error' ;
if ( id === LogLevel . Warn ) return 'warn' ;
if ( id === LogLevel . Info ) return 'info' ;
if ( id === LogLevel . Debug ) return 'debug' ;
2020-10-09 19:35:46 +02:00
throw new Error ( ` Unknown level ID: ${ id } ` ) ;
2017-08-20 22:11:32 +02:00
}
2023-03-06 16:22:01 +02:00
public static levelIds() {
2020-10-09 19:35:46 +02:00
return [ LogLevel . None , LogLevel . Error , LogLevel . Warn , LogLevel . Info , LogLevel . Debug ] ;
2017-08-20 22:11:32 +02:00
}
2017-06-23 23:32:24 +02:00
}
2020-10-09 19:35:46 +02:00
export default Logger ;