enh: winston logging

This commit is contained in:
Ben Allfree 2023-10-19 08:51:10 -07:00
parent 1b749a621b
commit 6a639a3021
20 changed files with 289 additions and 365 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
<script>
import UserLoggedIn from '$components/helpers/UserLoggedIn.svelte'
</script>
<UserLoggedIn redirect>
<slot />
</UserLoggedIn>

View File

@ -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
View File

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