diff --git a/packages/common/src/schema/InstanceLog.ts b/packages/common/src/schema/InstanceLog.ts index f16cc1fa..61033f7e 100644 --- a/packages/common/src/schema/InstanceLog.ts +++ b/packages/common/src/schema/InstanceLog.ts @@ -1,15 +1,11 @@ -import { BaseFields } from './types' - export enum StreamNames { - Info = 'info', - Warning = 'warning', - Debug = 'debug', - Error = 'error', - System = 'system', + StdOut = 'stdout', + StdErr = 'stderr', } -export type InstanceLogFields = BaseFields & { +export type InstanceLogFields = { message: string + time: string stream: StreamNames } diff --git a/packages/daemon/package.json b/packages/daemon/package.json index 0d2f5a61..02933de4 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -43,7 +43,8 @@ "tmp": "^0.2.1", "tsup": "^7.2.0", "tsx": "^3.11.0", - "url-pattern": "^1.0.3" + "url-pattern": "^1.0.3", + "winston": "^3.11.0" }, "devDependencies": { "@swc/cli": "^0.1.62", diff --git a/packages/daemon/src/constants.ts b/packages/daemon/src/constants.ts index 5773e0c0..a1cec96e 100644 --- a/packages/daemon/src/constants.ts +++ b/packages/daemon/src/constants.ts @@ -34,6 +34,8 @@ export const mkEdgeSubdomain = (subdomain: string) => mkFqDomain(`${subdomain}.${PUBLIC_EDGE_APEX_DOMAIN}`) export const mkEdgeUrl = (subdomain: string, path = '') => mkUrl(mkEdgeSubdomain(subdomain), path) +export const mkInstanceDataPath = (instanceId: string, ...path: string[]) => + join(DAEMON_PB_DATA_DIR, instanceId, ...path) // Derived export const MOTHERSHIP_URL = `${PUBLIC_HTTP_PROTOCOL}://${PUBLIC_MOTHERSHIP_NAME}.${PUBLIC_EDGE_APEX_DOMAIN}` diff --git a/packages/daemon/src/server.ts b/packages/daemon/src/server.ts index 8b547691..1b268bdf 100644 --- a/packages/daemon/src/server.ts +++ b/packages/daemon/src/server.ts @@ -18,7 +18,6 @@ import { } from '$services' import { LoggerService } from '@pockethost/common' import { centralDbService } from './services/CentralDbService' -import { instanceLoggerService } from './services/InstanceLoggerService' import { ipWhitelistService } from './services/IpWhitelistService' import { updaterService } from './services/UpdaterService/UpdaterService' // gen:import @@ -99,7 +98,6 @@ global.EventSource = require('eventsource') coreInternalUrl: url, }) await ipWhitelistService({ logger }) - await instanceLoggerService({ logger }) await sqliteService({ logger }) await realtimeLog({ logger }) await instanceService({ diff --git a/packages/daemon/src/services/CentralDbService.ts b/packages/daemon/src/services/CentralDbService.ts index 07dbc0a5..40aa6dfd 100644 --- a/packages/daemon/src/services/CentralDbService.ts +++ b/packages/daemon/src/services/CentralDbService.ts @@ -1,5 +1,5 @@ import { PUBLIC_MOTHERSHIP_NAME } from '$constants' -import { SingletonBaseConfig, mkSingleton } from '@pockethost/common' +import { mkSingleton, SingletonBaseConfig } from '@pockethost/common' import { proxyService } from './ProxyService' export type CentralDbServiceConfig = SingletonBaseConfig @@ -21,6 +21,7 @@ export const centralDbService = mkSingleton( `Forwarding proxy request for ${req.url} to central instance ${target}`, ) proxy.web(req, res, { target }) + return true }, `CentralDbService`, ) diff --git a/packages/daemon/src/services/InstanceLoggerService/DaemonContext.ts b/packages/daemon/src/services/InstanceLoggerService/DaemonContext.ts deleted file mode 100644 index b16884e5..00000000 --- a/packages/daemon/src/services/InstanceLoggerService/DaemonContext.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Logger } from '@pockethost/common' - -export type DaemonContext = { - parentLogger: Logger -} diff --git a/packages/daemon/src/services/InstanceLoggerService/SqliteLogger.ts b/packages/daemon/src/services/InstanceLoggerService/SqliteLogger.ts deleted file mode 100644 index 1def1b89..00000000 --- a/packages/daemon/src/services/InstanceLoggerService/SqliteLogger.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { SqliteChangeEvent, sqliteService } from '$services' -import { - InstanceLogFields, - InstanceLogFields_Create, - RecordId, - StreamNames, - newId, - pocketNow, - safeCatch, -} from '@pockethost/common' -import knex from 'knex' -import { AsyncReturnType } from 'type-fest' -import { DaemonContext } from './DaemonContext' - -export type SqliteLogger = AsyncReturnType -export const createSqliteLogger = async ( - logDbPath: string, - context: DaemonContext, -) => { - const { parentLogger } = context - const _dbLogger = parentLogger.create(`${logDbPath}`) - const { dbg, trace } = _dbLogger - - const { getDatabase } = sqliteService() - const db = await getDatabase(logDbPath) - - const conn = knex({ - client: 'sqlite3', - connection: { - filename: logDbPath, - }, - useNullAsDefault: true, - }) - - const write = safeCatch( - `write`, - _dbLogger, - async (message: string, stream: StreamNames = StreamNames.Info) => { - const _in: InstanceLogFields_Create = { - id: newId(), - message, - stream, - created: pocketNow(), - updated: pocketNow(), - } - const sql = conn('logs').insert(_in).toString() - trace(`Writing log ${JSON.stringify(_in)} ${sql}`) - await db.exec(sql) - }, - ) - - const subscribe = (cb: (e: SqliteChangeEvent) => void) => { - let _seenIds: { [_: RecordId]: boolean } | undefined = {} - - const unsub = db.subscribe((e) => { - // dbg(`Caught db modification ${logDbPath}`, e) - const { table, record } = e - if (table !== 'logs') return - if (_seenIds) { - _seenIds[record.id] = true - } - cb(e) - }) - return unsub - } - - const fetch = async (limit: number = 100) => { - return db.all( - `select * from logs order by created desc limit ${limit}`, - ) - } - - return { write, subscribe, fetch } -} diff --git a/packages/daemon/src/services/InstanceLoggerService/index.ts b/packages/daemon/src/services/InstanceLoggerService/index.ts index 7fa059de..a540bd0d 100644 --- a/packages/daemon/src/services/InstanceLoggerService/index.ts +++ b/packages/daemon/src/services/InstanceLoggerService/index.ts @@ -1,73 +1,84 @@ -import { DAEMON_PB_DATA_DIR } from '$constants' -import { sqliteService } from '$services' -import { - InstanceId, - mkSingleton, - SingletonBaseConfig, - StreamNames, -} from '@pockethost/common' -import { mkdirSync } from 'fs' -import { dirname, join } from 'path' -import { DaemonContext } from './DaemonContext' -import { createSqliteLogger, SqliteLogger } from './SqliteLogger' +import { mkInstanceDataPath } from '$constants' +import * as fs from 'fs' +import * as winston from 'winston' -const instances: { - [instanceId: InstanceId]: SqliteLogger -} = {} +type UnsubFunc = () => void -export const createInstanceLogger = async ( - instanceId: InstanceId, - context: DaemonContext, -) => { - const { parentLogger } = context - const _instanceLogger = parentLogger.create(`InstanceLogger`) +const loggers: { [key: string]: winston.Logger } = {} - if (!instances[instanceId]) { - const loggerApi = await (async () => { - const _thisLogger = _instanceLogger.create(instanceId) - const { dbg } = _thisLogger - - const logDbPath = join( - DAEMON_PB_DATA_DIR, - instanceId, - 'pb_data', - 'instance_logs.db', - ) - - dbg(`logs path`, logDbPath) - mkdirSync(dirname(logDbPath), { recursive: true }) - - dbg(`Running migrations`) - const { getDatabase } = sqliteService() - const db = await getDatabase(logDbPath) - await db.migrate({ - migrationsPath: join(__dirname, 'migrations'), - }) - - const api = await createSqliteLogger(logDbPath, { - parentLogger: _instanceLogger, - }) - await api.write(`Ran migrations`, StreamNames.System) - return api - })() - instances[instanceId] = loggerApi +function createOrGetLogger(instanceId: string, target: string): winston.Logger { + const loggerKey = `${instanceId}_${target}` + if (loggers[loggerKey]) { + return loggers[loggerKey]! } - return instances[instanceId]! + const logDirectory = mkInstanceDataPath(instanceId, `logs`) + console.log(`Creating ${logDirectory}`) + if (!fs.existsSync(logDirectory)) { + fs.mkdirSync(logDirectory, { recursive: true }) + } + + const logFile = mkInstanceDataPath(instanceId, `logs`, `${target}.log`) + + const logger = winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json(), + winston.format.printf((info) => { + return JSON.stringify({ + stream: info.level === 'info' ? 'stdout' : 'stderr', + time: info.timestamp, + message: info.message, + }) + }), + ), + transports: [ + new winston.transports.File({ + filename: logFile, + maxsize: 100 * 1024 * 1024, // 100MB + maxFiles: 10, + tailable: true, + zippedArchive: true, + }), + ], + }) + + loggers[loggerKey] = logger + return logger } -export type InstanceLoggerServiceConfig = SingletonBaseConfig +export function InstanceLogger(instanceId: string, target: string) { + const logger = createOrGetLogger(instanceId, target) -export const instanceLoggerService = mkSingleton( - (config: InstanceLoggerServiceConfig) => { - const { logger } = config - const { dbg } = logger.create(`InstanceLoggerService`) - dbg(`Starting up`) - return { - get: createInstanceLogger, - shutdown() { - dbg(`Shutting down`) - }, - } - }, -) + return { + info: (msg: string) => { + logger.info(msg) + }, + error: (msg: string) => { + logger.error(msg) + }, + tail: (linesBack: number, data: (line: string) => void): UnsubFunc => { + const stream = logger.stream({ start: -linesBack }) + const listener = (log: winston.LogEntry) => { + data(JSON.stringify(log)) + } + stream.on('log', listener) + + // Return an unsubscribe function to remove the listener when done + return () => { + stream.removeListener('log', listener) + } + }, + } +} + +// // Example usage +// const loggerInstance = InstanceLogger('123', 'my-target') +// loggerInstance.info('This is an info message') +// loggerInstance.error('This is an error message') +// const unsubscribe = loggerInstance.tail(10, (line) => { +// console.log(line) +// }) + +// // Later when you want to stop listening to the tail: +// // unsubscribe(); diff --git a/packages/daemon/src/services/InstanceLoggerService/migrations/001_create.sql b/packages/daemon/src/services/InstanceLoggerService/migrations/001_create.sql deleted file mode 100644 index c6642907..00000000 --- a/packages/daemon/src/services/InstanceLoggerService/migrations/001_create.sql +++ /dev/null @@ -1,23 +0,0 @@ --------------------------------------------------------------------------------- --- Up --------------------------------------------------------------------------------- - -CREATE TABLE "logs" ( - "id" TEXT UNIQUE, - "created" TEXT NOT NULL, - "updated" TEXT NOT NULL, - "message" TEXT NOT NULL, - "stream" TEXT NOT NULL, - PRIMARY KEY("id") -); - -CREATE INDEX "updated" ON "logs" ( - "updated" DESC -); - --------------------------------------------------------------------------------- --- Down --------------------------------------------------------------------------------- - -DROP INDEX "updated"; -DROP TABLE "logs"; \ No newline at end of file diff --git a/packages/daemon/src/services/InstanceService/InstanceService.ts b/packages/daemon/src/services/InstanceService/InstanceService.ts index 19d4c527..e6dfbd6d 100644 --- a/packages/daemon/src/services/InstanceService/InstanceService.ts +++ b/packages/daemon/src/services/InstanceService/InstanceService.ts @@ -16,16 +16,13 @@ import { InstanceStatus, mkSingleton, safeCatch, - serialAsyncExecutionGuard, SingletonBaseConfig, - StreamNames, } from '@pockethost/common' import { map, values } from '@s-libs/micro-dash' import Bottleneck from 'bottleneck' -import MemoryStream from 'memorystream' import { ClientResponseError } from 'pocketbase' import { AsyncReturnType } from 'type-fest' -import { instanceLoggerService } from '../InstanceLoggerService' +import { InstanceLogger } from '../InstanceLoggerService' import { pocketbaseService } from '../PocketBaseService/PocketBaseService' import { port } from '../PortManager' @@ -228,20 +225,7 @@ export const instanceService = mkSingleton( Create the user instance logger */ healthyGuard() - const userInstanceLogger = await instanceLoggerService().get( - instance.id, - { - parentLogger: systemInstanceLogger, - }, - ) - - const writeUserLog = serialAsyncExecutionGuard( - userInstanceLogger.write, - () => `${instance.id}:userLog`, - ) - shutdownManager.add(() => - writeUserLog(`Shutting down instance`).catch(error), - ) + const userInstanceLogger = InstanceLogger(instance.id, `exec`) /* Start the instance @@ -255,25 +239,11 @@ export const instanceService = mkSingleton( await updateInstanceStatus(id, InstanceStatus.Idle).catch(error) }) healthyGuard() - await writeUserLog(`Starting instance`) /* Spawn the child process */ - const stdout = new MemoryStream() - stdout.on('data', (data: Buffer) => { - data - .toString() - .split(/\n/) - .forEach((line) => writeUserLog(line)) - }) - const stderr = new MemoryStream() - stderr.on('data', (data: Buffer) => { - data - .toString() - .split(/\n/) - .forEach((line) => writeUserLog(line, StreamNames.Error)) - }) + const childProcess = await (async () => { try { const cp = await pbService.spawn({ @@ -283,31 +253,16 @@ export const instanceService = mkSingleton( port: newPort, env: instance.secrets || {}, version, - stdout, - stderr, - onUnexpectedStop: (code, stdout, stderr) => { + onUnexpectedStop: (code) => { warn( `PocketBase processes exited unexpectedly with ${code}. Putting in maintenance mode.`, ) - warn(stdout) - warn(stderr) shutdownManager.add(async () => { await updateInstance(instance.id, { maintenance: true, }) - await writeUserLog( + userInstanceLogger.error( `Putting instance in maintenance mode because it shut down with return code ${code}. `, - StreamNames.Error, - ) - await Promise.all( - stdout.map((data) => - writeUserLog(data, StreamNames.Error).catch(error), - ), - ) - await Promise.all( - stderr.map((data) => - writeUserLog(data, StreamNames.Error).catch(error), - ), ) }) setImmediate(() => { @@ -496,6 +451,7 @@ export const instanceService = mkSingleton( ) proxy.web(req, res, { target: api.internalUrl() }) + return true }, `InstanceService`, ) diff --git a/packages/daemon/src/services/IpWhitelistService/index.ts b/packages/daemon/src/services/IpWhitelistService/index.ts index 8eceae0d..e7898cfc 100644 --- a/packages/daemon/src/services/IpWhitelistService/index.ts +++ b/packages/daemon/src/services/IpWhitelistService/index.ts @@ -2,8 +2,8 @@ import { DAEMON_IPCIDR_LIST } from '$constants' import { assert } from '$util' import { LoggerService, - SingletonBaseConfig, mkSingleton, + SingletonBaseConfig, } from '@pockethost/common' import IPCIDR from 'ip-cidr' import { proxyService } from '../ProxyService' @@ -45,6 +45,7 @@ export const ipWhitelistService = mkSingleton( `Request from IP ${ipAddress} blocked because it is not in range.`, ) } + return false }, IP_WHITELIST_SERVICE_NAME, ) diff --git a/packages/daemon/src/services/PocketBaseService/PocketBaseService.ts b/packages/daemon/src/services/PocketBaseService/PocketBaseService.ts index 971be341..36bf429e 100644 --- a/packages/daemon/src/services/PocketBaseService/PocketBaseService.ts +++ b/packages/daemon/src/services/PocketBaseService/PocketBaseService.ts @@ -1,18 +1,18 @@ import { - DAEMON_PB_DATA_DIR, DAEMON_PB_HOOKS_DIR, DAEMON_PB_MIGRATIONS_DIR, + mkInstanceDataPath, PUBLIC_DEBUG, } from '$constants' import { assert, mkInternalUrl, tryFetch } from '$util' import { - InvocationPid, createCleanupManager, createTimerManager, + InvocationPid, } from '@pockethost/common' import { - SingletonBaseConfig, mkSingleton, + SingletonBaseConfig, } from '@pockethost/common/src/mkSingleton' import { map } from '@s-libs/micro-dash' import Docker, { Container, ContainerCreateOptions } from 'dockerode' @@ -23,6 +23,7 @@ import { dirname } from 'path' import { gte } from 'semver' import { AsyncReturnType } from 'type-fest' import { AsyncContext } from '../../util/AsyncContext' +import { InstanceLogger } from '../InstanceLoggerService' import { updaterService } from '../UpdaterService/UpdaterService' export type PocketbaseCommand = 'serve' | 'migrate' @@ -37,11 +38,7 @@ export type SpawnConfig = { env?: Env stdout?: MemoryStream stderr?: MemoryStream - onUnexpectedStop: ( - code: number | null, - stdout: string[], - stderr: string[], - ) => void + onUnexpectedStop: (code: number | null) => void } export type PocketbaseServiceApi = AsyncReturnType< typeof createPocketbaseService @@ -131,24 +128,23 @@ export const createPocketbaseService = async ( let isRunning = true const docker = new Docker() - const stdoutHistory: string[] = [] - const stderrHistory: string[] = [] + const iLogger = InstanceLogger(slug, 'exec') + iLogger.info(`Starting instance`) + const _stdoutData = (data: Buffer) => { const lines = data.toString().split(/\n/) lines.forEach((line) => { dbg(`${slug} stdout: ${line}`) + iLogger.info(line) }) - stdoutHistory.push(...lines) - while (stdoutHistory.length > 100) stdoutHistory.shift() } stdout.on('data', _stdoutData) const _stdErrData = (data: Buffer) => { const lines = data.toString().split(/\n/) lines.forEach((line) => { warn(`${slug} stderr: ${line}`) + iLogger.error(line) }) - stderrHistory.push(...lines) - while (stderrHistory.length > 100) stderrHistory.shift() } stderr.on('data', _stdErrData) const createOptions: ContainerCreateOptions = { @@ -164,16 +160,16 @@ export const createPocketbaseService = async ( }, Binds: [ `${dirname(binPath)}:/host_bin`, - `${DAEMON_PB_DATA_DIR}/${slug}:/host_data`, + `${mkInstanceDataPath(slug)}:/host_data`, `${ isMothership ? DAEMON_PB_MIGRATIONS_DIR - : `${DAEMON_PB_DATA_DIR}/${slug}/pb_migrations` + : mkInstanceDataPath(slug, `pb_migrations`) }:/host_data/pb_migrations`, `${ isMothership ? DAEMON_PB_HOOKS_DIR - : `${DAEMON_PB_DATA_DIR}/${slug}/pb_hooks` + : mkInstanceDataPath(slug, `pb_hooks`) }:/host_data/pb_hooks`, ], }, @@ -202,7 +198,7 @@ export const createPocketbaseService = async ( error(`Error: ${err.json.message}`) dbg(`${slug} stopped unexpectedly with code ${err}`, data) } - onUnexpectedStop?.(StatusCode, stdoutHistory, stderrHistory) + onUnexpectedStop?.(StatusCode) } resolveExit(0) }, diff --git a/packages/daemon/src/services/ProxyService.ts b/packages/daemon/src/services/ProxyService.ts index f791d171..5bd4e856 100644 --- a/packages/daemon/src/services/ProxyService.ts +++ b/packages/daemon/src/services/ProxyService.ts @@ -8,7 +8,7 @@ import { createServer, } from 'http' import { default as Server, default as httpProxy } from 'http-proxy' -import { AsyncReturnType, Asyncify } from 'type-fest' +import { AsyncReturnType, SetReturnType } from 'type-fest' import UrlPattern from 'url-pattern' export type ProxyServiceApi = AsyncReturnType @@ -23,7 +23,7 @@ export type ProxyMiddleware = ( host: string }, logger: Logger, -) => void | Promise +) => boolean | Promise export type ProxyServiceConfig = SingletonBaseConfig & { coreInternalUrl: string @@ -70,7 +70,9 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { ) for (let i = 0; i < middleware.length; i++) { const m = middleware[i]! - await m(req, res) + console.log(`Executing middleware`) + const handled = await m(req, res) + if (handled) break } } } catch (e) { @@ -98,7 +100,10 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { }) } - type MiddlewareListener = RequestListener | Asyncify + type MiddlewareListener = SetReturnType< + RequestListener, + boolean | Promise + > const middleware: MiddlewareListener[] = [] const use = ( @@ -139,7 +144,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { trace({ subdomainFilter, _urlFilters, host, url }) if (!_subdomainFilter(subdomain)) { trace(`Subdomain ${subdomain} does not match filter ${subdomainFilter}`) - return + return false } if ( !_urlFilters.find((u) => { @@ -153,7 +158,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => { }) ) { dbg(`${url} does not match pattern ${urlFilters}`) - return + return false } dbg(`${url} matches ${urlFilters}, sending to handler`) return handler( diff --git a/packages/daemon/src/services/RealtimeLog.ts b/packages/daemon/src/services/RealtimeLog.ts index 2f69d4d3..339752c2 100644 --- a/packages/daemon/src/services/RealtimeLog.ts +++ b/packages/daemon/src/services/RealtimeLog.ts @@ -1,15 +1,12 @@ -import { PUBLIC_MOTHERSHIP_NAME } from '$src/constants' import { InstanceFields, - RecordId, - SingletonBaseConfig, mkSingleton, + SingletonBaseConfig, } from '@pockethost/common' -import Bottleneck from 'bottleneck' import { text } from 'node:stream/consumers' import pocketbaseEs from 'pocketbase' import { JsonifiableObject } from 'type-fest/source/jsonifiable' -import { instanceLoggerService } from './InstanceLoggerService' +import { InstanceLogger } from './InstanceLoggerService' import { proxyService } from './ProxyService' export type RealtimeLogConfig = SingletonBaseConfig & {} @@ -25,29 +22,17 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { const { dbg, error } = _realtimeLogger ;(await proxyService()).use( - PUBLIC_MOTHERSHIP_NAME, + '*', '/logs', async (req, res, meta, logger) => { const { subdomain, host, coreInternalUrl } = meta if (!req.url?.startsWith('/logs')) { - return + return false } const _requestLogger = logger.create(`${subdomain}`) const { dbg, error, trace } = _requestLogger - const write = async (data: any) => { - return new Promise((resolve) => { - if (!res.write(data)) { - // dbg(`Waiting for drain after`, data) - res.once('drain', resolve) - } else { - // dbg(`Waiting for nexttick`, data) - process.nextTick(resolve) - } - }) - } - /** * Extract query params */ @@ -56,15 +41,12 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { // https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') - res.setHeader( - 'Access-Control-Allow-Headers', - 'authorization,content-type,cache-control', - ) + res.setHeader('Access-Control-Allow-Headers', '*') res.setHeader('Access-Control-Max-Age', 86400) if (req.method === 'OPTIONS') { res.statusCode = 204 res.end() - return + return true } // dbg(`Parsed URL is`, parsed) @@ -111,15 +93,10 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { } dbg(`Instance is `, instance) - const limiter = new Bottleneck({ maxConcurrent: 1 }) - /** * Get a database connection */ - const instanceLogger = await instanceLoggerService().get(instanceId, { - parentLogger: _requestLogger, - }) - const { subscribe } = instanceLogger + const instanceLogger = InstanceLogger(instanceId, `exec`) /** * Start the stream @@ -130,61 +107,15 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => { 'Cache-Control': 'no-store', }) - /** - * Track the IDs we send so we don't accidentally send old - * records in the initial burst (if one is requested) - */ - let _seenIds: { [_: RecordId]: boolean } | undefined = {} - - const unsub = await subscribe((e) => { - trace(`Caught db modification ${instanceId}`, e) - const { table, record } = e - const evt = mkEvent(`log`, record) - trace( - `Dispatching SSE log event from ${instance.subdomain} (${instance.id})`, - evt, - ) - limiter.schedule(() => write(evt)).catch(error) - }) - req.on('close', () => { - limiter.stop() - dbg( - `SSE request for ${instance.subdomain} (${instance.id}) closed. Unsubscribing.`, - ) - unsub() + const unsub = instanceLogger.tail(100, (line) => { + const obj = JSON.parse(line) + const evt = mkEvent(`log`, obj) + dbg(`****sending ${evt}`) + res.write(evt) }) - /** - * Send initial batch if requested - */ - if (nInitialRecords > 0) { - dbg(`Fetching initial ${nInitialRecords} logs to prime history`) - const recs = await instanceLogger.fetch(nInitialRecords) - recs - .sort((a, b) => (a.created < b.created ? -1 : 1)) - .forEach((rec) => { - limiter - .schedule(async () => { - if (_seenIds?.[rec.id]) { - trace(`Record ${rec.id} already sent `) - return - } // Skip if update already emitted - const evt = mkEvent(`log`, rec) - trace( - `Dispatching SSE initial log event from ${instance.subdomain} (${instance.id})`, - evt, - ) - return write(evt) - }) - .catch(error) - }) - limiter - .schedule(async () => { - // Set seenIds to `undefined` so the subscribe listener stops tracking them. - _seenIds = undefined - }) - .catch(error) - } + res.on('close', unsub) + return true }, `RealtimeLogService`, ) diff --git a/packages/daemon/src/services/clientService/PbClient.ts b/packages/daemon/src/services/clientService/PbClient.ts index a5e6109e..bf0ef73a 100644 --- a/packages/daemon/src/services/clientService/PbClient.ts +++ b/packages/daemon/src/services/clientService/PbClient.ts @@ -1,4 +1,4 @@ -import { DAEMON_PB_DATA_DIR, PUBLIC_MOTHERSHIP_NAME } from '$constants' +import { mkInstanceDataPath, PUBLIC_MOTHERSHIP_NAME } from '$constants' import { Logger, safeCatch } from '@pockethost/common' import { Knex } from 'knex' import { default as PocketBase, default as pocketbaseEs } from 'pocketbase' @@ -17,7 +17,7 @@ export const createPbClient = (url: string, logger: Logger) => { info(`Initializing client: ${url}`) const rawDb = createRawPbClient( - `${DAEMON_PB_DATA_DIR}/${PUBLIC_MOTHERSHIP_NAME}/pb_data/data.db`, + mkInstanceDataPath(PUBLIC_MOTHERSHIP_NAME, `pb_data`, `data.db`), _clientLogger, ) diff --git a/packages/dashboard/src/components/helpers/UserLoggedIn.svelte b/packages/dashboard/src/components/helpers/UserLoggedIn.svelte index eb864e53..12925066 100644 --- a/packages/dashboard/src/components/helpers/UserLoggedIn.svelte +++ b/packages/dashboard/src/components/helpers/UserLoggedIn.svelte @@ -1,6 +1,13 @@ diff --git a/packages/dashboard/src/pocketbase/PocketbaseClient.ts b/packages/dashboard/src/pocketbase/PocketbaseClient.ts index e4422cd8..3c361df1 100644 --- a/packages/dashboard/src/pocketbase/PocketbaseClient.ts +++ b/packages/dashboard/src/pocketbase/PocketbaseClient.ts @@ -1,3 +1,4 @@ +import { INSTANCE_URL } from '$src/env' import { createGenericSyncEvent } from '$util/events' import { fetchEventSource } from '@microsoft/fetch-event-source' import { @@ -286,8 +287,9 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => { const controller = new AbortController() const signal = controller.signal const continuallyFetchFromEventSource = () => { + const url = INSTANCE_URL(instanceId, `logs`) dbg(`Subscribing to ${url}`) - fetchEventSource(`${url}/logs`, { + fetchEventSource(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -298,10 +300,10 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => { auth, }), onmessage: (event) => { - trace(`Got stream event`, event) + dbg(`Got stream event`, event) const {} = event const log = JSON.parse(event.data) as InstanceLogFields - trace(`Log is`, log) + dbg(`Log is`, log) update(log) }, onopen: async (response) => { diff --git a/packages/dashboard/src/routes/app/+layout.svelte b/packages/dashboard/src/routes/app/+layout.svelte new file mode 100644 index 00000000..29192b90 --- /dev/null +++ b/packages/dashboard/src/routes/app/+layout.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/dashboard/src/routes/app/instances/[instanceId]/Logging.svelte b/packages/dashboard/src/routes/app/instances/[instanceId]/Logging.svelte index 05cafec2..ee1147cc 100644 --- a/packages/dashboard/src/routes/app/instances/[instanceId]/Logging.svelte +++ b/packages/dashboard/src/routes/app/instances/[instanceId]/Logging.svelte @@ -5,11 +5,10 @@ import { mkCleanup } from '$util/componentCleanup' import { LoggerService, + StreamNames, Unsubscribe, type InstanceLogFields, - type RecordId, } from '@pockethost/common' - import { values } from '@s-libs/micro-dash' import { onMount } from 'svelte' import { derived, writable } from 'svelte/store' import { instance } from './store' @@ -19,10 +18,9 @@ $: ({ id } = $instance) // This takes in a log type and returns a specific text color - const logColor = (type: string) => { - if (type === 'system') return 'text-success' - if (type === 'info') return 'text-info' - if (type === 'error') return 'text-error' + const logColor = (type: StreamNames) => { + if (type === StreamNames.StdOut) return 'text-info' + if (type === StreamNames.StdErr) return 'text-error' return 'text-info' } @@ -44,8 +42,7 @@ modal?.showModal() } - const logs = writable<{ [_: RecordId]: InstanceLogFields }>({}) - let logsArray: InstanceLogFields[] = [] + const logs = writable([]) const onDestroy = mkCleanup() @@ -56,18 +53,13 @@ const unsub = instanceId.subscribe((id) => { dbg(`Watching instance log ${id}`) unwatch?.() - logs.set({}) + logs.set([]) unwatch = client().watchInstanceLog(id, (newLog) => { trace(`Got new log`, newLog) logs.update((currentLogs) => { - return { ...currentLogs, [newLog.id]: newLog } + return [...currentLogs, newLog] }) - - logsArray = values($logs) - .sort((a, b) => (a.created > b.created ? 1 : -1)) - .slice(0, 1000) - .reverse() }) }) onDestroy(unsub) @@ -88,7 +80,7 @@

Instance Logging

- {#each logsArray as log} + {#each $logs as log}
- {log.created} + {log.time} {log.stream} @@ -121,7 +113,7 @@ >Fullscreen
- {#each logsArray as log} + {#each $logs as log}
- {log.created} + {log.time} {log.stream} diff --git a/yarn.lock b/yarn.lock index 5b3f86aa..fed1caaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,6 +181,20 @@ resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== +"@colors/colors@1.6.0", "@colors/colors@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" + integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@dansvel/vite-plugin-markdown@^2.0.5": version "2.0.5" resolved "https://registry.yarnpkg.com/@dansvel/vite-plugin-markdown/-/vite-plugin-markdown-2.0.5.tgz#55cff46adb457cb654b84a424aef2e25ca635926" @@ -834,6 +848,11 @@ resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.3.tgz#908bfb113419fd6a42273674c00994d40902c165" integrity sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA== +"@types/triple-beam@^1.3.2": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.4.tgz#a1d5f480245db86e2f4777000065d4fe7467a012" + integrity sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA== + "@types/unzipper@^0.10.5": version "0.10.5" resolved "https://registry.yarnpkg.com/@types/unzipper/-/unzipper-0.10.5.tgz#36a963cf025162b4ac31642590cb4192971d633b" @@ -1690,7 +1709,7 @@ code-red@^1.0.3: estree-walker "^3.0.3" periscopic "^3.1.0" -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1714,7 +1733,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.0: +color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -1727,6 +1746,14 @@ color-support@^1.1.2, color-support@^1.1.3: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + color@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" @@ -1745,6 +1772,14 @@ colorette@2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2353,6 +2388,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2760,6 +2800,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fecha@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" + integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw== + fetch-blob@^3.1.2, fetch-blob@^3.1.4: version "3.2.0" resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" @@ -2897,6 +2942,11 @@ flatted@^3.2.7: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + follow-redirects@^1.0.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -3951,6 +4001,11 @@ knex@^2.3.0: tarn "^3.0.2" tildify "2.0.0" +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + latest-version@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" @@ -4075,6 +4130,18 @@ log-symbols@^5.1.0: chalk "^5.0.0" is-unicode-supported "^1.1.0" +logform@^2.3.2, logform@^2.4.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.0.tgz#8c82a983f05d6eaeb2d75e3decae7a768b2bf9b5" + integrity sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ== + dependencies: + "@colors/colors" "1.6.0" + "@types/triple-beam" "^1.3.2" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + lower-case-first@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" @@ -4417,7 +4484,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4676,6 +4743,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -5640,6 +5714,11 @@ safe-json-stringify@~1: resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== +safe-stable-stringify@^2.3.1: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5973,6 +6052,11 @@ ssri@^8.0.0, ssri@^8.0.1: dependencies: minipass "^3.1.1" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -6324,6 +6408,11 @@ tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -6437,6 +6526,11 @@ trim-repeated@^2.0.0: dependencies: escape-string-regexp "^5.0.0" +triple-beam@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" + integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" @@ -6766,6 +6860,32 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" +winston-transport@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.6.0.tgz#f1c1a665ad1b366df72199e27892721832a19e1b" + integrity sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.11.0.tgz#2d50b0a695a2758bb1c95279f0a88e858163ed91" + integrity sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g== + dependencies: + "@colors/colors" "^1.6.0" + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + with@^7.0.0: version "7.0.2" resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"