diff --git a/packages/pockethost/package.json b/packages/pockethost/package.json index 0b66375b..563593ce 100644 --- a/packages/pockethost/package.json +++ b/packages/pockethost/package.json @@ -20,7 +20,7 @@ }, "scripts": { "check:types": "tsc --noEmit ", - "dev": "NODE_ENV=development tsx --watch ./src/cli/index.ts", + "dev": "NODE_ENV=development tsx ./src/cli/index.ts", "start": "tsx ./src/cli/index.ts" }, "dependencies": { @@ -30,6 +30,7 @@ "bottleneck": "^2.19.5", "commander": "^13.0.0", "cors": "^2.8.5", + "cron": "^4.3.2", "devcert": "^1.2.2", "dockerode": "^4.0.3", "dotenv": "^16.4.7", @@ -56,9 +57,7 @@ "tail": "^2.2.6", "tsx": "^4.20.3", "type-fest": "^4.32.0", - "vhost": "^3.0.2", - "winston": "^3.17.0", - "winston-transport": "^4.9.0" + "vhost": "^3.0.2" }, "devDependencies": { "@types/better-sqlite3": "^7.6.12", diff --git a/packages/pockethost/src/cli/commands/EdgeCommand/DaemonCommand/ServeCommand/daemon.ts b/packages/pockethost/src/cli/commands/EdgeCommand/DaemonCommand/ServeCommand/daemon.ts index 8cd82816..5f676456 100644 --- a/packages/pockethost/src/cli/commands/EdgeCommand/DaemonCommand/ServeCommand/daemon.ts +++ b/packages/pockethost/src/cli/commands/EdgeCommand/DaemonCommand/ServeCommand/daemon.ts @@ -1,5 +1,6 @@ import { DOCKER_INSTANCE_IMAGE_NAME, + LoggerService, MOTHERSHIP_ADMIN_PASSWORD, MOTHERSHIP_ADMIN_USERNAME, MOTHERSHIP_URL, @@ -7,7 +8,6 @@ import { PocketbaseService, discordAlert, instanceService, - logger, neverendingPromise, proxyService, realtimeLog, @@ -18,7 +18,8 @@ import { ErrorRequestHandler } from 'express' import { MothershipMirrorService } from 'src/services/MothershipMirrorService' export async function daemon() { - const { info, warn } = logger() + const logger = LoggerService().create(`cli:daemon`) + const { info, warn } = logger info(`Starting`) const docker = new Dockerode() @@ -41,9 +42,9 @@ export async function daemon() { }) ) - await PocketbaseService({}) + await PocketbaseService({ logger }) - await tryFetch(MOTHERSHIP_URL(`/api/health`), {}) + await tryFetch(MOTHERSHIP_URL(`/api/health`), { logger }) info(`Serving`) @@ -52,17 +53,20 @@ export async function daemon() { url: MOTHERSHIP_URL(), username: MOTHERSHIP_ADMIN_USERNAME(), password: MOTHERSHIP_ADMIN_PASSWORD(), + logger, }) - await MothershipMirrorService({ client: (await MothershipAdminClientService()).client.client }) + await MothershipMirrorService({ client: (await MothershipAdminClientService()).client.client, logger }) await proxyService({ coreInternalUrl: MOTHERSHIP_URL(), + logger, }) - await realtimeLog({}) + await realtimeLog({ logger }) await instanceService({ instanceApiCheckIntervalMs: 50, instanceApiTimeoutMs: 5000, + logger, }) const errorHandler: ErrorRequestHandler = (err: Error, req, res, next) => { @@ -71,5 +75,5 @@ export async function daemon() { } ;(await proxyService()).use(errorHandler) - await neverendingPromise() + await neverendingPromise(logger) } 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 216b89bc..2f90d73e 100644 --- a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/PhFs.ts +++ b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/PhFs.ts @@ -1,4 +1,4 @@ -import { InstanceFields, InstanceLogWriter, InstanceLogWriterApi, Logger, PocketBase, assert, seqid } from '@' +import { InstanceFields, InstanceLogWriterApi, Logger, PocketBase, assert, seqid } from '@' import { compact, forEach, map } from '@s-libs/micro-dash' import Bottleneck from 'bottleneck' import { spawn } from 'child_process' @@ -26,7 +26,7 @@ export type PathError = { const UNIX_SEP_REGEX = /\//g const WIN_SEP_REGEX = /\\/g -const checkBun = (instance: InstanceFields, virtualPath: string, cwd: string) => { +const checkBun = (instance: InstanceFields, virtualPath: string, cwd: string, logger: Logger) => { const [subdomain, maybeImportant, ...rest] = virtualPath.split('/').filter((p) => !!p) const isImportant = @@ -34,9 +34,8 @@ const checkBun = (instance: InstanceFields, virtualPath: string, cwd: string) => (rest.length === 0 && [`bun.lock`, `bun.lockb`, `package.json`].includes(maybeImportant || '')) if (isImportant) { - const logger = InstanceLogWriter(instance.id, instance.volume, `exec`) logger.info(`${maybeImportant} changed, running bun install`) - launchBunInstall(instance, virtualPath, cwd).catch(logger.error) + launchBunInstall(instance, virtualPath, cwd, logger).catch(logger.error) } } @@ -98,7 +97,7 @@ const runBun = (() => { const launchBunInstall = (() => { const runCache: { [key: string]: { runAgain: boolean } } = {} - return async (instance: InstanceFields, virtualPath: string, cwd: string) => { + return async (instance: InstanceFields, virtualPath: string, cwd: string, logger: Logger) => { if (cwd in runCache) { runCache[cwd]!.runAgain = true return @@ -106,7 +105,6 @@ const launchBunInstall = (() => { runCache[cwd] = { runAgain: true } while (runCache[cwd]!.runAgain) { runCache[cwd]!.runAgain = false - const logger = InstanceLogWriter(instance.id, instance.volume, `exec`) logger.info(`Launching 'bun install' in ${virtualPath}`) await prepPackageJson(cwd, logger) await runBun(cwd, logger) @@ -229,7 +227,7 @@ export class PhFs implements FileSystem { } async list(path = '.') { - const { dbg, error } = this.log.create(`list`).breadcrumb({ cwd: this.cwd, path }) + const { dbg, error } = this.log.create(`list`).breadcrumb(this.cwd).breadcrumb(path) const { fsPath, instance } = await this._resolvePath(path) @@ -278,7 +276,7 @@ export class PhFs implements FileSystem { async get(fileName: string): Promise { const { fsPath, instance, clientPath } = await this._resolvePath(fileName) - const { dbg, error } = this.log.create(`get`).breadcrumb({ cwd: this.cwd, fileName, fsPath }) + const { dbg, error } = this.log.create(`get`).breadcrumb(this.cwd).breadcrumb(fileName).breadcrumb(fsPath) dbg(`get`) /* @@ -319,7 +317,8 @@ export class PhFs implements FileSystem { } async write(fileName: string, options?: { append?: boolean | undefined; start?: any } | undefined) { - const { dbg, error } = this.log.create(`write`).breadcrumb({ cwd: this.cwd, fileName }) + const logger = this.log.create(`write`).breadcrumb(this.cwd).breadcrumb(fileName) + const { dbg, error } = this.log.create(`write`).breadcrumb(this.cwd).breadcrumb(fileName) dbg(`write`) const { fsPath, clientPath, instance } = await this._resolvePath(fileName) @@ -341,7 +340,7 @@ export class PhFs implements FileSystem { const virtualPath = join(this.cwd, fileName) dbg(`write(${virtualPath}) closing`) stream.end(() => { - checkBun(instance, virtualPath, dirname(fsPath)) + checkBun(instance, virtualPath, dirname(fsPath), logger) }) }) return { @@ -351,7 +350,7 @@ export class PhFs implements FileSystem { } async read(fileName: string, options: { start?: any } | undefined): Promise { - const { dbg, error } = this.log.create(`read`).breadcrumb({ cwd: this.cwd, fileName }) + const { dbg, error } = this.log.create(`read`).breadcrumb(this.cwd).breadcrumb(fileName) dbg(`read`) const { fsPath, clientPath } = await this._resolvePath(fileName) @@ -373,7 +372,7 @@ export class PhFs implements FileSystem { } async delete(path: string) { - const { dbg, error } = this.log.create(`delete`).breadcrumb({ cwd: this.cwd, path }) + const { dbg, error } = this.log.create(`delete`).breadcrumb(this.cwd).breadcrumb(path) dbg(`delete`) const { fsPath, instance } = await this._resolvePath(path) @@ -388,7 +387,7 @@ export class PhFs implements FileSystem { } async mkdir(path: string) { - const { dbg, error } = this.log.create(`mkdir`).breadcrumb({ cwd: this.cwd, path }) + const { dbg, error } = this.log.create(`mkdir`).breadcrumb(this.cwd).breadcrumb(path) dbg(`mkdir`) const { fsPath } = await this._resolvePath(path) @@ -397,7 +396,7 @@ export class PhFs implements FileSystem { } async rename(from: string, to: string) { - const { dbg, error } = this.log.create(`rename`).breadcrumb({ cwd: this.cwd, from, to }) + const { dbg, error } = this.log.create(`rename`).breadcrumb(this.cwd).breadcrumb(from).breadcrumb(to) dbg(`rename`) const { fsPath: fromPath, instance } = await this._resolvePath(from) @@ -410,7 +409,7 @@ export class PhFs implements FileSystem { } async chmod(path: string, mode: Mode) { - const { dbg, error } = this.log.create(`chmod`).breadcrumb({ cwd: this.cwd, path, mode }) + const { dbg, error } = this.log.create(`chmod`).breadcrumb(this.cwd).breadcrumb(path).breadcrumb(`${mode}`) dbg(`chmod`) const { fsPath } = await this._resolvePath(path) diff --git a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/index.ts b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/index.ts index 5f26f739..28a42b1a 100644 --- a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/index.ts +++ b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/FtpService/index.ts @@ -7,6 +7,7 @@ import { PocketBase, SSL_CERT, SSL_KEY, + SingletonBaseConfig, asyncExitHook, logger, mergeConfig, @@ -16,9 +17,9 @@ import { readFileSync } from 'fs' import { FtpSrv } from 'ftp-srv' import { PhFs } from './PhFs' -export type FtpConfig = { mothershipUrl: string } +export type FtpConfig = SingletonBaseConfig & { mothershipUrl: string } -export const ftpService = mkSingleton((config: Partial = {}) => { +export const ftpService = mkSingleton((config: FtpConfig) => { const { mothershipUrl } = mergeConfig( { mothershipUrl: MOTHERSHIP_URL(), diff --git a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/ServeCommand/ftp.ts b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/ServeCommand/ftp.ts index 7ed87a49..f44491f8 100644 --- a/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/ServeCommand/ftp.ts +++ b/packages/pockethost/src/cli/commands/EdgeCommand/FtpCommand/ServeCommand/ftp.ts @@ -1,16 +1,18 @@ -import { logger } from '@' +import { LoggerService } from '@' import { MOTHERSHIP_URL, neverendingPromise, tryFetch } from '../../../../..' import { ftpService } from '../FtpService' export async function ftp() { - const { info } = logger() + const logger = LoggerService().create(`cli:edge:ftp:serve`) + const { info } = logger info(`Starting`) await tryFetch(MOTHERSHIP_URL(`/api/health`), {}) await ftpService({ mothershipUrl: MOTHERSHIP_URL(), + logger, }) - await neverendingPromise() + await neverendingPromise(logger) } diff --git a/packages/pockethost/src/cli/commands/EdgeCommand/VolumeCommand/MigrateCommand.ts b/packages/pockethost/src/cli/commands/EdgeCommand/VolumeCommand/MigrateCommand.ts index da9c270b..668fcdec 100644 --- a/packages/pockethost/src/cli/commands/EdgeCommand/VolumeCommand/MigrateCommand.ts +++ b/packages/pockethost/src/cli/commands/EdgeCommand/VolumeCommand/MigrateCommand.ts @@ -1,5 +1,6 @@ import { InstanceFields, + LoggerService, mkInstanceDataPath, MOTHERSHIP_ADMIN_PASSWORD, MOTHERSHIP_ADMIN_USERNAME, @@ -18,7 +19,10 @@ export const MigrateCommand = () => { .option(`-i, --instance `, `The instance to migrate`) .option(`-m, --mount-point `, `The mount point`, `cloud-storage`) .action(async (options) => { - console.log({ options }) + const logger = LoggerService().create(`cli:edge:volume:migrate`) + const { dbg } = logger + dbg({ options }) + const { instance: instanceId, mountPoint } = options const pb = new PocketBase(MOTHERSHIP_URL()) 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 4edaed3e..a3566b94 100644 --- a/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/server.ts +++ b/packages/pockethost/src/cli/commands/FirewallCommand/ServeCommand/firewall/server.ts @@ -3,7 +3,7 @@ import { DAEMON_PORT, IPCIDR_LIST, IS_DEV, - logger, + LoggerService, MOTHERSHIP_NAME, MOTHERSHIP_PORT, neverendingPromise, @@ -23,7 +23,8 @@ import { createIpWhitelistMiddleware } from './cidr' import { createVhostProxyMiddleware } from './createVhostProxyMiddleware' export const firewall = async () => { - const { dbg, error } = logger() + const logger = LoggerService().create(`cli:firewall:serve`) + const { dbg, error } = logger const PROD_ROUTES = { [`${MOTHERSHIP_NAME()}.${APEX_DOMAIN()}`]: `http://localhost:${MOTHERSHIP_PORT()}`, @@ -98,5 +99,5 @@ export const firewall = async () => { dbg('HTTPS server running on port 443') }) - await neverendingPromise() + await neverendingPromise(logger) } diff --git a/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/index.ts b/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/index.ts index c54acc56..34b96ad8 100644 --- a/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/index.ts +++ b/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/index.ts @@ -1,4 +1,4 @@ -import { logger } from '@' +import { LoggerService, neverendingPromise } from '@' import { Command } from 'commander' import { mothership } from './mothership' @@ -11,9 +11,11 @@ export const ServeCommand = () => { .description(`Run the PocketHost mothership`) .option(`--isolate`, `Use Docker for process isolation.`, false) .action(async (options: Options) => { - logger().context({ cli: 'mothership:serve' }) - console.log({ options }) + const logger = LoggerService().create(`cli:mothership:serve`) + const { dbg } = logger + dbg({ options }) await mothership(options) + await neverendingPromise(logger) }) return cmd } diff --git a/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/mothership.ts b/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/mothership.ts index 4e43d559..bef5acd1 100644 --- a/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/mothership.ts +++ b/packages/pockethost/src/cli/commands/MothershipCommand/ServeCommand/mothership.ts @@ -1,13 +1,16 @@ import { + _MOTHERSHIP_APP_ROOT, APP_URL, DISCORD_ALERT_CHANNEL_URL, DISCORD_HEALTH_CHANNEL_URL, DISCORD_STREAM_CHANNEL_URL, DISCORD_TEST_CHANNEL_URL, + exitHook, GobotService, IS_DEV, - LS_WEBHOOK_SECRET, LoggerService, + LS_WEBHOOK_SECRET, + mkContainerHomePath, MOTHERSHIP_CLOUDFLARE_ACCOUNT_ID, MOTHERSHIP_CLOUDFLARE_API_TOKEN, MOTHERSHIP_CLOUDFLARE_ZONE_ID, @@ -16,16 +19,16 @@ import { MOTHERSHIP_MIGRATIONS_DIR, MOTHERSHIP_PORT, MOTHERSHIP_SEMVER, + MOTHERSHIP_URL, TEST_EMAIL, - _MOTHERSHIP_APP_ROOT, - mkContainerHomePath, + tryFetch, } from '@' import { GobotOptions } from 'gobot' export type MothershipConfig = {} export async function mothership(cfg: MothershipConfig) { - const logger = LoggerService().create(`Mothership`) + const logger = LoggerService().create(`cli:mothership`) const { dbg, error, info, warn } = logger info(`Starting`) @@ -44,13 +47,13 @@ export async function mothership(cfg: MothershipConfig) { MOTHERSHIP_CLOUDFLARE_ZONE_ID: MOTHERSHIP_CLOUDFLARE_ZONE_ID(), MOTHERSHIP_CLOUDFLARE_ACCOUNT_ID: MOTHERSHIP_CLOUDFLARE_ACCOUNT_ID(), } - dbg(env) + dbg({ env }) const options: Partial = { version: MOTHERSHIP_SEMVER(), env, } - dbg(`options`, options) + dbg({ options }) const { gobot } = GobotService() const bot = await gobot(`pocketbase`, options) @@ -71,6 +74,30 @@ export async function mothership(cfg: MothershipConfig) { args.push(`--dev`) } dbg({ args }) - const code = await bot.run(args, { env, cwd: _MOTHERSHIP_APP_ROOT() }) - dbg({ code }) + + bot.run(args, { env, cwd: _MOTHERSHIP_APP_ROOT() }, (proc) => { + proc.stdout.on('data', (data) => { + info(data.toString()) + }) + proc.stderr.on('data', (data) => { + error(data.toString()) + }) + proc.on('close', (code, signal) => { + error(`Pocketbase exited with code ${code} and signal ${signal}`) + }) + proc.on('error', (err) => { + error(`Pocketbase error: ${err}`) + }) + proc.on('exit', (code, signal) => { + error(`Pocketbase exited with code ${code} and signal ${signal}`) + }) + proc.on('message', (msg) => { + console.log(`***message`, msg) + }) + exitHook(() => { + proc.kill() + }) + }) + const ready = tryFetch(MOTHERSHIP_URL(`/api/health`), { logger }) + return ready } diff --git a/packages/pockethost/src/cli/commands/ServeCommand/index.ts b/packages/pockethost/src/cli/commands/ServeCommand/index.ts index 4cabd9bb..cd257b0f 100644 --- a/packages/pockethost/src/cli/commands/ServeCommand/index.ts +++ b/packages/pockethost/src/cli/commands/ServeCommand/index.ts @@ -1,4 +1,4 @@ -import { logger, neverendingPromise } from '@' +import { LoggerService, neverendingPromise } from '@' import { Command } from 'commander' import { daemon } from '../EdgeCommand/DaemonCommand/ServeCommand/daemon' import { firewall } from '../FirewallCommand/ServeCommand/firewall/server' @@ -10,13 +10,17 @@ type Options = { export const ServeCommand = () => { const cmd = new Command(`serve`).description(`Run the entire PocketHost stack`).action(async (options: Options) => { - logger().context({ cli: 'serve' }) - const { dbg, error, info, warn } = logger() + const logger = LoggerService().create(`cli:serve`) + const { dbg, error, info, warn } = logger info(`Starting`) - await Promise.all([mothership(options), daemon(), firewall()]) - - await neverendingPromise() + await mothership(options) + dbg(`Mothership ready`) + await daemon() + dbg(`Daemon ready`) + await firewall() + dbg(`Firewall ready`) + await neverendingPromise(logger) }) return cmd } diff --git a/packages/pockethost/src/cli/ioc.ts b/packages/pockethost/src/cli/ioc.ts index 463864f7..a32278d4 100644 --- a/packages/pockethost/src/cli/ioc.ts +++ b/packages/pockethost/src/cli/ioc.ts @@ -1,9 +1,9 @@ -import { GobotService, ioc, RegisterEnvSettingsService, WinstonLoggerService } from '@' +import { ConsoleLogger, DEBUG, GobotService, ioc, LogLevelName, RegisterEnvSettingsService } from '@' import { version } from '../../package.json' export const initIoc = async () => { - const logger = await WinstonLoggerService({}) + const logger = ConsoleLogger({ level: DEBUG() ? LogLevelName.Debug : LogLevelName.Info }) ioc('logger', logger.context('ph_version', version)) RegisterEnvSettingsService() - GobotService({}) + GobotService({ logger }) } diff --git a/packages/pockethost/src/common/ConsoleLogger.ts b/packages/pockethost/src/common/ConsoleLogger.ts index 5e2247dc..192475ad 100644 --- a/packages/pockethost/src/common/ConsoleLogger.ts +++ b/packages/pockethost/src/common/ConsoleLogger.ts @@ -14,45 +14,56 @@ export function ConsoleLogger(initialConfig: Partial = {}): Logger let config: LoggerConfig = { level: LogLevelName.Info, pfx: [], + breadcrumbs: [], + context: {}, ...initialConfig, } - function log(level: LogLevelName, ...args: any[]) { + function log(level: LogLevelName, args: any[]) { if (isLevelGte(level, config.level)) { const prefix = config.pfx.length > 0 ? `[${config.pfx.join(':')}] ` : '' CONSOLE_METHODS[level](`${prefix}${level.toUpperCase()}:`, ...args) } } + const breadcrumbs: string[] = [] + const context: Record = {} + const withBreadcrumbs = (args: any[]) => { + return [breadcrumbs.map((b) => `[${b}]`).join(' '), ...args] + } + const logger: Logger = { raw(...args: any[]) { - log(LogLevelName.Raw, ...args) + log(LogLevelName.Raw, withBreadcrumbs(args)) }, trace(...args: any[]) { - log(LogLevelName.Trace, ...args) + log(LogLevelName.Trace, withBreadcrumbs(args)) }, debug(...args: any[]) { - log(LogLevelName.Debug, ...args) + log(LogLevelName.Debug, withBreadcrumbs(args)) }, dbg(...args: any[]) { logger.debug(...args) }, info(...args: any[]) { - log(LogLevelName.Info, ...args) + log(LogLevelName.Info, withBreadcrumbs(args)) }, warn(...args: any[]) { - log(LogLevelName.Warn, ...args) + log(LogLevelName.Warn, withBreadcrumbs(args)) }, error(...args: any[]) { - log(LogLevelName.Error, ...args) + log(LogLevelName.Error, withBreadcrumbs(args)) }, criticalError(...args: any[]) { - logger.error('CRITICAL:', ...args) + logger.error('CRITICAL:', withBreadcrumbs(args)) }, create(name: string, configOverride?: Partial): Logger { const newConfig = { ...config, + breadcrumbs: [...breadcrumbs, name], + context: { ...context }, pfx: [...config.pfx, name], + logger: logger, ...configOverride, } return ConsoleLogger(newConfig) @@ -60,16 +71,22 @@ export function ConsoleLogger(initialConfig: Partial = {}): Logger child(name: string): Logger { return logger.create(name) }, - breadcrumb(s: object): Logger { - console.log('Breadcrumb:', s) + breadcrumb(s: string): Logger { + breadcrumbs.push(s) return logger }, context(name: string | object, value?: string | number): Logger { - console.log('Context:', name, value) + if (typeof name === `object`) { + Object.entries(name).forEach(([k, v]) => { + context[k] = v + }) + } else { + context[name] = value + } return logger }, abort(...args: any[]): never { - log(LogLevelName.Abort, ...args) + log(LogLevelName.Abort, withBreadcrumbs(args)) process.exit(1) }, shutdown() { diff --git a/packages/pockethost/src/common/Logger.ts b/packages/pockethost/src/common/Logger.ts index 13d1fa6a..b9518f03 100644 --- a/packages/pockethost/src/common/Logger.ts +++ b/packages/pockethost/src/common/Logger.ts @@ -4,6 +4,8 @@ import { ConsoleLogger } from './ConsoleLogger' export type LoggerConfig = { level: LogLevelName pfx: string[] + breadcrumbs: string[] + context: Record } export const isLevelLte = (a: LogLevelName, b: LogLevelName) => { @@ -48,6 +50,8 @@ export const LogLevels = { [LogLevelName.Abort]: 6, } as const +export type LoggerContextValue = string | number | undefined + export type Logger = { raw: (...args: any[]) => void dbg: (...args: any[]) => void @@ -59,8 +63,8 @@ export type Logger = { child: (name: string) => Logger trace: (...args: any[]) => void debug: (...args: any[]) => void - breadcrumb: (s: object) => Logger - context: (name: string | object, value?: string | number) => Logger + breadcrumb: (s: string) => Logger + context: (name: string | object, value?: LoggerContextValue) => Logger abort: (...args: any[]) => never shutdown: () => void setLevel: (level: LogLevelName) => void diff --git a/packages/pockethost/src/common/mkSingleton.ts b/packages/pockethost/src/common/mkSingleton.ts index 8d01362a..43cdf48f 100644 --- a/packages/pockethost/src/common/mkSingleton.ts +++ b/packages/pockethost/src/common/mkSingleton.ts @@ -1,6 +1,10 @@ +import { Logger } from './Logger' + export type SingletonApi = Object -export type SingletonBaseConfig = {} +export type SingletonBaseConfig = { + logger: Logger +} export function mkSingleton( factory: (config: TConfig) => TInstance diff --git a/packages/pockethost/src/common/schema/Instance.ts b/packages/pockethost/src/common/schema/Instance.ts index 47c8906e..a2c77cb1 100644 --- a/packages/pockethost/src/common/schema/Instance.ts +++ b/packages/pockethost/src/common/schema/Instance.ts @@ -19,6 +19,14 @@ export type InstanceSecretCollection = { [name: InstanceSecretKey]: InstanceSecretValue } +export type InstanceWebhookEndpoint = string +export type InstanceWebhookValue = string +export type InstanceWebhookCollection = InstanceWebhookItem[] +export type InstanceWebhookItem = { + endpoint: InstanceWebhookEndpoint + value: InstanceWebhookValue +} + export type InstanceFields = BaseFields & { region: string subdomain: Subdomain @@ -26,6 +34,7 @@ export type InstanceFields = BaseFields & { status: InstanceStatus version: VersionId secrets: InstanceSecretCollection | null + webhooks: InstanceWebhookCollection | null power: boolean suspension: string syncAdmin: boolean diff --git a/packages/pockethost/src/common/schema/Rest/UpdateInstance.ts b/packages/pockethost/src/common/schema/Rest/UpdateInstance.ts index 92783c23..7c8855a9 100644 --- a/packages/pockethost/src/common/schema/Rest/UpdateInstance.ts +++ b/packages/pockethost/src/common/schema/Rest/UpdateInstance.ts @@ -3,7 +3,9 @@ import { InstanceFields, InstanceId } from '..' export type UpdateInstancePayload = { id: InstanceId - fields: Partial> + fields: Partial< + Pick + > } export const SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/ @@ -37,6 +39,18 @@ export const UpdateInstancePayloadSchema: JSONSchemaType }, required: [], }, + webhooks: { + type: 'array', + nullable: true, + items: { + type: 'object', + properties: { + endpoint: { type: 'string' }, + value: { type: 'string' }, + }, + required: ['endpoint', 'value'], + }, + }, dev: { type: 'boolean', nullable: true }, cname: { type: 'string', nullable: true }, }, diff --git a/packages/pockethost/src/core/DiscordTransport.ts b/packages/pockethost/src/core/DiscordTransport.ts deleted file mode 100644 index d56b2ac5..00000000 --- a/packages/pockethost/src/core/DiscordTransport.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/exit.ts b/packages/pockethost/src/core/exit.ts index 4d021173..e63a818b 100644 --- a/packages/pockethost/src/core/exit.ts +++ b/packages/pockethost/src/core/exit.ts @@ -1,4 +1,4 @@ -import { logger } from '@' +import { Logger } from '@' import exitHook, { asyncExitHook as _, gracefulExit as __ } from 'exit-hook' export const asyncExitHook = (cb: () => Promise) => _(cb, { wait: 5000 }) @@ -11,7 +11,7 @@ export const gracefulExit = async (signal?: number) => { } export { exitHook } -export const neverendingPromise = () => +export const neverendingPromise = (logger: Logger) => new Promise((resolve) => { - logger().dbg('Neverending promise') + logger.dbg('Neverending promise') }) diff --git a/packages/pockethost/src/core/index.ts b/packages/pockethost/src/core/index.ts index 68dddef8..86636484 100644 --- a/packages/pockethost/src/core/index.ts +++ b/packages/pockethost/src/core/index.ts @@ -10,4 +10,3 @@ export * from './process' export * from './Settings' export * from './smartFetch' export * from './tryFetch' -export * from './winston' diff --git a/packages/pockethost/src/core/tryFetch.ts b/packages/pockethost/src/core/tryFetch.ts index fd203fbb..8d251847 100644 --- a/packages/pockethost/src/core/tryFetch.ts +++ b/packages/pockethost/src/core/tryFetch.ts @@ -1,4 +1,4 @@ -import { LoggerService } from '@' +import { Logger, LoggerService } from '@' import fetch, { Response } from 'node-fetch' export const TRYFETCH_RETRY_MS = 50 @@ -8,6 +8,7 @@ export type TryFetchConfig = { preflight: () => Promise retryMs: number timeoutMs: number + logger: Logger } /** @@ -23,14 +24,14 @@ export type TryFetchConfig = { * Note: tryFetch exits ONLY on success or a rejected preflight. */ export const tryFetch = async (url: string, config?: Partial) => { - const { preflight, retryMs, timeoutMs }: TryFetchConfig = { + const { preflight, retryMs, timeoutMs, logger }: TryFetchConfig = { preflight: async () => true, retryMs: TRYFETCH_RETRY_MS, timeoutMs: TRYFETCH_TIMEOUT_MS, + logger: config?.logger ?? LoggerService(), ...config, } - const logger = LoggerService().create(`tryFetch`).breadcrumb({ url }) - const { dbg } = logger + const { dbg } = logger.create(`tryFetch`).breadcrumb(url) return new Promise((resolve, reject) => { const again = () => setTimeout(_real_tryFetch, retryMs) const _real_tryFetch = async () => { diff --git a/packages/pockethost/src/core/winston.ts b/packages/pockethost/src/core/winston.ts deleted file mode 100644 index ba6dd1f4..00000000 --- a/packages/pockethost/src/core/winston.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { asyncExitHook, DEBUG, DISCORD_ALERT_CHANNEL_URL, Logger, mkSingleton } from '@' -import { inspect } from 'node:util' -import winston from 'winston' -import { DiscordTransport } from './DiscordTransport' - -const format = winston.format.combine( - winston.format.colorize(), - winston.format.printf(({ level, message, timestamp, ...meta }) => { - const final: string[] = [] - // @ts-expect-error - ;[...message, meta].forEach((m: string) => { - if (typeof m === 'string' && !!m.match(/\n/)) { - final.push(...m.split(/\n/)) - } else if (typeof m === 'object') { - // Filter out Symbol properties and inspect the object - const filtered = Object.fromEntries(Object.entries(m).filter(([key]) => typeof key === 'string')) - final.push( - inspect(filtered, { - depth: null, - compact: true, - breakLength: Infinity, - }) - ) - } 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 - - asyncExitHook(async () => { - console.log('Closing Winston logger outside') - return new Promise((resolve) => { - console.log('Closing Winston logger inside promise') - setTimeout(() => { - console.log('Closing Winston logger inside timeout') - logger.close() - resolve() - }, 2000) - }) - }) - - { - 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 - }, - context: (name: string | object, value?: string | number) => { - if (typeof name === 'string') { - if (value !== undefined) { - logger.defaultMeta[name] = value - } else { - delete logger.defaultMeta[name] - } - } else { - Object.assign(logger.defaultMeta, name) - } - return api - }, - shutdown: () => {}, - child: (name) => api.create(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 8194d20c..0c6458d1 100644 --- a/packages/pockethost/src/services/InstanceLoggerService/index.ts +++ b/packages/pockethost/src/services/InstanceLoggerService/index.ts @@ -1,4 +1,4 @@ -import { ensureInstanceDirectoryStructure, logger, LoggerService, mkInstanceDataPath, stringify } from '@' +import { ensureInstanceDirectoryStructure, Logger, mkInstanceDataPath, stringify } from '@' import Bottleneck from 'bottleneck' import { existsSync } from 'fs' import { appendFile, cp, stat, truncate } from 'fs/promises' @@ -74,8 +74,8 @@ const MultiChannelLimiter = () => { const limiter = MultiChannelLimiter() -export function InstanceLogWriter(instanceId: string, volume: string, target: string) { - const lgr = logger().create(`InstanceLogWriter`).breadcrumb({ instanceId, target }) +export function InstanceLogWriter(instanceId: string, volume: string, target: string, logger: Logger) { + const lgr = logger.create(`InstanceLogWriter`).breadcrumb(`${instanceId}-${target}`) const { dbg, info, error, warn } = lgr ensureInstanceDirectoryStructure(instanceId, volume, lgr) @@ -124,9 +124,8 @@ export function InstanceLogWriter(instanceId: string, volume: string, target: st return api } -export function InstanceLogReader(instanceId: string, volume: string, target: string) { - const logger = LoggerService().create(instanceId).breadcrumb({ target }) - const { dbg, info, error, warn } = logger +export function InstanceLogReader(instanceId: string, volume: string, target: string, logger: Logger) { + const { dbg, info, error, warn } = logger.create(`InstanceLogReader`).breadcrumb(`${instanceId}-${target}`) ensureInstanceDirectoryStructure(instanceId, volume, logger) diff --git a/packages/pockethost/src/services/InstanceService/index.ts b/packages/pockethost/src/services/InstanceService/index.ts index d9c6a19c..8c7149a8 100644 --- a/packages/pockethost/src/services/InstanceService/index.ts +++ b/packages/pockethost/src/services/InstanceService/index.ts @@ -48,7 +48,7 @@ export type InstanceServiceConfig = SingletonBaseConfig & { export type InstanceServiceApi = AsyncReturnType export const instanceService = mkSingleton(async (config: InstanceServiceConfig) => { - const instanceServiceLogger = LoggerService().create('InstanceService') + const instanceServiceLogger = (config.logger ?? LoggerService()).create('InstanceService') const { dbg, raw, error, warn } = instanceServiceLogger const { client } = await MothershipAdminClientService() @@ -62,7 +62,7 @@ export const instanceService = mkSingleton(async (config: InstanceServiceConfig) const { id, subdomain, version } = instance const systemInstanceLogger = instanceServiceLogger.create(`${subdomain}:${id}:${version}`) const { dbg, warn, error, info, trace } = systemInstanceLogger - const userInstanceLogger = InstanceLogWriter(instance.id, instance.volume, `exec`) + const userInstanceLogger = InstanceLogWriter(instance.id, instance.volume, `exec`, systemInstanceLogger) shutdownManager.push(() => { dbg(`Shutting down`) @@ -140,6 +140,7 @@ export const instanceService = mkSingleton(async (config: InstanceServiceConfig) PH_INSTANCE_URL: mkInstanceUrl(instance), }, version, + logger: systemInstanceLogger, } /** Add admin sync info if enabled */ @@ -183,6 +184,7 @@ export const instanceService = mkSingleton(async (config: InstanceServiceConfig) if (stopped()) throw new Error(`Container stopped ${id}`) return started() }, + logger: systemInstanceLogger, }) /** Idle check */ @@ -234,7 +236,7 @@ export const instanceService = mkSingleton(async (config: InstanceServiceConfig) const mirror = await MothershipMirrorService() ;(await proxyService()).use(async (req, res, next) => { - const logger = LoggerService().create(`InstanceRequest`) + const logger = (config.logger ?? LoggerService()).create(`InstanceRequest`) const { dbg } = logger diff --git a/packages/pockethost/src/services/InstanceService/mkInstanceCache.ts b/packages/pockethost/src/services/InstanceService/mkInstanceCache.ts index eb4c01ff..7aebe304 100644 --- a/packages/pockethost/src/services/InstanceService/mkInstanceCache.ts +++ b/packages/pockethost/src/services/InstanceService/mkInstanceCache.ts @@ -11,32 +11,42 @@ import { import { forEach } from '@s-libs/micro-dash' export const mkInstanceCache = (client: PocketBase) => { - const { dbg } = LoggerService().create(`InstanceCache`) + const { dbg, error } = LoggerService().create(`InstanceCache`) const cache: { [_: InstanceId]: InstanceFields_WithUser | undefined } = {} const byUid: { [_: UserId]: { [_: InstanceId]: InstanceFields_WithUser } } = {} - client.collection(`users`).subscribe(`*`, (e) => { - const { action, record } = e - if ([`create`, `update`].includes(action)) { - dbg({ action, record }) - updateUser(record) - } - }) - - client.collection(INSTANCE_COLLECTION).subscribe( - `*`, - (e) => { + client + .collection(`users`) + .subscribe(`*`, (e) => { const { action, record } = e if ([`create`, `update`].includes(action)) { - setItem(record) dbg({ action, record }) + updateUser(record) } - }, - { expand: 'uid' } - ) + }) + .catch((e) => { + error(e) + }) + + client + .collection(INSTANCE_COLLECTION) + .subscribe( + `*`, + (e) => { + const { action, record } = e + if ([`create`, `update`].includes(action)) { + setItem(record) + dbg({ action, record }) + } + }, + { expand: 'uid' } + ) + .catch((e) => { + error(e) + }) function blankItem(host: string) { cache[host] = undefined diff --git a/packages/pockethost/src/services/MothershipAdminClientService/createAdminPbClient.ts b/packages/pockethost/src/services/MothershipAdminClientService/createAdminPbClient.ts index 776a7055..67276e1c 100644 --- a/packages/pockethost/src/services/MothershipAdminClientService/createAdminPbClient.ts +++ b/packages/pockethost/src/services/MothershipAdminClientService/createAdminPbClient.ts @@ -2,7 +2,7 @@ import { GetUserTokenPayload, GetUserTokenPayloadSchema, GetUserTokenResult, - LoggerService, + Logger, PocketBase, RestCommands, RestMethods, @@ -14,8 +14,8 @@ import { createInstanceMixin } from './InstanceMIxin' export type PocketbaseClientApi = ReturnType -export const createAdminPbClient = (url: string) => { - const _clientLogger = LoggerService().create('PbClient') +export const createAdminPbClient = (url: string, logger: Logger) => { + const _clientLogger = logger.create('PbClient') const { info } = _clientLogger info(`Initializing client: ${url}`) diff --git a/packages/pockethost/src/services/MothershipAdminClientService/index.ts b/packages/pockethost/src/services/MothershipAdminClientService/index.ts index 590b8f02..70ed7cd5 100644 --- a/packages/pockethost/src/services/MothershipAdminClientService/index.ts +++ b/packages/pockethost/src/services/MothershipAdminClientService/index.ts @@ -5,15 +5,17 @@ import { MOTHERSHIP_ADMIN_USERNAME, MOTHERSHIP_URL, PocketBase, + SingletonBaseConfig, mergeConfig, mkSingleton, } from '@' import { createAdminPbClient } from './createAdminPbClient' -export type ClientServiceConfig = { +export type ClientServiceConfig = SingletonBaseConfig & { url: string username: string password: string + logger: Logger } export type MixinContext = { @@ -21,18 +23,18 @@ export type MixinContext = { logger: Logger } -export const MothershipAdminClientService = mkSingleton(async (cfg: Partial = {}) => { - const { url, username, password } = mergeConfig( +export const MothershipAdminClientService = mkSingleton(async (cfg: ClientServiceConfig) => { + const { url, username, password, logger } = mergeConfig( { url: MOTHERSHIP_URL(), username: MOTHERSHIP_ADMIN_USERNAME(), password: MOTHERSHIP_ADMIN_PASSWORD(), + logger: LoggerService(), }, cfg ) - const _clientLogger = LoggerService().create(`client singleton`) - const { dbg, error } = _clientLogger - const client = createAdminPbClient(url) + const { dbg, error } = logger.create(`MothershipAdminClientService`) + const client = createAdminPbClient(url, logger) while (true) { try { diff --git a/packages/pockethost/src/services/MothershipMirrorService/index.ts b/packages/pockethost/src/services/MothershipMirrorService/index.ts index 55f27f90..6bad88e9 100644 --- a/packages/pockethost/src/services/MothershipMirrorService/index.ts +++ b/packages/pockethost/src/services/MothershipMirrorService/index.ts @@ -16,7 +16,7 @@ export type MothershipMirrorServiceConfig = SingletonBaseConfig & { } export const MothershipMirrorService = mkSingleton(async (config: MothershipMirrorServiceConfig) => { - const { dbg, error } = LoggerService().create(`MothershipMirrorService`) + const { dbg, error } = (config.logger ?? LoggerService()).create(`MothershipMirrorService`) const client = config.client @@ -117,6 +117,7 @@ export const MothershipMirrorService = mkSingleton(async (config: MothershipMirr upsertInstance(instance) }) }) + .catch(error) const usersPromise = client .collection(`users`) .getFullList() @@ -126,6 +127,7 @@ export const MothershipMirrorService = mkSingleton(async (config: MothershipMirr upsertUser(user) }) }) + .catch(error) await Promise.all([instancesPromise, usersPromise]) } await init().catch(error) diff --git a/packages/pockethost/src/services/PocketBaseService/index.ts b/packages/pockethost/src/services/PocketBaseService/index.ts index 5794f6b4..e2907833 100644 --- a/packages/pockethost/src/services/PocketBaseService/index.ts +++ b/packages/pockethost/src/services/PocketBaseService/index.ts @@ -2,6 +2,7 @@ import { APEX_DOMAIN, GobotService, InstanceLogWriter, + Logger, LoggerService, SingletonBaseConfig, asyncExitHook, @@ -30,6 +31,7 @@ export type SpawnConfig = { stdout?: MemoryStream stderr?: MemoryStream dev?: boolean + logger: Logger } export type PocketbaseServiceApi = AsyncReturnType @@ -46,7 +48,7 @@ export type PocketbaseProcess = { export const DOCKER_INSTANCE_IMAGE_NAME = `benallfree/pockethost-instance` export const createPocketbaseService = async (config: PocketbaseServiceConfig) => { - const _serviceLogger = LoggerService().create('PocketbaseService') + const _serviceLogger = (config.logger ?? LoggerService()).create('PocketbaseService') const { dbg, error, warn, abort } = _serviceLogger const { gobot } = GobotService() @@ -58,7 +60,7 @@ export const createPocketbaseService = async (config: PocketbaseServiceConfig) = const _spawn = async (cfg: SpawnConfig) => { const cm = createCleanupManager() - const logger = LoggerService().create('spawn') + const logger = (cfg.logger ?? config.logger ?? LoggerService()).create('spawn') const { dbg, info, warn, error } = logger const _cfg: Required = { @@ -72,8 +74,8 @@ export const createPocketbaseService = async (config: PocketbaseServiceConfig) = } const { version, subdomain, instanceId, volume, extraBinds, env, stderr, stdout, dev } = _cfg - logger.breadcrumb({ subdomain, instanceId }) - const iLogger = InstanceLogWriter(instanceId, volume, 'exec') + logger.breadcrumb(`${subdomain}-${instanceId}`) + const iLogger = InstanceLogWriter(instanceId, volume, 'exec', logger) const _version = version || maxVersion // If _version is blank, we use the max version available const realVersion = await bot.maxSatisfyingVersion(_version) @@ -260,7 +262,7 @@ export const createPocketbaseService = async (config: PocketbaseServiceConfig) = }) const url = mkInternalUrl(container.portBinding) - logger.breadcrumb({ url }) + logger.breadcrumb(url) dbg(`Making exit hook for ${url}`) const unsub = asyncExitHook(async () => { await api.kill() diff --git a/packages/pockethost/src/services/RealtimeLog.ts b/packages/pockethost/src/services/RealtimeLog.ts index 05ee8714..985dbd24 100644 --- a/packages/pockethost/src/services/RealtimeLog.ts +++ b/packages/pockethost/src/services/RealtimeLog.ts @@ -60,7 +60,7 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { dbg(`Instance is `, instance) /** Get a database connection */ - const instanceLogger = InstanceLogReader(instance.id, instance.volume, `exec`) + const instanceLogger = InstanceLogReader(instance.id, instance.volume, `exec`, logger) /** Start the stream */ res.writeHead(200, {