chore: log context update

This commit is contained in:
Ben Allfree 2023-06-14 11:52:07 -07:00
parent ac5948be40
commit 56aff5db0a
28 changed files with 449 additions and 324 deletions

View File

@ -21,6 +21,7 @@ export const createWatchHelper = (config: WatchHelperConfig) => {
const watchById = safeCatch(
`watchById`,
logger(),
async <TRec>(
collectionName: string,
id: RecordId,
@ -93,6 +94,7 @@ export const createWatchHelper = (config: WatchHelperConfig) => {
const watchAllById = safeCatch(
`watchAllById`,
logger(),
async <TRec extends BaseFields>(
collectionName: string,
idName: keyof TRec,

View File

@ -1,9 +1,10 @@
import { ClientResponseError } from 'pocketbase'
import { logger } from './Logger'
import { Logger } from './Logger'
let c = 0
export const safeCatch = <TIn extends any[], TOut>(
name: string,
logger: Logger,
cb: (...args: TIn) => Promise<TOut>,
timeoutMs = 5000
) => {
@ -11,7 +12,7 @@ export const safeCatch = <TIn extends any[], TOut>(
const _c = c++
const uuid = `${name}:${_c}`
const pfx = `safeCatch:${uuid}`
const { raw, error, warn, dbg } = logger().create(pfx)
const { raw, error, warn, dbg } = logger.create(pfx)
raw(`args`, args)
const tid = setTimeout(() => {
error(`timeout ${timeoutMs}ms waiting for ${pfx}`)

View File

@ -16,17 +16,18 @@ import {
rpcService,
sqliteService,
} from '$services'
import { logger } from '@pockethost/common'
import { logger as loggerService } from '@pockethost/common'
import { exec } from 'child_process'
import { centralDbService } from './services/CentralDbService'
import { instanceLoggerService } from './services/InstanceLoggerService'
logger({ debug: DEBUG, trace: TRACE, errorTrace: !DEBUG })
loggerService({ debug: DEBUG, trace: TRACE, errorTrace: !DEBUG })
// npm install eventsource --save
global.EventSource = require('eventsource')
;(async () => {
const { dbg, error, info, warn } = logger().create(`server.ts`)
const logger = loggerService().create(`server.ts`)
const { dbg, error, info, warn } = logger
info(`Starting`)
/**
@ -45,6 +46,7 @@ global.EventSource = require('eventsource')
const pbService = await pocketbase({
cachePath: PH_BIN_CACHE,
checkIntervalMs: 1000 * 5 * 60,
logger,
})
/**
@ -73,18 +75,18 @@ global.EventSource = require('eventsource')
/**
* Launch services
*/
await clientService(url)
ftpService({})
await rpcService({})
await clientService({ url, logger })
ftpService({ logger })
await rpcService({ logger })
await proxyService({
logger,
coreInternalUrl: url,
})
await instanceLoggerService({})
await sqliteService({})
await realtimeLog({})
await instanceService({})
await centralDbService({})
await backupService({})
await instanceLoggerService({ logger })
await sqliteService({ logger })
await realtimeLog({ logger })
await instanceService({ logger })
await centralDbService({ logger })
info(`Hooking into process exit event`)

View File

@ -1,14 +1,19 @@
import { PUBLIC_APP_DB } from '$constants'
import { logger, mkSingleton } from '@pockethost/common'
import { mkSingleton, SingletonBaseConfig } from '@pockethost/common'
import { proxyService } from './ProxyService'
export const centralDbService = mkSingleton(async () => {
const { dbg } = logger().create(`centralDbService`)
export type CentralDbServiceConfig = SingletonBaseConfig
export const centralDbService = mkSingleton(
async (config: CentralDbServiceConfig) => {
const { logger } = config
const { dbg } = logger.create(`centralDbService`)
;(await proxyService()).use(
PUBLIC_APP_DB,
['/api(/*)', '/_(/*)', '/'],
(req, res, meta) => {
(req, res, meta, logger) => {
const { dbg } = logger
const { subdomain, coreInternalUrl, proxy } = meta
if (subdomain !== PUBLIC_APP_DB) return
@ -25,4 +30,5 @@ export const centralDbService = mkSingleton(async () => {
return {
shutdown() {},
}
})
}
)

View File

@ -7,12 +7,12 @@ import {
SSL_KEY,
} from '$constants'
import { clientService, createPbClient } from '$services'
import { logger, mkSingleton } from '@pockethost/common'
import { mkSingleton, SingletonBaseConfig } from '@pockethost/common'
import { readFileSync } from 'fs'
import { FtpSrv } from 'ftp-srv'
import { PhFs } from './PhFs'
export type FtpConfig = {}
export type FtpConfig = SingletonBaseConfig & {}
export enum FolderNames {
PbData = 'pb_data',
@ -49,13 +49,14 @@ const tls = {
}
export const ftpService = mkSingleton((config: FtpConfig) => {
const log = logger().create('FtpService')
const { dbg, info } = log
const { logger } = config
const _ftpServiceLogger = logger.create('FtpService')
const { dbg, info } = _ftpServiceLogger
const ftpServer = new FtpSrv({
url: 'ftp://0.0.0.0:' + PH_FTP_PORT,
anonymous: false,
log: log.create(`ftpServer`, { errorTrace: false }),
log: _ftpServiceLogger.create(`ftpServer`, { errorTrace: false }),
tls,
pasv_url: PH_FTP_PASV_IP,
pasv_max: PH_FTP_PASV_PORT_MAX,
@ -66,13 +67,13 @@ export const ftpService = mkSingleton((config: FtpConfig) => {
'login',
async ({ connection, username, password }, resolve, reject) => {
const url = (await clientService()).client.url
const client = createPbClient(url)
const client = createPbClient(url, _ftpServiceLogger)
try {
await client.client
.collection('users')
.authWithPassword(username, password)
dbg(`Logged in`)
const fs = new PhFs(connection, client)
const fs = new PhFs(connection, client, _ftpServiceLogger)
resolve({ fs })
} catch (e) {
reject(new Error(`Invalid username or password`))

View File

@ -1,5 +1,5 @@
import { DAEMON_PB_DATA_DIR } from '$constants'
import { Logger, logger } from '@pockethost/common'
import { Logger } from '@pockethost/common'
import { existsSync, mkdirSync } from 'fs'
import { FileStat, FileSystem, FtpConnection } from 'ftp-srv'
import { join } from 'path'
@ -16,10 +16,14 @@ export class PhFs extends FileSystem {
private log: Logger
private client: PocketbaseClientApi
constructor(connection: FtpConnection, client: PocketbaseClientApi) {
constructor(
connection: FtpConnection,
client: PocketbaseClientApi,
logger: Logger
) {
super(connection, { root: '/', cwd: '/' })
this.client = client
this.log = logger().create(`PhFs`)
this.log = logger.create(`PhFs`)
}
async chdir(path?: string | undefined): Promise<string> {

View File

@ -0,0 +1,5 @@
import { Logger } from '@pockethost/common'
export type DaemonContext = {
parentLogger: Logger
}

View File

@ -0,0 +1,74 @@
import { SqliteChangeEvent, sqliteService } from '$services'
import {
InstanceLogFields,
InstanceLogFields_Create,
newId,
pocketNow,
RecordId,
safeCatch,
StreamNames,
} 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 } = _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()
dbg(`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 }
}

View File

@ -1,85 +1,30 @@
import { DAEMON_PB_DATA_DIR } from '$constants'
import { SqliteChangeEvent, sqliteService } from '$services'
import { sqliteService } from '$services'
import {
InstanceId,
InstanceLogFields,
InstanceLogFields_Create,
logger,
mkSingleton,
newId,
pocketNow,
RecordId,
safeCatch,
SingletonBaseConfig,
StreamNames,
} from '@pockethost/common'
import { mkdirSync } from 'fs'
import knex from 'knex'
import { dirname, join } from 'path'
import { AsyncReturnType } from 'type-fest'
export type InstanceLogger = AsyncReturnType<typeof mkApi>
const mkApi = async (logDbPath: string) => {
const { dbg } = logger().create(`InstanceLogger ${logDbPath}`)
const { getDatabase } = sqliteService()
const db = await getDatabase(logDbPath)
const conn = knex({
client: 'sqlite3',
connection: {
filename: logDbPath,
},
useNullAsDefault: true,
})
const write = safeCatch(
`workerLogger:write`,
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()
dbg(`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 }
}
import { DaemonContext } from './DaemonContext'
import { createSqliteLogger, SqliteLogger } from './SqliteLogger'
const instances: {
[instanceId: InstanceId]: Promise<InstanceLogger>
[instanceId: InstanceId]: Promise<SqliteLogger>
} = {}
export const createInstanceLogger = (instanceId: InstanceId) => {
export const createInstanceLogger = (
instanceId: InstanceId,
context: DaemonContext
) => {
const { parentLogger } = context
if (!instances[instanceId]) {
instances[instanceId] = new Promise<InstanceLogger>(async (resolve) => {
const { dbg } = logger().create(`WorkerLogger:${instanceId}`)
instances[instanceId] = new Promise<SqliteLogger>(async (resolve) => {
const _instanceLogger = parentLogger.create(`InstanceLogger`)
const { dbg } = _instanceLogger
const logDbPath = join(
DAEMON_PB_DATA_DIR,
@ -98,7 +43,9 @@ export const createInstanceLogger = (instanceId: InstanceId) => {
migrationsPath: join(__dirname, 'migrations'),
})
const api = await mkApi(logDbPath)
const api = await createSqliteLogger(logDbPath, {
parentLogger: _instanceLogger,
})
await api.write(`Ran migrations`, StreamNames.System)
resolve(api)
})
@ -107,8 +54,12 @@ export const createInstanceLogger = (instanceId: InstanceId) => {
return instances[instanceId]!
}
export const instanceLoggerService = mkSingleton(() => {
const { dbg } = logger().create(`InstanceLoggerService`)
export type InstanceLoggerServiceConfig = SingletonBaseConfig
export const instanceLoggerService = mkSingleton(
(config: InstanceLoggerServiceConfig) => {
const { logger } = config
const { dbg } = logger.create(`InstanceLoggerService`)
dbg(`Starting up`)
return {
get: createInstanceLogger,
@ -116,4 +67,5 @@ export const instanceLoggerService = mkSingleton(() => {
dbg(`Shutting down`)
},
}
})
}
)

View File

@ -1,5 +1,5 @@
import { DAEMON_PB_DATA_DIR, DENO_PATH } from '$constants'
import { InstanceFields, logger, StreamNames } from '@pockethost/common'
import { InstanceFields, Logger, StreamNames } from '@pockethost/common'
import { keys } from '@s-libs/micro-dash'
import { spawn } from 'child_process'
import { join } from 'path'
@ -11,12 +11,15 @@ export type DenoProcessConfig = {
path: string
port: number
instance: InstanceFields
logger: Logger
}
export type DenoApi = AsyncReturnType<typeof createDenoProcess>
export const createDenoProcess = async (config: DenoProcessConfig) => {
const { dbg, error } = logger().create(`DenoProcess.ts`)
const { logger } = config
const _denoLogger = logger.create(`DenoProcess.ts`)
const { dbg, error } = _denoLogger
const { instance, port, path } = config
const internalUrl = mkInternalUrl(port)
@ -34,7 +37,9 @@ export const createDenoProcess = async (config: DenoProcessConfig) => {
path,
]
const denoLogger = await instanceLoggerService().get(instance.id)
const denoLogger = await instanceLoggerService().get(instance.id, {
parentLogger: _denoLogger,
})
const denoWrite = (
message: string,

View File

@ -16,13 +16,13 @@ import {
createTimerManager,
InstanceId,
InstanceStatus,
logger,
mkSingleton,
RpcCommands,
safeCatch,
SaveSecretsPayload,
SaveSecretsPayloadSchema,
SaveSecretsResult,
SingletonBaseConfig,
} from '@pockethost/common'
import { forEachRight, map } from '@s-libs/micro-dash'
import Bottleneck from 'bottleneck'
@ -42,12 +42,14 @@ type InstanceApi = {
startRequest: () => () => void
}
export type InstanceServiceConfig = {}
export type InstanceServiceConfig = SingletonBaseConfig & {}
export type InstanceServiceApi = AsyncReturnType<typeof instanceService>
export const instanceService = mkSingleton(
async (config: InstanceServiceConfig) => {
const { dbg, raw, error, warn } = logger().create('InstanceService')
const { logger } = config
const _instanceLogger = logger.create('InstanceService')
const { dbg, raw, error, warn } = _instanceLogger
const { client } = await clientService()
const { registerCommand } = await rpcService()
@ -91,9 +93,8 @@ export const instanceService = mkSingleton(
const getInstance = (subdomain: string) =>
instanceLimiter
.schedule(async () => {
const { dbg, warn, error } = logger().create(
`InstanceService ${subdomain}`
)
const _subdomainLogger = _instanceLogger.create(subdomain)
const { dbg, warn, error } = _subdomainLogger
dbg(`Getting instance`)
{
const instance = instances[subdomain]
@ -112,6 +113,7 @@ export const instanceService = mkSingleton(
)
}
dbg(`Instance found`)
_subdomainLogger.breadcrumb(instance.id)
dbg(`Checking for verified account`)
if (!owner?.verified) {
@ -132,9 +134,13 @@ export const instanceService = mkSingleton(
error(`Failed to get port for ${subdomain}`)
throw e
})
_subdomainLogger.breadcrumb(newPort.toString())
dbg(`Found port: ${newPort}`)
const instanceLogger = await instanceLoggerService().get(instance.id)
const instanceLogger = await instanceLoggerService().get(
instance.id,
{ parentLogger: _subdomainLogger }
)
await clientLimiter.schedule(() => {
dbg(`Instance status: starting`)
@ -146,7 +152,9 @@ export const instanceService = mkSingleton(
dbg(`Starting instance`)
await instanceLogger.write(`Starting instance`)
const childProcess = await pbService.spawn({
const childProcess = await (async () => {
try {
const cp = await pbService.spawn({
command: 'serve',
slug: instance.id,
port: newPort,
@ -156,6 +164,13 @@ export const instanceService = mkSingleton(
api.shutdown()
},
})
return cp
} catch (e) {
throw new Error(
`Could not launch PocketBase ${instance.version}. It may be time to upgrade.`
)
}
})()
const { pid } = childProcess
assertTruthy(pid, `Expected PID here but got ${pid}`)
@ -190,6 +205,7 @@ export const instanceService = mkSingleton(
path: workerPath,
port: newPort,
instance,
logger: _instanceLogger,
})
return api
} else {
@ -210,6 +226,7 @@ export const instanceService = mkSingleton(
port: newPort,
shutdown: safeCatch(
`Instance ${subdomain} invocation ${invocation.id} pid ${pid} shutdown`,
_subdomainLogger,
async () => {
dbg(`Shutting down`)
await instanceLogger.write(`Shutting down instance`)
@ -247,7 +264,7 @@ export const instanceService = mkSingleton(
{
tm.repeat(
safeCatch(`idleCheck`, async () => {
safeCatch(`idleCheck`, _subdomainLogger, async () => {
raw(
`${subdomain} idle check: ${openRequestCount} open requests`
)
@ -272,7 +289,7 @@ export const instanceService = mkSingleton(
}
{
const uptime = safeCatch(`uptime`, async () => {
const uptime = safeCatch(`uptime`, _subdomainLogger, async () => {
raw(`${subdomain} uptime`)
await clientLimiter.schedule(() =>
client.pingInvocation(invocation)
@ -314,7 +331,7 @@ export const instanceService = mkSingleton(
;(await proxyService()).use(
(subdomain) => subdomain !== PUBLIC_APP_DB,
['/api(/*)', '/_(/*)', '(/*)'],
async (req, res, meta) => {
async (req, res, meta, logger) => {
const { subdomain, host, proxy } = meta
// Do not handle central db requests, that is handled separately

View File

@ -9,10 +9,12 @@ import {
import {
createCleanupManager,
createTimerManager,
logger,
safeCatch,
} from '@pockethost/common'
import { mkSingleton } from '@pockethost/common/src/mkSingleton'
import {
mkSingleton,
SingletonBaseConfig,
} from '@pockethost/common/src/mkSingleton'
import { keys } from '@s-libs/micro-dash'
import { spawn } from 'child_process'
import { chmodSync, existsSync } from 'fs'
@ -35,7 +37,7 @@ export type PocketbaseServiceApi = AsyncReturnType<
typeof createPocketbaseService
>
export type PocketbaseServiceConfig = {
export type PocketbaseServiceConfig = SingletonBaseConfig & {
cachePath: string
checkIntervalMs: number
}
@ -60,7 +62,9 @@ export type Releases = Release[]
export const createPocketbaseService = async (
config: PocketbaseServiceConfig
) => {
const { dbg, error } = logger().create('PocketbaseService')
const { logger } = config
const _serviceLogger = logger.create('PocketbaseService')
const { dbg, error } = _serviceLogger
const { cachePath, checkIntervalMs } = config
@ -100,7 +104,7 @@ export const createPocketbaseService = async (
versions[sanitizedTagName] = Promise.resolve('')
return
}
await downloadAndExtract(url, binPath)
await downloadAndExtract(url, binPath, _serviceLogger)
resolve(binPath)
})
@ -138,7 +142,10 @@ export const createPocketbaseService = async (
}
}
const _spawn = safeCatch(`spawnInstance`, async (cfg: SpawnConfig) => {
const _spawn = safeCatch(
`spawnInstance`,
_serviceLogger,
async (cfg: SpawnConfig) => {
const _cfg: Required<SpawnConfig> = {
version: maxVersion,
port: await getPort(),
@ -207,7 +214,7 @@ export const createPocketbaseService = async (
const url = mkInternalUrl(port)
if (command === 'serve') {
await tryFetch(url, async () => isRunning)
await tryFetch(_serviceLogger)(url, async () => isRunning)
}
const api: PocketbaseProcess = {
url,
@ -216,7 +223,8 @@ export const createPocketbaseService = async (
kill: () => ls.kill(),
}
return api
})
}
)
const shutdown = () => {
dbg(`Shutting down pocketbaseService`)

View File

@ -1,5 +1,5 @@
import { PUBLIC_APP_DOMAIN } from '$constants'
import { logger, mkSingleton } from '@pockethost/common'
import { Logger, mkSingleton, SingletonBaseConfig } from '@pockethost/common'
import { isFunction } from '@s-libs/micro-dash'
import {
createServer,
@ -21,14 +21,17 @@ export type ProxyMiddleware = (
coreInternalUrl: string
proxy: Server
host: string
}
},
logger: Logger
) => void | Promise<void>
export type ProxyServiceConfig = {
export type ProxyServiceConfig = SingletonBaseConfig & {
coreInternalUrl: string
}
export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
const { dbg, error, info, trace, warn } = logger().create('ProxyService')
const { logger } = config
const _proxyLogger = logger.create('ProxyService')
const { dbg, error, info, trace, warn } = _proxyLogger
const { coreInternalUrl } = config
@ -38,7 +41,7 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
})
const server = createServer(async (req, res) => {
dbg(`Incoming request ${req.headers.host}/${req.url}`)
dbg(`Incoming request ${req.method} ${req.headers.host}/${req.url}`)
if (!req.headers.host?.endsWith(PUBLIC_APP_DOMAIN)) {
warn(
`Request for ${req.headers.host} rejected because host does not end in ${PUBLIC_APP_DOMAIN}`
@ -89,7 +92,8 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
handler: ProxyMiddleware,
handlerName: string
) => {
const { dbg, trace } = logger().create(`ProxyService:${handlerName}`)
const _handlerLogger = _proxyLogger.create(`${handlerName}`)
const { dbg, trace } = _handlerLogger
dbg({ subdomainFilter, urlFilters })
const _urlFilters = Array.isArray(urlFilters)
@ -105,6 +109,10 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
if (!host) {
throw new Error(`Host not found`)
}
const _requestLogger = _handlerLogger.create(host)
const { dbg, trace } = _requestLogger
_requestLogger.breadcrumb(req.method)
_requestLogger.breadcrumb(req.url)
const [subdomain, ...junk] = host.split('.')
if (!subdomain) {
throw new Error(`${host} has no subdomain.`)
@ -133,7 +141,12 @@ export const proxyService = mkSingleton(async (config: ProxyServiceConfig) => {
return
}
dbg(`${url} matches ${urlFilters}, sending to handler`)
return handler(req, res, { host, subdomain, coreInternalUrl, proxy })
return handler(
req,
res,
{ host, subdomain, coreInternalUrl, proxy },
_requestLogger
)
})
}

View File

@ -1,9 +1,9 @@
import { PUBLIC_APP_DB } from '$src/constants'
import {
InstanceFields,
logger,
mkSingleton,
RecordId,
SingletonBaseConfig,
} from '@pockethost/common'
import Bottleneck from 'bottleneck'
import { text } from 'node:stream/consumers'
@ -12,7 +12,7 @@ import { JsonifiableObject } from 'type-fest/source/jsonifiable'
import { instanceLoggerService } from './InstanceLoggerService'
import { proxyService } from './ProxyService'
export type RealtimeLogConfig = {}
export type RealtimeLogConfig = SingletonBaseConfig & {}
const mkEvent = (name: string, data: JsonifiableObject) => {
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`
@ -20,20 +20,21 @@ const mkEvent = (name: string, data: JsonifiableObject) => {
export type RealtimeLog = ReturnType<typeof realtimeLog>
export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
const { dbg, error } = logger().create(`RealtimeLog.ts`)
const { logger } = config
const _realtimeLogger = logger.create(`RealtimeLog`)
const { dbg, error } = _realtimeLogger
;(await proxyService()).use(
PUBLIC_APP_DB,
'/logs',
async (req, res, meta) => {
async (req, res, meta, logger) => {
const { subdomain, host, coreInternalUrl } = meta
if (!req.url?.startsWith('/logs')) {
return
}
const { dbg, error, trace } = logger().create(
`RealtimeLog:${subdomain}:${host}`
)
const _requestLogger = logger.create(`${subdomain}`)
const { dbg, error, trace } = _requestLogger
const write = async (data: any) => {
return new Promise<void>((resolve) => {
@ -115,7 +116,9 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
/**
* Get a database connection
*/
const instanceLogger = await instanceLoggerService().get(instanceId)
const instanceLogger = await instanceLoggerService().get(instanceId, {
parentLogger: _requestLogger,
})
const { subscribe } = instanceLogger
/**
@ -182,8 +185,6 @@ export const realtimeLog = mkSingleton(async (config: RealtimeLogConfig) => {
})
.catch(error)
}
return true
},
`RealtimeLogService`
)

View File

@ -1,12 +1,12 @@
import { clientService } from '$services'
import {
assertTruthy,
logger,
mkSingleton,
RpcCommands,
RpcFields,
RpcStatus,
RPC_COMMANDS,
SingletonBaseConfig,
} from '@pockethost/common'
import { isObject } from '@s-libs/micro-dash'
import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv'
@ -29,10 +29,11 @@ export type RpcRunner<
TResult extends JsonObject
> = (job: RpcFields<TPayload, TResult>) => Promise<TResult>
export type RpcServiceConfig = {}
export type RpcServiceConfig = SingletonBaseConfig & {}
export const rpcService = mkSingleton(async (config: RpcServiceConfig) => {
const { dbg, error } = logger().create('RpcService')
const { logger } = config
const { dbg, error } = logger.create('RpcService')
const { client } = await clientService()
const limiter = new Bottleneck({ maxConcurrent: 1 })

View File

@ -1,8 +1,8 @@
import {
createCleanupManager,
createEvent,
logger,
mkSingleton,
SingletonBaseConfig,
} from '@pockethost/common'
import Bottleneck from 'bottleneck'
import { Database as SqliteDatabase, open } from 'sqlite'
@ -28,12 +28,13 @@ export type SqliteServiceApi = {
cb: SqliteChangeHandler<TRecord>
) => SqliteUnsubscribe
}
export type SqliteServiceConfig = {}
export type SqliteServiceConfig = SingletonBaseConfig & {}
export type SqliteService = ReturnType<typeof sqliteService>
export const sqliteService = mkSingleton((config: SqliteServiceConfig) => {
const { dbg, trace } = logger().create(`sqliteService`)
const { logger } = config
const { dbg, trace } = logger.create(`sqliteService`)
const connections: { [_: string]: Promise<SqliteServiceApi> } = {}
const cm = createCleanupManager()
@ -41,7 +42,8 @@ export const sqliteService = mkSingleton((config: SqliteServiceConfig) => {
const limiter = new Bottleneck({ maxConcurrent: 1 })
const getDatabase = async (filename: string): Promise<SqliteServiceApi> => {
const { dbg } = logger().create(`sqliteService:${filename}`)
const _dbLogger = logger.create(`${filename}`)
const { dbg } = _dbLogger
trace(`Fetching database for ${filename}`, connections)
if (!connections[filename]) {

View File

@ -4,7 +4,6 @@ import {
InstanceFields_Create,
InstanceId,
InstanceStatus,
logger,
safeCatch,
UserFields,
} from '@pockethost/common'
@ -16,12 +15,14 @@ import { MixinContext } from './PbClient'
export type InstanceApi = ReturnType<typeof createInstanceMixin>
export const createInstanceMixin = (context: MixinContext) => {
const { dbg, raw } = logger().create('InstanceMixin')
const { logger } = context
const { dbg, raw } = logger.create('InstanceMixin')
const { client, rawDb } = context
const createInstance = safeCatch(
`createInstance`,
logger,
(payload: InstanceFields_Create): Promise<InstanceFields> => {
return client.collection('instances').create<InstanceFields>(payload)
}
@ -29,6 +30,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const getInstanceBySubdomain = safeCatch(
`getInstanceBySubdomain`,
logger,
(subdomain: string): Promise<[InstanceFields, UserFields] | []> =>
client
.collection('instances')
@ -46,6 +48,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const getInstanceById = safeCatch(
`getInstanceById`,
logger,
async (
instanceId: InstanceId
): Promise<[InstanceFields, UserFields] | []> => {
@ -66,6 +69,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const updateInstance = safeCatch(
`updateInstance`,
logger,
async (instanceId: InstanceId, fields: Partial<InstanceFields>) => {
await client.collection('instances').update(instanceId, fields)
}
@ -73,6 +77,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const updateInstanceStatus = safeCatch(
`updateInstanceStatus`,
logger,
async (instanceId: InstanceId, status: InstanceStatus) => {
await updateInstance(instanceId, { status })
}
@ -80,17 +85,19 @@ export const createInstanceMixin = (context: MixinContext) => {
const getInstance = safeCatch(
`getInstance`,
logger,
async (instanceId: InstanceId) => {
return client.collection('instances').getOne<InstanceFields>(instanceId)
}
)
const getInstances = safeCatch(`getInstances`, async () => {
const getInstances = safeCatch(`getInstances`, logger, async () => {
return client.collection('instances').getFullList<InstanceFields>()
})
const updateInstances = safeCatch(
'updateInstances',
logger,
async (cb: (rec: InstanceFields) => Partial<InstanceFields>) => {
const res = await client
.collection('instances')
@ -116,6 +123,7 @@ export const createInstanceMixin = (context: MixinContext) => {
const updateInstanceSeconds = safeCatch(
`updateInstanceSeconds`,
logger,
async (instanceId: InstanceId, forPeriod = new Date()) => {
const startIso = startOfMonth(forPeriod).toISOString()
const endIso = endOfMonth(forPeriod).toISOString()

View File

@ -1,7 +1,6 @@
import {
InstanceFields,
InvocationFields,
logger,
pocketNow,
safeCatch,
} from '@pockethost/common'
@ -12,12 +11,14 @@ export const createInvocationMixin = (
context: MixinContext,
instanceApi: InstanceApi
) => {
const { dbg } = logger().create('InvocationMixin')
const { logger } = context
const { dbg } = logger.create('InvocationMixin')
const { client } = context
const createInvocation = safeCatch(
`createInvocation`,
logger,
async (instance: InstanceFields, pid: number) => {
const init: Partial<InvocationFields> = {
startedAt: pocketNow(),
@ -34,6 +35,7 @@ export const createInvocationMixin = (
const pingInvocation = safeCatch(
`pingInvocation`,
logger,
async (invocation: InvocationFields) => {
const totalSeconds =
(+new Date() - Date.parse(invocation.startedAt)) / 1000
@ -50,6 +52,7 @@ export const createInvocationMixin = (
const finalizeInvocation = safeCatch(
`finalizeInvocation`,
logger,
async (invocation: InvocationFields) => {
dbg('finalizing')
const totalSeconds =

View File

@ -1,8 +1,7 @@
import { DAEMON_PB_DATA_DIR, PUBLIC_APP_DB } from '$constants'
import { logger, safeCatch } from '@pockethost/common'
import { Logger, safeCatch } from '@pockethost/common'
import { Knex } from 'knex'
import { default as PocketBase, default as pocketbaseEs } from 'pocketbase'
import { createBackupMixin } from './BackupMixin'
import { createInstanceMixin } from './InstanceMIxin'
import { createInvocationMixin } from './InvocationMixin'
import { createRawPbClient } from './RawPbClient'
@ -10,26 +9,30 @@ import { createRpcHelper } from './RpcHelper'
export type PocketbaseClientApi = ReturnType<typeof createPbClient>
export type MixinContext = { client: pocketbaseEs; rawDb: Knex }
export type MixinContext = { client: pocketbaseEs; rawDb: Knex; logger: Logger }
export const createPbClient = (url: string) => {
const { info } = logger().create('PbClient')
export const createPbClient = (url: string, logger: Logger) => {
const _clientLogger = logger.create('PbClient')
const { info } = _clientLogger
info(`Initializing client: ${url}`)
const rawDb = createRawPbClient(
`${DAEMON_PB_DATA_DIR}/${PUBLIC_APP_DB}/pb_data/data.db`
`${DAEMON_PB_DATA_DIR}/${PUBLIC_APP_DB}/pb_data/data.db`,
_clientLogger
)
const client = new PocketBase(url)
const adminAuthViaEmail = safeCatch(
`adminAuthViaEmail`,
_clientLogger,
(email: string, password: string) =>
client.admins.authWithPassword(email, password)
)
const createFirstAdmin = safeCatch(
`createFirstAdmin`,
_clientLogger,
(email: string, password: string) =>
client.admins
.create({ email, password, passwordConfirm: password })
@ -40,10 +43,9 @@ export const createPbClient = (url: string) => {
})
)
const context: MixinContext = { client, rawDb }
const context: MixinContext = { client, rawDb, logger: _clientLogger }
const rpcApi = createRpcHelper(context)
const instanceApi = createInstanceMixin(context)
const backupApi = createBackupMixin(context)
const invocationApi = createInvocationMixin(context, instanceApi)
const api = {
@ -55,7 +57,6 @@ export const createPbClient = (url: string) => {
...rpcApi,
...instanceApi,
...invocationApi,
...backupApi,
}
return api

View File

@ -1,9 +1,9 @@
import { logger } from '@pockethost/common'
import { Logger } from '@pockethost/common'
import { existsSync } from 'fs'
import knex from 'knex'
export const createRawPbClient = (filename: string) => {
const { dbg } = logger().create(`rawPbClient`)
export const createRawPbClient = (filename: string, logger: Logger) => {
const { dbg } = logger.create(`rawPbClient`)
dbg(filename)
if (!existsSync(filename)) {

View File

@ -18,9 +18,10 @@ export type RpcHelperConfig = MixinContext
export type RpcHelper = ReturnType<typeof createRpcHelper>
export const createRpcHelper = (config: RpcHelperConfig) => {
const { client, rawDb } = config
const { client, rawDb, logger } = config
const onNewRpc = safeCatch(
`onNewRpc`,
logger,
async (cb: (e: RpcFields<any, any>) => void) => {
const unsub = await client
.collection(RPC_COLLECTION)
@ -32,7 +33,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
}
)
const resetRpcs = safeCatch(`resetRpcs`, async () =>
const resetRpcs = safeCatch(`resetRpcs`, logger, async () =>
rawDb(RPC_COLLECTION)
.whereNotIn('status', [
RpcStatus.FinishedError,
@ -44,7 +45,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
})
)
const incompleteRpcs = safeCatch(`incompleteRpcs`, async () => {
const incompleteRpcs = safeCatch(`incompleteRpcs`, logger, async () => {
return client
.collection(RPC_COLLECTION)
.getFullList<RpcFields<any, any>>(100, {
@ -54,6 +55,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
const rejectRpc = safeCatch(
`rejectRpc`,
logger,
async (rpc: RpcFields<any, any>, err: Error) => {
const fields: Partial<RpcFields<any, any>> = {
status: RpcStatus.FinishedError,
@ -67,6 +69,7 @@ export const createRpcHelper = (config: RpcHelperConfig) => {
const setRpcStatus = safeCatch(
`setRpcStatus`,
logger,
async (
rpc: RpcFields<any, any>,
status: RpcStatus,

View File

@ -5,12 +5,19 @@ import {
PUBLIC_APP_DOMAIN,
PUBLIC_APP_PROTOCOL,
} from '$constants'
import { logger, mkSingleton } from '@pockethost/common'
import { Logger, mkSingleton } from '@pockethost/common'
import { createPbClient } from './PbClient'
export const clientService = mkSingleton(async (url: string) => {
const { dbg, error } = logger().create(`client singleton`)
const client = createPbClient(url)
export type ClientServiceConfig = {
logger: Logger
url: string
}
export const clientService = mkSingleton(async (cfg: ClientServiceConfig) => {
const { logger, url } = cfg
const _clientLogger = logger.create(`client singleton`)
const { dbg, error } = _clientLogger
const client = createPbClient(url, _clientLogger)
try {
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)

View File

@ -1,4 +1,3 @@
export * from './BackupService'
export * from './clientService/clientService'
export * from './clientService/PbClient'
export * from './FtpService/FtpService'

View File

@ -1,11 +1,15 @@
import { logger } from '@pockethost/common'
import { Logger } from '@pockethost/common'
import { chmodSync } from 'fs'
import fetch from 'node-fetch'
import { dirname } from 'path'
import { Extract } from 'unzipper'
export const downloadAndExtract = async (url: string, binPath: string) => {
const { dbg, error } = logger().create('downloadAndExtract')
export const downloadAndExtract = async (
url: string,
binPath: string,
logger: Logger
) => {
const { dbg, error } = logger.create('downloadAndExtract')
await new Promise<void>(async (resolve, reject) => {
dbg(`Fetching ${url}`)

View File

@ -1,4 +1,3 @@
export * from './backupInstance'
export * from './downloadAndExtract'
export * from './ensureDirExists'
export * from './env'

View File

@ -1,8 +1,9 @@
import { logger, safeCatch } from '@pockethost/common'
import { Logger, safeCatch } from '@pockethost/common'
import { exec } from 'child_process'
export const pexec = safeCatch(`pexec`, (cmd: string) => {
const { dbg, error } = logger().create('pexec')
export const pexec = (logger: Logger) =>
safeCatch(`pexec`, logger, (cmd: string) => {
const { dbg, error } = logger.create('pexec')
return new Promise<void>((resolve, reject) => {
dbg(cmd)
exec(cmd, (err, stdout, stderr) => {

View File

@ -1,16 +1,19 @@
import { logger, safeCatch } from '@pockethost/common'
import { Logger, safeCatch } from '@pockethost/common'
export const tryFetch = safeCatch(
export const tryFetch = (logger: Logger) =>
safeCatch(
`tryFetch`,
logger,
(url: string, preflight?: () => Promise<boolean>) => {
const { dbg } = logger().create('tryFetch')
const { dbg } = logger.create('tryFetch')
return new Promise<void>((resolve, reject) => {
const tryFetch = async () => {
if (preflight) {
dbg(`Checking preflight`)
const shouldFetch = await preflight()
if (!shouldFetch) {
throw new Error(`tryFetch failed preflight, aborting`)
reject(new Error(`tryFetch failed preflight, aborting`))
return
}
}
try {

View File

@ -44,7 +44,8 @@ export type PocketbaseClient = ReturnType<typeof createPocketbaseClient>
export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const { url } = config
const { dbg, error } = logger()
const _logger = logger()
const { dbg, error } = _logger
const client = new PocketBase(url)
@ -56,7 +57,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const logOut = () => authStore.clear()
const createUser = safeCatch(`createUser`, (email: string, password: string) =>
const createUser = safeCatch(`createUser`, _logger, (email: string, password: string) =>
client
.collection('users')
.create({
@ -70,7 +71,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
})
)
const confirmVerification = safeCatch(`confirmVerification`, (token: string) =>
const confirmVerification = safeCatch(`confirmVerification`, _logger, (token: string) =>
client
.collection('users')
.confirmVerification(token)
@ -79,7 +80,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
})
)
const requestPasswordReset = safeCatch(`requestPasswordReset`, (email: string) =>
const requestPasswordReset = safeCatch(`requestPasswordReset`, _logger, (email: string) =>
client
.collection('users')
.requestPasswordReset(email)
@ -90,6 +91,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const requestPasswordResetConfirm = safeCatch(
`requestPasswordResetConfirm`,
_logger,
(token: string, password: string) =>
client
.collection('users')
@ -99,11 +101,11 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
})
)
const authViaEmail = safeCatch(`authViaEmail`, (email: string, password: string) =>
const authViaEmail = safeCatch(`authViaEmail`, _logger, (email: string, password: string) =>
client.collection('users').authWithPassword(email, password)
)
const refreshAuthToken = safeCatch(`refreshAuthToken`, () =>
const refreshAuthToken = safeCatch(`refreshAuthToken`, _logger, () =>
client.collection('users').authRefresh()
)
@ -123,6 +125,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
const getInstanceById = safeCatch(
`getInstanceById`,
_logger,
(id: InstanceId): Promise<InstanceFields | undefined> =>
client.collection('instances').getOne<InstanceFields>(id)
)
@ -145,7 +148,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v)
}
const resendVerificationEmail = safeCatch(`resendVerificationEmail`, async () => {
const resendVerificationEmail = safeCatch(`resendVerificationEmail`, _logger, async () => {
const user = client.authStore.model
assertExists(user, `Login required`)
await client.collection('users').requestVerification(user.email)
@ -210,7 +213,7 @@ export const createPocketbaseClient = (config: PocketbaseClientConfig) => {
unsub()
return
}
const _check = safeCatch(`_checkVerified`, refreshAuthToken)
const _check = safeCatch(`_checkVerified`, _logger, refreshAuthToken)
setTimeout(_check, 1000)
// FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8