mirror of
https://github.com/pockethost/pockethost.git
synced 2025-05-28 09:46:46 +00:00
enh: winston logging
This commit is contained in:
parent
1b749a621b
commit
6a639a3021
@ -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
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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}`
|
||||
|
@ -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({
|
||||
|
@ -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`,
|
||||
)
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { Logger } from '@pockethost/common'
|
||||
|
||||
export type DaemonContext = {
|
||||
parentLogger: Logger
|
||||
}
|
@ -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<typeof createSqliteLogger>
|
||||
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<InstanceLogFields>) => void) => {
|
||||
let _seenIds: { [_: RecordId]: boolean } | undefined = {}
|
||||
|
||||
const unsub = db.subscribe<InstanceLogFields>((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<InstanceLogFields[]>(
|
||||
`select * from logs order by created desc limit ${limit}`,
|
||||
)
|
||||
}
|
||||
|
||||
return { write, subscribe, fetch }
|
||||
}
|
@ -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();
|
||||
|
@ -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";
|
@ -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`,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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)
|
||||
},
|
||||
|
@ -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<typeof proxyService>
|
||||
@ -23,7 +23,7 @@ export type ProxyMiddleware = (
|
||||
host: string
|
||||
},
|
||||
logger: Logger,
|
||||
) => void | Promise<void>
|
||||
) => boolean | Promise<boolean>
|
||||
|
||||
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<RequestListener>
|
||||
type MiddlewareListener = SetReturnType<
|
||||
RequestListener,
|
||||
boolean | Promise<boolean>
|
||||
>
|
||||
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(
|
||||
|
@ -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<void>((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`,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { isUserLoggedIn } from '$util/stores'
|
||||
import { isAuthStateInitialized, isUserLoggedIn } from '$util/stores'
|
||||
import AuthStateGuard from './AuthStateGuard.svelte'
|
||||
|
||||
export let redirect = false
|
||||
$: {
|
||||
if ($isAuthStateInitialized && redirect && !$isUserLoggedIn) {
|
||||
window.location.href = '/'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AuthStateGuard>
|
||||
|
@ -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) => {
|
||||
|
7
packages/dashboard/src/routes/app/+layout.svelte
Normal file
7
packages/dashboard/src/routes/app/+layout.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import UserLoggedIn from '$components/helpers/UserLoggedIn.svelte'
|
||||
</script>
|
||||
|
||||
<UserLoggedIn redirect>
|
||||
<slot />
|
||||
</UserLoggedIn>
|
@ -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<InstanceLogFields[]>([])
|
||||
|
||||
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 @@
|
||||
<h3 class="font-bold text-lg">Instance Logging</h3>
|
||||
|
||||
<div class="py-4 h-[80vh] overflow-y-scroll flex flex-col-reverse gap-3">
|
||||
{#each logsArray as log}
|
||||
{#each $logs as log}
|
||||
<div
|
||||
class="px-4 text-[11px] font-mono flex align-center"
|
||||
data-prefix=">"
|
||||
@ -98,7 +90,7 @@
|
||||
>
|
||||
|
||||
<div>
|
||||
<span class="mr-1 text-accent">{log.created}</span>
|
||||
<span class="mr-1 text-accent">{log.time}</span>
|
||||
<span class={`mr-1 font-bold ${logColor(log.stream)}`}
|
||||
>{log.stream}</span
|
||||
>
|
||||
@ -121,7 +113,7 @@
|
||||
>Fullscreen <i class="fa-regular fa-arrows-maximize"></i></button
|
||||
>
|
||||
<div class="h-[450px] flex flex-col-reverse overflow-y-scroll gap-3">
|
||||
{#each logsArray as log}
|
||||
{#each $logs as log}
|
||||
<div
|
||||
class="px-4 text-[11px] font-mono flex align-center"
|
||||
data-prefix=">"
|
||||
@ -131,7 +123,7 @@
|
||||
>
|
||||
|
||||
<div>
|
||||
<span class="mr-1 text-accent">{log.created}</span>
|
||||
<span class="mr-1 text-accent">{log.time}</span>
|
||||
<span class={`mr-1 font-bold ${logColor(log.stream)}`}
|
||||
>{log.stream}</span
|
||||
>
|
||||
|
126
yarn.lock
126
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user