diff --git a/cspell.json b/cspell.json index 24f83e5b..cfd03d31 100644 --- a/cspell.json +++ b/cspell.json @@ -20,6 +20,7 @@ "POCKETSTREAM", "rizzdown", "superadmin", + "syslogd", "unzipper", "upserting" ] diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 2b45212d..1e9f4002 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -12,6 +12,10 @@ module.exports = { name: `edge-ftp`, script: 'pnpm prod:edge:ftp', }, + { + name: `edge-syslog`, + script: 'pnpm prod:edge:syslog', + }, { name: `mothership`, script: 'pnpm prod:mothership', diff --git a/package.json b/package.json index 674d16f1..539ce0a3 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,14 @@ "dev:superadmin": "cd frontends/superadmin && pnpm dev", "dev:edge:daemon": "tsx watch src/cli/edge-daemon.ts", "dev:edge:ftp": "tsx watch src/cli/edge-ftp.ts", + "dev:edge:syslogd": "tsx watch src/cli/edge-syslogd.ts", "dev:downloader": "pnpm download-versions", "dev:mothership:maildev": "npx -y maildev", "dev:mothership:pocketbase": "nodemon --signal SIGTERM --watch src --exec tsx ./src/cli/mothership.ts", "prod:proxy": "dotenv tsx ./src/cli/proxy/server.ts", "prod:edge:daemon": "tsx src/cli/edge-daemon.ts", "prod:edge:ftp": "tsx src/cli/edge-ftp.ts", + "prod:edge:syslog": "tsx src/cli/edge-syslogd.ts", "prod:mothership": "tsx src/cli/mothership.ts", "plop": "plop", "nofile": "cat /proc/sys/fs/file-nr", @@ -50,6 +52,7 @@ "type": "module", "dependencies": { "@s-libs/micro-dash": "^16.1.0", + "@types/winston-syslog": "^2.4.3", "ajv": "^8.12.0", "boolean": "^3.2.0", "bottleneck": "^2.19.5", @@ -77,10 +80,12 @@ "pocketbase": "^0.20.1", "semver": "^7.5.4", "sqlite3": "^5.1.6", + "syslog-parse": "^2.0.0", "tail": "^2.2.6", "tmp": "^0.2.1", "url-pattern": "^1.0.3", - "winston": "^3.11.0" + "winston": "^3.11.0", + "winston-syslog": "^2.7.0" }, "devDependencies": { "@swc/cli": "^0.1.62", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0563f7d1..d2893c52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@s-libs/micro-dash': specifier: ^16.1.0 version: 16.1.0 + '@types/winston-syslog': + specifier: ^2.4.3 + version: 2.4.3 ajv: specifier: ^8.12.0 version: 8.12.0 @@ -92,6 +95,9 @@ importers: sqlite3: specifier: ^5.1.6 version: 5.1.6 + syslog-parse: + specifier: ^2.0.0 + version: 2.0.0 tail: specifier: ^2.2.6 version: 2.2.6 @@ -104,6 +110,9 @@ importers: winston: specifier: ^3.11.0 version: 3.11.0 + winston-syslog: + specifier: ^2.7.0 + version: 2.7.0(winston@3.11.0) devDependencies: '@swc/cli': specifier: ^0.1.62 @@ -1421,6 +1430,12 @@ packages: resolution: {integrity: sha512-mZ0onxTS5OyfSwBNecTKT0h79e4XXHrc9RI5tQfEAf+Fp6NbBmNnc0kg59HO+97V+y3opS+sfo4k4qpYwLt6NQ==} dev: true + /@types/glossy@0.1.3: + resolution: {integrity: sha512-CrdAR+ZgRf0MQnDAW4tUm2LpPmfC6sAWlrBwcX0O2oUKyZvseb6wlHZ0alo++DyaLckxqM4CUa+EfzyITJM7mA==} + dependencies: + '@types/node': 20.8.10 + dev: false + /@types/http-cache-semantics@4.0.3: resolution: {integrity: sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==} dev: true @@ -1527,7 +1542,6 @@ packages: resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} dependencies: undici-types: 5.26.5 - dev: true /@types/pug@2.0.8: resolution: {integrity: sha512-QzhsZ1dMGyJbn/D9V80zp4GIA4J4rfAjCCxc3MP+new0E8dyVdSkR735Lx+n3LIaHNFcjHL5+TbziccuT+fdoQ==} @@ -1606,6 +1620,15 @@ packages: '@types/connect': 3.4.38 dev: true + /@types/winston-syslog@2.4.3: + resolution: {integrity: sha512-z9mO5hxDls4lSTth76sddIETonCMLguppeudk1YxBz4Y/OmdRkeKMfrOTfH74T9gN5WllLnF8XbHdiM8K6EL7A==} + dependencies: + '@types/glossy': 0.1.3 + '@types/node': 20.8.10 + winston: 3.11.0 + winston-transport: 4.6.0 + dev: false + /a-sync-waterfall@1.0.1: resolution: {integrity: sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==} dev: true @@ -1961,6 +1984,14 @@ packages: resolution: {integrity: sha512-v4N2l3RxL+m4zDxyxz3Ne2aTmiPn8ZUpKFpdPtO+ItW1NcTCXA7JeHG5GMBSvoKSkQZ9ycS+EouDVxYB9ufKWA==} dev: true + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true + dependencies: + file-uri-to-path: 1.0.0 + dev: false + optional: true + /bl@1.2.3: resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} dependencies: @@ -3491,6 +3522,12 @@ packages: engines: {node: '>=4'} dev: false + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true + dev: false + optional: true + /filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: @@ -3890,6 +3927,11 @@ packages: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} dev: true + /glossy@0.1.7: + resolution: {integrity: sha512-mTCC51QFadK75MvAhrL5nPVIP291NjML1guo10Sa7Yj04tJU4V++Vgm780NIddg9etQD9D8FM67hFGqM8EE2HQ==} + engines: {node: '>= 0.2.5'} + dev: false + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -7322,6 +7364,11 @@ packages: periscopic: 3.1.0 dev: true + /syslog-parse@2.0.0: + resolution: {integrity: sha512-FI6xGyKM9dRdNCrCWiEy1QhRZskDYkW+lUNAIXkFeht0/XCsSdZ7UsPANFLk0h8b+8Is6Ll8bllUNjME+XCANA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + /tail@2.2.6: resolution: {integrity: sha512-IQ6G4wK/t8VBauYiGPLx+d3fA5XjSVagjWV5SIYzvEvglbQjwEcukeYI68JOPpdydjxhZ9sIgzRlSmwSpphHyw==} engines: {node: '>= 6.0.0'} @@ -7627,7 +7674,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /undici@5.26.5: resolution: {integrity: sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==} @@ -7665,6 +7711,16 @@ packages: '@types/unist': 3.0.2 dev: true + /unix-dgram@2.0.6: + resolution: {integrity: sha512-AURroAsb73BZ6CdAyMrTk/hYKNj3DuYYEuOaB8bYMOHGKupRNScw90Q5C71tWJc3uE7dIeXRyuwN0xLLq3vDTg==} + engines: {node: '>=0.10.48'} + requiresBuild: true + dependencies: + bindings: 1.5.0 + nan: 2.18.0 + dev: false + optional: true + /unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -7864,6 +7920,18 @@ packages: string-width: 5.1.2 dev: true + /winston-syslog@2.7.0(winston@3.11.0): + resolution: {integrity: sha512-w+V0lHO2W6XqcYlvVi4DrblwJShvQbAaruRvUlMPzH1Z+dYvUvo4ra2hhoF6UNTFmC9LBltcTG05ypYL6S/B8A==} + engines: {node: '>= 8'} + peerDependencies: + winston: ^3.8.2 + dependencies: + glossy: 0.1.7 + winston: 3.11.0 + optionalDependencies: + unix-dgram: 2.0.6 + dev: false + /winston-transport@4.6.0: resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} engines: {node: '>= 12.0.0'} diff --git a/src/cli/edge-syslogd.ts b/src/cli/edge-syslogd.ts new file mode 100644 index 00000000..f5c519ee --- /dev/null +++ b/src/cli/edge-syslogd.ts @@ -0,0 +1,53 @@ +import { + DEBUG, + DefaultSettingsService, + SETTINGS, + SYSLOGD_PORT, +} from '$constants' +import { InstanceLogger } from '$services' +import { LogLevelName, LoggerService } from '$src/shared' +import * as dgram from 'dgram' +import parse from 'syslog-parse' + +const server = dgram.createSocket('udp4') + +DefaultSettingsService(SETTINGS) + +const PORT = SYSLOGD_PORT() +const HOST = '0.0.0.0' + +const { dbg, info, error } = LoggerService({ + level: DEBUG() ? LogLevelName.Debug : LogLevelName.Info, +}).create(`edge-syslogd`) + +console.log(`debug is ${DEBUG()}`) + +server.on('error', (err) => { + console.log(`Server error:\n${err.stack}`) + server.close() +}) + +server.on('message', (msg, rinfo) => { + const raw = msg.toString() + const parsed = parse(raw) + if (!parsed) { + return + } + dbg(parsed) + + const { process: instanceId, severity, message } = parsed + + const logger = InstanceLogger(instanceId, `exec`) + if (severity === 'info') { + logger.info(message) + } else { + logger.error(message) + } +}) + +server.on('listening', () => { + const address = server.address() + console.log(`Server listening ${address.address}:${address.port}`) +}) + +server.bind(PORT, HOST) diff --git a/src/constants.ts b/src/constants.ts index 06108d73..0c1024a8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -98,6 +98,8 @@ export const SETTINGS = { TEST_EMAIL: mkString(), LS_WEBHOOK_SECRET: mkString(''), + + SYSLOGD_PORT: mkNumber(6514), } ;(() => { let passed = true @@ -221,6 +223,8 @@ export const TEST_EMAIL = () => settings().TEST_EMAIL export const LS_WEBHOOK_SECRET = () => settings().LS_WEBHOOK_SECRET +export const SYSLOGD_PORT = () => settings().SYSLOGD_PORT + /** Helpers */ export const MOTHERSHIP_DATA_ROOT = () => INSTANCE_DATA_ROOT(MOTHERSHIP_NAME()) diff --git a/src/services/InstanceLoggerService/index.ts b/src/services/InstanceLoggerService/index.ts index 1e3886ca..4e28c42f 100644 --- a/src/services/InstanceLoggerService/index.ts +++ b/src/services/InstanceLoggerService/index.ts @@ -25,8 +25,8 @@ export function InstanceLogger(instanceId: string, target: string) { } const logDirectory = mkInstanceDataPath(instanceId, `logs`) - console.log(`Creating ${logDirectory}`) if (!fs.existsSync(logDirectory)) { + console.log(`Creating ${logDirectory}`) fs.mkdirSync(logDirectory, { recursive: true }) } diff --git a/src/services/PocketBaseService/index.ts b/src/services/PocketBaseService/index.ts index 5abc605d..31513d9c 100644 --- a/src/services/PocketBaseService/index.ts +++ b/src/services/PocketBaseService/index.ts @@ -1,9 +1,10 @@ import { APEX_DOMAIN, + SYSLOGD_PORT, mkContainerHomePath, mkInstanceDataPath, } from '$constants' -import { InstanceLogger, PortService } from '$services' +import { PortService } from '$services' import { LoggerService, SingletonBaseConfig, @@ -19,6 +20,7 @@ import MemoryStream from 'memorystream' import { gte } from 'semver' import { AsyncReturnType } from 'type-fest' import { PocketbaseReleaseVersionService } from '../PocketbaseReleaseVersionService' +import { SyslogLogger } from '../SyslogService' export type Env = { [_: string]: string } export type SpawnConfig = { @@ -92,7 +94,7 @@ export const createPocketbaseService = async ( } = _cfg logger.breadcrumb(subdomain).breadcrumb(instanceId) - const iLogger = InstanceLogger(instanceId, 'exec') + const iLogger = SyslogLogger(instanceId, 'exec') const _version = version || maxVersion // If _version is blank, we use the max version available const realVersion = await getVersion(_version) @@ -106,20 +108,9 @@ export const createPocketbaseService = async ( const docker = new Docker() iLogger.info(`Starting instance`) - const _stdoutData = (data: Buffer) => { - const lines = data.toString().split(/\n/) - lines.forEach((line) => { - iLogger.info(line) - }) - } + const _stdoutData = (data: Buffer) => {} stdout.on('data', _stdoutData) - const _stdErrData = (data: Buffer) => { - const lines = data.toString().split(/\n/) - lines.forEach((line) => { - error(line) - iLogger.error(line) - }) - } + const _stdErrData = (data: Buffer) => {} stderr.on('data', _stdErrData) const Binds = [ `${mkInstanceDataPath(instanceId)}:${mkContainerHomePath()}`, @@ -154,6 +145,13 @@ export const createPocketbaseService = async ( Hard: 4096, }, ], + LogConfig: { + Type: 'syslog', + Config: { + 'syslog-address': `udp://localhost:${SYSLOGD_PORT()}`, + tag: instanceId, + }, + }, }, Tty: false, ExposedPorts: { diff --git a/src/services/SyslogService/index.ts b/src/services/SyslogService/index.ts new file mode 100644 index 00000000..6dba4a21 --- /dev/null +++ b/src/services/SyslogService/index.ts @@ -0,0 +1,48 @@ +import { SYSLOGD_PORT } from '$constants' +import { LoggerService } from '$shared' +import * as winston from 'winston' +import 'winston-syslog' + +const loggers: { + [key: string]: { + info: (msg: string) => void + error: (msg: string) => void + } +} = {} + +export function SyslogLogger(instanceId: string, target: string) { + const loggerKey = `${instanceId}_${target}` + if (loggers[loggerKey]) { + return loggers[loggerKey]! + } + + const logger = winston.createLogger({ + format: winston.format.printf((info) => { + return info.message + }), + transports: [ + new winston.transports.Syslog({ + host: `localhost`, + port: SYSLOGD_PORT(), + app_name: instanceId, + }), + ], + }) + + const { error, warn } = LoggerService() + .create('SyslogLogger') + .breadcrumb(instanceId) + .breadcrumb(target) + + const api = { + info: (msg: string) => { + logger.info(msg) + }, + error: (msg: string) => { + logger.error(msg) + }, + } + + loggers[loggerKey] = api + return api +}