From 46aeaaa1c20d3a35827d70670c2ab0cc6c7613b5 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Wed, 25 Sep 2024 07:27:00 +0000 Subject: [PATCH] Winston log provider --- ecosystem.config.cjs | 2 +- packages/pockethost/package.json | 3 +- .../EdgeCommand/FtpCommand/FtpService/PhFs.ts | 30 ++-- .../firewall/createVhostProxyMiddleware.ts | 6 +- .../ServeCommand/firewall/server.ts | 15 +- .../src/cli/commands/HealthCommand/compact.ts | 7 +- .../SendMailCommand/SqliteService/index.ts | 2 +- packages/pockethost/src/cli/index.ts | 10 +- packages/pockethost/src/cli/ioc.ts | 11 ++ packages/pockethost/src/common/Logger.ts | 134 +++--------------- packages/pockethost/src/constants.ts | 53 ++----- .../pockethost/src/core/DiscordTransport.ts | 19 +++ packages/pockethost/src/core/SyslogLogger.ts | 3 +- packages/pockethost/src/core/ioc.ts | 35 +++++ packages/pockethost/src/core/tryFetch.ts | 2 +- packages/pockethost/src/core/winston.ts | 111 +++++++++++++++ .../services/InstanceLoggerService/index.ts | 7 +- .../src/services/InstanceService/index.ts | 4 +- .../src/services/PocketBaseService/index.ts | 4 +- pnpm-lock.yaml | 28 +++- 20 files changed, 266 insertions(+), 220 deletions(-) create mode 100644 packages/pockethost/src/cli/ioc.ts create mode 100644 packages/pockethost/src/core/DiscordTransport.ts create mode 100644 packages/pockethost/src/core/ioc.ts create mode 100644 packages/pockethost/src/core/winston.ts diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 65695dc0..c8499186 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -34,7 +34,7 @@ module.exports = { }, { name: `health-compact`, - restart_delay: 60 * 1000, // 1 minute + restart_delay: 60 * 60 * 1000 * 24, // 1 day script: 'pnpm prod:cli health compact', }, ], diff --git a/packages/pockethost/package.json b/packages/pockethost/package.json index 3c8bec27..8191374c 100644 --- a/packages/pockethost/package.json +++ b/packages/pockethost/package.json @@ -65,7 +65,8 @@ "url-pattern": "^1.0.3", "vhost": "^3.0.2", "winston": "^3.11.0", - "winston-syslog": "^2.7.0" + "winston-syslog": "^2.7.0", + "winston-transport": "^4.7.1" }, "devDependencies": { "@types/cors": "^2.8.17", diff --git a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/PhFs.ts b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/PhFs.ts index 0ee4a780..6128dc95 100644 --- a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/PhFs.ts +++ b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/PhFs.ts @@ -140,7 +140,6 @@ export class PhFs implements FileSystem { ? normalize(resolvedVirtualPath) : join('/', this.cwd, resolvedVirtualPath) - console.log(`***finalVirtualPath`, { finalVirtualPath }) // Create local filesystem path using the platform separator const [empty, subdomain, ...restOfVirtualPath] = finalVirtualPath.split('/') dbg({ @@ -154,7 +153,7 @@ export class PhFs implements FileSystem { // Check if the instance is valid const instance = await (async () => { - console.log(`***checking validity`, { subdomain }) + dbg(`checking validity`, { subdomain }) if (!subdomain) return const instance = await this.client .collection(`instances`) @@ -225,8 +224,7 @@ export class PhFs implements FileSystem { async list(path = '.') { const { dbg, error } = this.log .create(`list`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(path) + .breadcrumb({ cwd: this.cwd, path }) const { fsPath, instance } = await this._resolvePath(path) @@ -277,9 +275,7 @@ export class PhFs implements FileSystem { const { dbg, error } = this.log .create(`get`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(fileName) - .breadcrumb(fsPath) + .breadcrumb({ cwd: this.cwd, fileName, fsPath }) dbg(`get`) /* @@ -325,8 +321,7 @@ export class PhFs implements FileSystem { ) { const { dbg, error } = this.log .create(`write`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(fileName) + .breadcrumb({ cwd: this.cwd, fileName }) dbg(`write`) const { fsPath, clientPath, instance } = await this._resolvePath(fileName) @@ -362,8 +357,7 @@ export class PhFs implements FileSystem { ): Promise { const { dbg, error } = this.log .create(`read`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(fileName) + .breadcrumb({ cwd: this.cwd, fileName }) dbg(`read`) const { fsPath, clientPath } = await this._resolvePath(fileName) @@ -387,8 +381,7 @@ export class PhFs implements FileSystem { async delete(path: string) { const { dbg, error } = this.log .create(`delete`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(path) + .breadcrumb({ cwd: this.cwd, path }) dbg(`delete`) const { fsPath, instance } = await this._resolvePath(path) @@ -405,8 +398,7 @@ export class PhFs implements FileSystem { async mkdir(path: string) { const { dbg, error } = this.log .create(`mkdir`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(path) + .breadcrumb({ cwd: this.cwd, path }) dbg(`mkdir`) const { fsPath } = await this._resolvePath(path) @@ -417,9 +409,7 @@ export class PhFs implements FileSystem { async rename(from: string, to: string) { const { dbg, error } = this.log .create(`rename`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(from) - .breadcrumb(to) + .breadcrumb({ cwd: this.cwd, from, to }) dbg(`rename`) const { fsPath: fromPath, instance } = await this._resolvePath(from) @@ -434,9 +424,7 @@ export class PhFs implements FileSystem { async chmod(path: string, mode: Mode) { const { dbg, error } = this.log .create(`chmod`) - .breadcrumb(`cwd:${this.cwd}`) - .breadcrumb(path) - .breadcrumb(mode.toString()) + .breadcrumb({ cwd: this.cwd, path, mode }) dbg(`chmod`) const { fsPath } = await this._resolvePath(path) diff --git a/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/createVhostProxyMiddleware.ts b/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/createVhostProxyMiddleware.ts index 31bb87f8..97fb8d61 100644 --- a/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/createVhostProxyMiddleware.ts +++ b/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/createVhostProxyMiddleware.ts @@ -1,20 +1,22 @@ import { Request } from 'express' import { createProxyMiddleware } from 'http-proxy-middleware' import vhost from 'vhost' +import { logger } from '../../../../../core/ioc' export function createVhostProxyMiddleware( host: string, target: string, ws = false, ) { - console.log(`Creating ${host}->${target}`) + const { dbg } = logger() + dbg(`Creating ${host}->${target}`) const handler = createProxyMiddleware({ target, ws, changeOrigin: ws }) return vhost(host, (_req, res, next) => { const req = _req as unknown as Request const method = req.method const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl - console.log(`${method} ${fullUrl} -> ${target}`) + dbg(`${method} ${fullUrl} -> ${target}`) // @ts-ignore return handler(req, res, next) }) diff --git a/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/server.ts b/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/server.ts index d04525f0..5e4c8f5b 100644 --- a/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/server.ts +++ b/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/server.ts @@ -7,7 +7,6 @@ import fs from 'fs' import http from 'http' import { createProxyMiddleware } from 'http-proxy-middleware' import https from 'https' -import { LoggerService } from '../../../../../common' import { APEX_DOMAIN, APP_NAME, @@ -18,13 +17,13 @@ import { MOTHERSHIP_PORT, SSL_CERT, SSL_KEY, - discordAlert, } from '../../../../../core' +import { logger } from '../../../../../core/ioc' import { createIpWhitelistMiddleware } from './cidr' import { createVhostProxyMiddleware } from './createVhostProxyMiddleware' export const firewall = async () => { - const { debug } = LoggerService().create(`proxy`) + const { dbg, error } = logger() const PROD_ROUTES = { [`${MOTHERSHIP_NAME()}.${APEX_DOMAIN()}`]: `http://localhost:${MOTHERSHIP_PORT()}`, @@ -49,10 +48,12 @@ export const firewall = async () => { app.use(createIpWhitelistMiddleware(IPCIDR_LIST())) forEach(hostnameRoutes, (target, host) => { + dbg(`Creating ${host}->${target}`) app.use(createVhostProxyMiddleware(host, target, IS_DEV())) }) app.get(`/_api/health`, (req, res, next) => { + dbg(`Health check`) res.json({ status: 'ok' }) res.end() }) @@ -65,19 +66,19 @@ export const firewall = async () => { const method = req.method const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl - debug(`${method} ${fullUrl} -> ${`http://localhost:${DAEMON_PORT()}`}`) + dbg(`${method} ${fullUrl} -> ${`http://localhost:${DAEMON_PORT()}`}`) handler(req, res, next) }) const errorHandler: ErrorRequestHandler = (err, req, res, next) => { - discordAlert(err.toString()) + error(err) res.status(500).send(err.toString()) } app.use(errorHandler) http.createServer(app).listen(80, () => { - console.log('SSL redirect server listening on 80') + dbg('SSL redirect server listening on 80') }) // HTTPS server options @@ -88,6 +89,6 @@ export const firewall = async () => { // Create HTTPS server https.createServer(httpsOptions, app).listen(443, () => { - console.log('HTTPS server running on port 443') + dbg('HTTPS server running on port 443') }) } diff --git a/packages/pockethost/src/cli/commands/HealthCommand/compact.ts b/packages/pockethost/src/cli/commands/HealthCommand/compact.ts index 7b53700d..b316df72 100644 --- a/packages/pockethost/src/cli/commands/HealthCommand/compact.ts +++ b/packages/pockethost/src/cli/commands/HealthCommand/compact.ts @@ -1,15 +1,18 @@ import { exec } from 'child_process' import { globSync } from 'glob' import { DATA_ROOT } from '../../../../core' +import { logger } from '../../../core/ioc' export const compact = async () => { + const { info } = logger() + const files = [`data`, `logs`].flatMap((db) => globSync(`${DATA_ROOT()}/*/pb_data/${db}.db{-shm,-wal}`), ) files.map(async (file) => { - console.log(`Compacting ${file}`) + info(`Compacting ${file}`) exec(`sqlite3 ${file} ".tables"`) }) - console.log(`Compaction complete`) + info(`Compaction complete`) } diff --git a/packages/pockethost/src/cli/commands/SendMailCommand/SqliteService/index.ts b/packages/pockethost/src/cli/commands/SendMailCommand/SqliteService/index.ts index 46e0a79c..1e4a8418 100644 --- a/packages/pockethost/src/cli/commands/SendMailCommand/SqliteService/index.ts +++ b/packages/pockethost/src/cli/commands/SendMailCommand/SqliteService/index.ts @@ -25,7 +25,7 @@ export const SqliteService = mkSingleton((config: SqliteServiceConfig) => { filename: string, ): Promise => { const _dbLogger = LoggerService().create(`SqliteService`) - _dbLogger.breadcrumb(filename) + _dbLogger.breadcrumb({ filename }) const { dbg, error, abort } = _dbLogger trace(`Fetching database`, connections) diff --git a/packages/pockethost/src/cli/index.ts b/packages/pockethost/src/cli/index.ts index bc2ed675..a0065c77 100755 --- a/packages/pockethost/src/cli/index.ts +++ b/packages/pockethost/src/cli/index.ts @@ -3,16 +3,12 @@ import { program } from 'commander' import EventSource from 'eventsource' import { - DEBUG, - DefaultSettingsService, LogLevelName, LoggerService, PH_PLUGINS, - SETTINGS, loadPlugins, } from '../../core' import { version } from '../../package.json' -import { GobotService } from '../services/GobotService' import { EdgeCommand } from './commands/EdgeCommand' import { FirewallCommand } from './commands/FirewallCommand' import { HealthCommand } from './commands/HealthCommand' @@ -20,17 +16,13 @@ import { MothershipCommand } from './commands/MothershipCommand' import { PocketBaseCommand } from './commands/PocketBaseCommand' import { SendMailCommand } from './commands/SendMailCommand' import { ServeCommand } from './commands/ServeCommand' +import './ioc' export type GlobalOptions = { logLevel?: LogLevelName debug: boolean } -DefaultSettingsService(SETTINGS) - -LoggerService({ level: DEBUG() ? LogLevelName.Debug : LogLevelName.Info }) -GobotService({}) - //@ts-ignore global.EventSource = EventSource diff --git a/packages/pockethost/src/cli/ioc.ts b/packages/pockethost/src/cli/ioc.ts new file mode 100644 index 00000000..2b5789bf --- /dev/null +++ b/packages/pockethost/src/cli/ioc.ts @@ -0,0 +1,11 @@ +import { LoggerService } from '../common' +import { RegisterEnvSettingsService } from '../constants' +import { ioc } from '../core/ioc' +import { WinstonLoggerService } from '../core/winston' +import { GobotService } from '../services/GobotService' + +ioc.register('logger', WinstonLoggerService({})) + +RegisterEnvSettingsService() +LoggerService({}) +GobotService({}) diff --git a/packages/pockethost/src/common/Logger.ts b/packages/pockethost/src/common/Logger.ts index b63755f3..a3f44e42 100644 --- a/packages/pockethost/src/common/Logger.ts +++ b/packages/pockethost/src/common/Logger.ts @@ -1,16 +1,13 @@ /// -import chalk from 'chalk' -import stringify from 'json-stringify-safe' -import { action, mergeConfig, mkSingleton } from '.' +import { mkSingleton } from '.' +import { logger } from '../core/ioc' export type LoggerConfig = { level: LogLevelName pfx: string[] } -export type Logger = ReturnType - export const isLevelLte = (a: LogLevelName, b: LogLevelName) => { return LogLevels[a] <= LogLevels[b] } @@ -53,118 +50,23 @@ export const LogLevels = { [LogLevelName.Abort]: 6, } as const -export const createLogger = (config: Partial) => { - const _config = mergeConfig( - { - level: LogLevelName.Debug, - pfx: [''], - }, - config, - ) - const { pfx } = _config - - const setLevel = (level: LogLevelName) => { - _config.level = level - } - - const _pfx = (s: string) => - [new Date().toISOString(), s, ...pfx] - .filter((v) => !!v) - .map((p) => `[${p}]`) - .join(' ') - - const _log = (levelIn: LogLevelName, ...args: any[]) => { - action(`log`, _config.level, levelIn, args) - } - - const raw = (...args: any[]) => { - _log(LogLevelName.Raw, _pfx('RAW'), ...args) - } - - const trace = (...args: any[]) => { - _log(LogLevelName.Trace, _pfx(`TRACE`), ...args) - } - - const dbg = (...args: any[]) => { - _log(LogLevelName.Debug, _pfx(chalk.blueBright('DBG')), ...args) - } - - const info = (...args: any[]) => { - _log( - LogLevelName.Info, - _pfx( - isLevelGt(LogLevelName.Info, _config.level) ? chalk.gray(`INFO`) : '', - ), - ...args, - ) - } - - const warn = (...args: any[]) => { - _log( - LogLevelName.Warn, - _pfx(chalk.yellow(chalk.cyanBright('WARN'))), - ...args, - ) - } - - const error = (...args: any[]) => { - _log(LogLevelName.Error, ...[_pfx(chalk.bgRed(`ERROR`)), ...args]) - } - - const criticalError = (...args: any[]) => { - _log(LogLevelName.Error, ...[_pfx(chalk.bgRed(`ERROR`)), ...args]) - new Error().stack?.split(/\n/).forEach((line) => { - _log(LogLevelName.Debug, _pfx(chalk.bgRed(`ERROR`)), line) - }) - } - - const abort = (...args: any[]): never => { - _log(LogLevelName.Abort, true, ...[_pfx(chalk.bgRed(`ABORT`)), ...args]) - throw new Error(`Fatal error: ${stringify(args)}`) - } - - const create = (name: string, configOverride?: Partial) => - createLogger({ - ..._config, - ...configOverride, - pfx: [..._config.pfx, name], - }) - - const breadcrumb = (s: string | object) => { - if (typeof s === 'string') { - pfx.push(s) - } else { - Object.entries(s).forEach(([k, v]) => pfx.push(`${k}: ${v}`)) - } - return api - } - - // Compatibility func - const child = (name: string) => create(name) - - const api = { - raw, - dbg, - warn, - info, - error, - criticalError, - create, - child, - trace, - debug: dbg, - breadcrumb, - abort, - shutdown() { - dbg(`Logger shutting down`) - }, - setLevel, - } - return api +export type Logger = { + raw: (...args: any[]) => void + dbg: (...args: any[]) => void + warn: (...args: any[]) => void + info: (...args: any[]) => void + error: (...args: any[]) => void + criticalError: (...args: any[]) => void + create: (name: string, configOverride?: Partial) => Logger + child: (name: string) => Logger + trace: (...args: any[]) => void + debug: (...args: any[]) => void + breadcrumb: (s: object) => Logger + abort: (...args: any[]) => never + shutdown: () => void + setLevel: (level: LogLevelName) => void } -export type LoggerServiceApi = ReturnType - export const LoggerService = mkSingleton((config: Partial = {}) => - createLogger(config), + logger(), ) diff --git a/packages/pockethost/src/constants.ts b/packages/pockethost/src/constants.ts index 54b7a7dc..f3a884ac 100644 --- a/packages/pockethost/src/constants.ts +++ b/packages/pockethost/src/constants.ts @@ -6,15 +6,11 @@ import { default as env } from 'env-var' import { mkdirSync, writeFileSync } from 'fs' import { dirname, join } from 'path' import { fileURLToPath } from 'url' -import { LogEntry } from 'winston' import { InstanceFields, InstanceId, - IoCManager, SettingsHandlerFactory, SettingsService, - UserFields, - mkSingleton, } from '../core' import { mkBoolean, @@ -23,6 +19,7 @@ import { mkPath, mkString, } from './core/Settings' +import { ioc, settings } from './core/ioc' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -147,55 +144,23 @@ export const SETTINGS = { PH_GOBOT_ROOT: mkPath(join(_PH_HOME, 'gobot'), { create: true }), } -export type Settings = ReturnType +export type Settings = ReturnType export type SettingsDefinition = { [_ in keyof Settings]: SettingsHandlerFactory } -export const DefaultSettingsService = mkSingleton( - (settings: typeof SETTINGS) => { - const _settings = SettingsService(settings) +export const RegisterEnvSettingsService = () => { + const _settings = SettingsService(SETTINGS) - ioc.register('settings', _settings) + ioc.register('settings', _settings) - if (DEBUG()) { - logConstants() - } + if (DEBUG()) { + logConstants() + } - return _settings - }, -) - -export type MothershipProvider = { - getAllInstances(): Promise - getInstanceById(id: InstanceId): Promise<[InstanceFields, UserFields] | []> - getInstanceBySubdomain( - subdomain: InstanceFields['subdomain'], - ): Promise<[InstanceFields, UserFields] | []> - updateInstance(id: InstanceId, fields: Partial): Promise + return _settings } -type UnsubFunc = () => void - -export type InstanceLogProvider = ( - instanceId: InstanceId, - target: string, -) => { - info(msg: string): void - error(msg: string): void - tail(linesBack: number, data: (line: LogEntry) => void): UnsubFunc -} - -export const ioc = new IoCManager<{ - settings: Settings - mothership: MothershipProvider - instanceLogger: InstanceLogProvider -}>() - -export const settings = () => ioc.service('settings') -export const mothership = () => ioc.service('mothership') -export const instanceLogger = () => ioc.service('instanceLogger') - /** Accessors */ export const PH_PLUGINS = () => settings().PH_PLUGINS diff --git a/packages/pockethost/src/core/DiscordTransport.ts b/packages/pockethost/src/core/DiscordTransport.ts new file mode 100644 index 00000000..d56b2ac5 --- /dev/null +++ b/packages/pockethost/src/core/DiscordTransport.ts @@ -0,0 +1,19 @@ +import TransportStream from 'winston-transport' +import { discordAlert } from '..' + +export type DiscordTransportType = { + webhookUrl: string +} & TransportStream.TransportStreamOptions + +export class DiscordTransport extends TransportStream { + private url: string + constructor(opts: DiscordTransportType) { + super(opts) + this.url = opts.webhookUrl + } + + log(info: any, callback: any) { + discordAlert(info) + callback() + } +} diff --git a/packages/pockethost/src/core/SyslogLogger.ts b/packages/pockethost/src/core/SyslogLogger.ts index 240b27df..203ea4ee 100644 --- a/packages/pockethost/src/core/SyslogLogger.ts +++ b/packages/pockethost/src/core/SyslogLogger.ts @@ -20,8 +20,7 @@ export function SyslogLogger(instanceId: string, target: string) { const { error, warn } = LoggerService() .create('SyslogLogger') - .breadcrumb(instanceId) - .breadcrumb(target) + .breadcrumb({ instanceId, target }) const api = { info: (msg: string) => { diff --git a/packages/pockethost/src/core/ioc.ts b/packages/pockethost/src/core/ioc.ts new file mode 100644 index 00000000..7cf0e866 --- /dev/null +++ b/packages/pockethost/src/core/ioc.ts @@ -0,0 +1,35 @@ +import { LogEntry } from 'winston' +import { InstanceFields, InstanceId, Logger, UserFields } from '../common' +import { IoCManager } from '../common/ioc' +import { Settings } from '../constants' + +export type MothershipProvider = { + getAllInstances(): Promise + getInstanceById(id: InstanceId): Promise<[InstanceFields, UserFields] | []> + getInstanceBySubdomain( + subdomain: InstanceFields['subdomain'], + ): Promise<[InstanceFields, UserFields] | []> + updateInstance(id: InstanceId, fields: Partial): Promise +} +type UnsubFunc = () => void + +export type InstanceLogProvider = ( + instanceId: InstanceId, + target: string, +) => { + info(msg: string): void + error(msg: string): void + tail(linesBack: number, data: (line: LogEntry) => void): UnsubFunc +} + +export const ioc = new IoCManager<{ + settings: Settings + mothership: MothershipProvider + instanceLogger: InstanceLogProvider + logger: Logger +}>() + +export const settings = () => ioc.service('settings') +export const mothership = () => ioc.service('mothership') +export const instanceLogger = () => ioc.service('instanceLogger') +export const logger = () => ioc.service('logger') diff --git a/packages/pockethost/src/core/tryFetch.ts b/packages/pockethost/src/core/tryFetch.ts index 85f9d5ba..2674454d 100644 --- a/packages/pockethost/src/core/tryFetch.ts +++ b/packages/pockethost/src/core/tryFetch.ts @@ -33,7 +33,7 @@ export const tryFetch = async ( timeoutMs: TRYFETCH_TIMEOUT_MS, ...config, } - const logger = LoggerService().create(`tryFetch`).breadcrumb(url) + const logger = LoggerService().create(`tryFetch`).breadcrumb({ url }) const { dbg } = logger return new Promise((resolve, reject) => { const again = () => setTimeout(_real_tryFetch, retryMs) diff --git a/packages/pockethost/src/core/winston.ts b/packages/pockethost/src/core/winston.ts new file mode 100644 index 00000000..44b05fea --- /dev/null +++ b/packages/pockethost/src/core/winston.ts @@ -0,0 +1,111 @@ +import { inspect } from 'node:util' +import winston from 'winston' +import { Logger, mkSingleton } from '../common' +import { DEBUG, DISCORD_ALERT_CHANNEL_URL } from '../constants' +import { DiscordTransport } from './DiscordTransport' + +const format = winston.format.combine( + winston.format.colorize(), + winston.format.printf(({ level, message, timestamp, ...meta }) => { + const final: string[] = [] + message.forEach((m: string) => { + if (typeof m === 'string' && !!m.match(/\n/)) { + final.push(...m.split(/\n/)) + } else if (typeof m === 'object') { + final.push(inspect(m, { depth: null })) + } else { + final.push(m) + } + }) + return `${level}: ${final.join(' ')}` + }), +) + +export const WinstonLoggerService = mkSingleton<{}, Logger>(() => { + const logger = winston.createLogger({ + format: winston.format.json(), + transports: [ + new winston.transports.Console({ + level: DEBUG() ? 'debug' : 'info', + format, + }), + new winston.transports.File({ + filename: 'error.log', + level: 'error', + maxsize: 100 * 1024 * 1024, + maxFiles: 10, + tailable: true, + }), + new winston.transports.File({ + filename: 'debug.log', + level: 'debug', + maxsize: 100 * 1024 * 1024, + maxFiles: 10, + tailable: true, + }), + ], + rejectionHandlers: [ + new winston.transports.Console({ + level: 'error', + format, + }), + new winston.transports.File({ + filename: 'rejections.log', + maxsize: 100 * 1024 * 1024, + maxFiles: 10, + tailable: true, + }), + ], + exceptionHandlers: [ + new winston.transports.Console({ + level: 'error', + format, + }), + new winston.transports.File({ + filename: 'exceptions.log', + maxsize: 100 * 1024 * 1024, + maxFiles: 10, + tailable: true, + }), + ], + defaultMeta: {}, + }) + logger.exitOnError = true + + { + const url = DISCORD_ALERT_CHANNEL_URL() + if (url) { + logger.add(new DiscordTransport({ level: 'error', webhookUrl: url })) + } + } + + const createApi = (logger: winston.Logger): Logger => { + const api: Logger = { + create: (name: string) => { + return createApi(logger.child({ ...logger.defaultMeta, name })) + }, + raw: (...args: any[]) => logger.silly(args), + dbg: (...args: any[]) => logger.debug(args), + warn: (...args: any[]) => logger.warn(args), + info: (...args: any[]) => logger.info(args), + error: (...args: any[]) => logger.error(args), + criticalError: (...args: any[]) => logger.error(args), + setLevel: (level) => {}, + trace: (...args: any[]) => logger.silly(args), + debug: (...args: any[]) => logger.debug(args), + breadcrumb: (s) => { + Object.assign(logger.defaultMeta, s) + return api + }, + shutdown: () => {}, + child: (name) => createApi(logger.child({ name })), + abort: (...args) => { + logger.error(args) + process.exit(1) + }, + } + return api + } + + return createApi(logger) +}) diff --git a/packages/pockethost/src/services/InstanceLoggerService/index.ts b/packages/pockethost/src/services/InstanceLoggerService/index.ts index 87571e5d..8b4961ed 100644 --- a/packages/pockethost/src/services/InstanceLoggerService/index.ts +++ b/packages/pockethost/src/services/InstanceLoggerService/index.ts @@ -33,7 +33,9 @@ export function InstanceLogger( target: string, options: Partial = {}, ) { - const { dbg, info } = LoggerService().create(instanceId).breadcrumb(target) + const { dbg, info } = LoggerService() + .create(instanceId) + .breadcrumb({ target }) const { ttl } = mergeConfig({ ttl: 0 }, options) dbg({ ttl }) @@ -85,8 +87,7 @@ export function InstanceLogger( const { error, warn } = LoggerService() .create('InstanceLogger') - .breadcrumb(instanceId) - .breadcrumb(target) + .breadcrumb({ instanceId, target }) const resetTtl = (() => { let tid: ReturnType diff --git a/packages/pockethost/src/services/InstanceService/index.ts b/packages/pockethost/src/services/InstanceService/index.ts index f6c35694..4ffe9db4 100644 --- a/packages/pockethost/src/services/InstanceService/index.ts +++ b/packages/pockethost/src/services/InstanceService/index.ts @@ -72,7 +72,7 @@ export const instanceService = mkSingleton( const getInstanceApi = (instance: InstanceFields): Promise => { const _logger = instanceServiceLogger.create(`getInstanceApi`) const { id, subdomain, version } = instance - _logger.breadcrumb(`${subdomain}:${id}:${version}`) + _logger.breadcrumb({ subdomain, id, version }) const { dbg, trace } = _logger return new Promise((resolve, reject) => { let maxTries = instanceApiTimeoutMs / instanceApiCheckIntervalMs @@ -256,7 +256,7 @@ export const instanceService = mkSingleton( dbg(`shut down: releasing port`) releasePort() }, CLEANUP_PRIORITY_LAST) - systemInstanceLogger.breadcrumb(`port:${newPort}`) + systemInstanceLogger.breadcrumb({ port: newPort }) dbg(`Found port`) /* diff --git a/packages/pockethost/src/services/PocketBaseService/index.ts b/packages/pockethost/src/services/PocketBaseService/index.ts index 9d6db75f..f6798b8e 100644 --- a/packages/pockethost/src/services/PocketBaseService/index.ts +++ b/packages/pockethost/src/services/PocketBaseService/index.ts @@ -94,7 +94,7 @@ export const createPocketbaseService = async ( dev, } = _cfg - logger.breadcrumb(subdomain).breadcrumb(instanceId) + logger.breadcrumb({ subdomain, instanceId }) const iLogger = SyslogLogger(instanceId, 'exec') cm.add(async () => { dbg(`Shutting down iLogger`) @@ -259,7 +259,7 @@ export const createPocketbaseService = async ( cm.shutdown().catch(error) }) const url = mkInternalUrl(port) - logger.breadcrumb(url) + logger.breadcrumb({ url }) dbg(`Making exit hook for ${url}`) const unsub = asyncExitHook(async () => { await api.kill() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa153bb7..116fe687 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -479,6 +479,9 @@ importers: winston-syslog: specifier: ^2.7.0 version: 2.7.0(winston@3.11.0) + winston-transport: + specifier: ^4.7.1 + version: 4.7.1 devDependencies: '@types/cors': specifier: ^2.8.17 @@ -3894,6 +3897,10 @@ packages: resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} engines: {node: '>= 12.0.0'} + logform@2.6.1: + resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} + engines: {node: '>= 12.0.0'} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -6012,8 +6019,8 @@ packages: peerDependencies: winston: ^3.8.2 - winston-transport@4.6.0: - resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} + winston-transport@4.7.1: + resolution: {integrity: sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==} engines: {node: '>= 12.0.0'} winston@3.11.0: @@ -7159,7 +7166,7 @@ snapshots: '@types/glossy': 0.1.3 '@types/node': 20.8.10 winston: 3.11.0 - winston-transport: 4.6.0 + winston-transport: 4.7.1 JSONStream@1.3.5: dependencies: @@ -9832,6 +9839,15 @@ snapshots: safe-stable-stringify: 2.4.3 triple-beam: 1.4.1 + logform@2.6.1: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.4 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.4.1 + lower-case@2.0.2: dependencies: tslib: 2.6.2 @@ -12167,9 +12183,9 @@ snapshots: optionalDependencies: unix-dgram: 2.0.6 - winston-transport@4.6.0: + winston-transport@4.7.1: dependencies: - logform: 2.6.0 + logform: 2.6.1 readable-stream: 3.6.2 triple-beam: 1.4.1 @@ -12185,7 +12201,7 @@ snapshots: safe-stable-stringify: 2.4.3 stack-trace: 0.0.10 triple-beam: 1.4.1 - winston-transport: 4.6.0 + winston-transport: 4.7.1 with@7.0.2: dependencies: