chore(pockethost): remove winston

This commit is contained in:
Ben Allfree 2025-07-21 17:09:13 -07:00
parent e1f90aa22b
commit 8ec5d00df4
29 changed files with 228 additions and 282 deletions

View File

@ -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",

View File

@ -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)
}

View File

@ -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<FileStat> {
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<any> {
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)

View File

@ -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<FtpConfig> = {}) => {
export const ftpService = mkSingleton((config: FtpConfig) => {
const { mothershipUrl } = mergeConfig(
{
mothershipUrl: MOTHERSHIP_URL(),

View File

@ -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)
}

View File

@ -1,5 +1,6 @@
import {
InstanceFields,
LoggerService,
mkInstanceDataPath,
MOTHERSHIP_ADMIN_PASSWORD,
MOTHERSHIP_ADMIN_USERNAME,
@ -18,7 +19,10 @@ export const MigrateCommand = () => {
.option(`-i, --instance <instanceId>`, `The instance to migrate`)
.option(`-m, --mount-point <mountPoint>`, `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())

View File

@ -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)
}

View File

@ -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
}

View File

@ -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<GobotOptions> = {
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
}

View File

@ -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
}

View File

@ -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 })
}

View File

@ -14,45 +14,56 @@ export function ConsoleLogger(initialConfig: Partial<LoggerConfig> = {}): 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<string, string | number | undefined> = {}
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<LoggerConfig>): 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<LoggerConfig> = {}): 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() {

View File

@ -4,6 +4,8 @@ import { ConsoleLogger } from './ConsoleLogger'
export type LoggerConfig = {
level: LogLevelName
pfx: string[]
breadcrumbs: string[]
context: Record<string, LoggerContextValue>
}
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

View File

@ -1,6 +1,10 @@
import { Logger } from './Logger'
export type SingletonApi = Object
export type SingletonBaseConfig = {}
export type SingletonBaseConfig = {
logger: Logger
}
export function mkSingleton<TConfig extends SingletonBaseConfig, TInstance extends SingletonApi>(
factory: (config: TConfig) => TInstance

View File

@ -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<TExtra = {}> = BaseFields & {
region: string
subdomain: Subdomain
@ -26,6 +34,7 @@ export type InstanceFields<TExtra = {}> = BaseFields & {
status: InstanceStatus
version: VersionId
secrets: InstanceSecretCollection | null
webhooks: InstanceWebhookCollection | null
power: boolean
suspension: string
syncAdmin: boolean

View File

@ -3,7 +3,9 @@ import { InstanceFields, InstanceId } from '..'
export type UpdateInstancePayload = {
id: InstanceId
fields: Partial<Pick<InstanceFields, 'power' | 'secrets' | 'subdomain' | 'syncAdmin' | 'version' | 'dev' | 'cname'>>
fields: Partial<
Pick<InstanceFields, 'power' | 'secrets' | 'webhooks' | 'subdomain' | 'syncAdmin' | 'version' | 'dev' | 'cname'>
>
}
export const SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/
@ -37,6 +39,18 @@ export const UpdateInstancePayloadSchema: JSONSchemaType<UpdateInstancePayload>
},
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 },
},

View File

@ -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()
}
}

View File

@ -1,4 +1,4 @@
import { logger } from '@'
import { Logger } from '@'
import exitHook, { asyncExitHook as _, gracefulExit as __ } from 'exit-hook'
export const asyncExitHook = (cb: () => Promise<any>) => _(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')
})

View File

@ -10,4 +10,3 @@ export * from './process'
export * from './Settings'
export * from './smartFetch'
export * from './tryFetch'
export * from './winston'

View File

@ -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<boolean>
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<TryFetchConfig>) => {
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<Response>((resolve, reject) => {
const again = () => setTimeout(_real_tryFetch, retryMs)
const _real_tryFetch = async () => {

View File

@ -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<void>((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)
})

View File

@ -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)

View File

@ -48,7 +48,7 @@ export type InstanceServiceConfig = SingletonBaseConfig & {
export type InstanceServiceApi = AsyncReturnType<typeof instanceService>
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

View File

@ -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<UserFields>(`*`, (e) => {
const { action, record } = e
if ([`create`, `update`].includes(action)) {
dbg({ action, record })
updateUser(record)
}
})
client.collection(INSTANCE_COLLECTION).subscribe<InstanceFields_WithUser>(
`*`,
(e) => {
client
.collection(`users`)
.subscribe<UserFields>(`*`, (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<InstanceFields_WithUser>(
`*`,
(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

View File

@ -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<typeof createAdminPbClient>
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}`)

View File

@ -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<ClientServiceConfig> = {}) => {
const { url, username, password } = mergeConfig<ClientServiceConfig>(
export const MothershipAdminClientService = mkSingleton(async (cfg: ClientServiceConfig) => {
const { url, username, password, logger } = mergeConfig<ClientServiceConfig>(
{
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 {

View File

@ -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<UserFields>()
@ -126,6 +127,7 @@ export const MothershipMirrorService = mkSingleton(async (config: MothershipMirr
upsertUser(user)
})
})
.catch(error)
await Promise.all([instancesPromise, usersPromise])
}
await init().catch(error)

View File

@ -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<typeof createPocketbaseService>
@ -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<SpawnConfig> = {
@ -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()

View File

@ -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, {