diff --git a/docker-compose.yml b/docker-compose.yml index 31825444e..92cd17277 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,6 +78,20 @@ services: labels: CONNECT: "localhost:3000" cpu_shares: "${CPU_SHARES_IMPORTANT}" + logpipe: + container_name: logpipe + image: ghcr.io/riccardobl/logpipe:0.0.5 + restart: unless-stopped + healthcheck: + <<: *healthcheck + test: ["CMD", "curl", "-f", "http://localhost:7068/health"] + expose: + - "7068:7068" + ports: + - "7068" + tmpfs: + - /tmp + cpu_shares: "${CPU_SHARES_LOW}" capture: container_name: capture build: diff --git a/lib/logger.js b/lib/logger.js index 40cba6ea5..5b20a3860 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,6 +1,8 @@ // ts-check import { SSR } from '@/lib/constants' +const isBrowser = !SSR + export const LogLevel = { TRACE: 600, DEBUG: 500, @@ -16,27 +18,48 @@ export const LogLevel = { */ export class LogAttachment { /** - * Log something - * @param {Logger} logger - the logger that called this attachment - * @param {number} level - the log level - * @param {string[]} tags - the tags - * @param {...any} message - the message to log - * @abstract - * @protected - * @returns {Promise} - */ + * Log something + * @param {Logger} logger - the logger that called this attachment + * @param {number} level - the log level + * @param {string[]} tags - the tags + * @param {...any} message - the message to log + * @abstract + * @protected + */ log (logger, level, tags, ...message) { - throw new Error('not implemented') + throw new Error('Method not implemented.') } } export class ConsoleLogAttachment extends LogAttachment { + level = undefined + /** + * @param {number} level + */ + constructor (level) { + super() + this.level = level + } + log (logger, level, tags, ...message) { + if (this.level && level > this.level) return + let head = '' - if (SSR) { - head += `[${new Date().toISOString()}] ` + if (!isBrowser) { + const date = new Date() + const year = date.getFullYear() + const month = ('0' + (date.getMonth() + 1)).slice(-2) + const day = ('0' + date.getDate()).slice(-2) + const hour = ('0' + date.getHours()).slice(-2) + const minute = ('0' + date.getMinutes()).slice(-2) + const second = ('0' + date.getSeconds()).slice(-2) + head += `[${year}-${month}-${day} ${hour}:${minute}:${second}] ` + } + head += `[${logger.name}] ` + if (!isBrowser) { + head += `[${LogLevel[level]}] ` } - head += `[${logger.getName()}] ` + const tail = tags.length ? ` ${tags.join(',')}` : '' if (level <= LogLevel.ERROR) { console.error(head, ...message, tail) @@ -51,36 +74,38 @@ export class ConsoleLogAttachment extends LogAttachment { } export class JSONLogAttachment extends LogAttachment { + level = undefined endpoint = null - util = undefined - constructor (endpoint, util) { + + constructor (endpoint, level) { super() this.endpoint = endpoint - this.util = util + this.level = level } log (logger, level, tags, ...message) { + if (this.level && level > this.level) return + const serialize = (m) => { - if (typeof m === 'function') { - return m.toString() + '\n' + (new Error()).stack - } else if (typeof m === 'undefined') { + const type = typeof m + if (type === 'function') { + return m.toString() + '\n' + new Error().stack + } else if (type === 'undefined') { return 'undefined' } else if (m === null) { return 'null' - } else if (typeof m === 'string') { - return m - } else if (typeof m === 'number' || typeof m === 'bigint') { - return m.toString() + } else if (type === 'string' || type === 'number' || type === 'bigint' || type === 'boolean') { + return String(m) } else if (m instanceof Error) { return m.message || m.toString() } else if (m instanceof ArrayBuffer || m instanceof Uint8Array) { - return 'Buffer:' + Array.prototype.map.call(new Uint8Array(m), x => ('00' + x.toString(16)).slice(-2)).join('') + return 'Buffer:' + Array.prototype.map.call(new Uint8Array(m), (x) => ('00' + x.toString(16)).slice(-2)).join('') + } else if (type === 'object' && Array.isArray(m)) { + return JSON.stringify(m.map(serialize), null, 2) } else { try { - if (SSR && this.util) { - const inspected = this.util.inspect(m, { depth: 6 }) - return inspected - } + const str = m.toString() + if (str !== '[object Object]') return str } catch (e) { console.error(e) } @@ -88,27 +113,20 @@ export class JSONLogAttachment extends LogAttachment { } } - const messageParts = message.map(m => { - return Promise.resolve(serialize(m)) - }) - - const date = new Date().toISOString() - Promise.all(messageParts) - .then(parts => { - return fetch(this.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - logger: logger.getName(), - tags, - level: Object.entries(LogLevel).find(([k, v]) => v === level)[0], - message: parts.join(' '), - createdAt: date - }) - }) - }).catch(e => console.error('Error in JSONLogAttachment', e)) + const logLevelStr = Object.entries(LogLevel).find(([k, v]) => v === level)[0] + fetch(this.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + logger: logger.name, + tags, + level: logLevelStr, + message: message.map((m) => serialize(m)).join(' '), + createdAt: new Date().toISOString() + }) + }).catch((e) => console.error('Error in JSONLogAttachment', e)) } } @@ -120,40 +138,64 @@ export class JSONLogAttachment extends LogAttachment { */ export class Logger { tags = [] - globalTags = [] + globalTags = {} attachments = [] - constructor (name, level, tags, globalTags) { + name = null + level = null + groupTags = [] + + /** + * + * @param {string} name + * @param {number} [level] + * @param {string[]} [tags] + * @param {{[key: string]: string}} [globalTags] + * @param {string[]} [groupTags] + */ + constructor (name, level, tags, globalTags, groupTags) { this.name = name this.tags.push(...tags) this.globalTags = globalTags || {} - this.level = LogLevel[level.toUpperCase()] || LogLevel.INFO - } - - getName () { - return this.name + this.level = level + if (groupTags) this.groupTags.push(...groupTags) } /** - * Add a log attachment - * @param {LogAttachment} attachment - the attachment to add - * @public - */ + * Add a log attachment + * @param {LogAttachment} attachment - the attachment to add + * @public + */ addAttachment (attachment) { this.attachments.push(attachment) } + group (label) { + this.groupTags.push(label) + } + + groupEnd () { + this.groupTags.pop() + } + + fork (label) { + const logger = new Logger(this.name, this.level, [...this.tags], this.globalTags, [...this.groupTags, label]) + for (const attachment of this.attachments) { + logger.addAttachment(attachment) + } + return logger + } + /** - * Log something - * @param {number} level - the log level - * @param {...any} message - the message to log - * @returns {Promise} - * @public - */ + * Log something + * @param {number} level - the log level + * @param {...any} message - the message to log + * @public + */ log (level, ...message) { if (level > this.level) return for (const attachment of this.attachments) { try { - attachment.log(this, level, [...this.tags, ...Object.entries(this.globalTags).map(([k, v]) => `${k}:${v}`)], ...message) + attachment.log(this, level, [...this.tags, ...this.groupTags, ...Object.entries(this.globalTags).map(([k, v]) => `${k}:${v}`)], ...message) } catch (e) { console.error('Error in log attachment', e) } @@ -161,26 +203,31 @@ export class Logger { } /** - * Log something lazily. - * @param {number} level - the log level - * @param {() => (string | string[] | Promise)} func - The function to call (can be async, but better not) - * @returns {Promise} - * @throws {Error} if func is not a function - * @public - */ + * Log something lazily. + * @param {number} level - the log level + * @param {() => (any|Promise)} func - The function to call (can be async, but better not) + * @throws {Error} if func is not a function + * @public + */ logLazy (level, func) { if (typeof func !== 'function') { throw new Error('lazy log needs a function to call') } + if (level > this.level) return + try { const res = func() + const _log = (message) => { message = Array.isArray(message) ? message : [message] this.log(level, ...message) } + if (res instanceof Promise) { - res.then(_log).catch(e => this.error('Error in lazy log', e)) + res.then(_log) + .catch((e) => this.error('Error in lazy log', e)) + .catch((e) => console.error('Error in lazy log', e)) } else { _log(res) } @@ -248,18 +295,10 @@ export function setGlobalLoggerTag (key, value) { } } -export function getLogger (name, tags, level) { - if (!name) { - throw new Error('name is required') - } - - name = name || 'default' - tags = tags || [] - if (!Array.isArray(tags)) { - tags = [tags] - } +export function getLogger (name = 'default', tags = [], level) { + if (!Array.isArray(tags)) tags = [tags] - let httpEndpoint = SSR ? 'http://logpipe:7068/write' : 'http://localhost:7068/write' + let httpEndpoint = !isBrowser ? 'http://logpipe:7068/write' : 'http://localhost:7068/write' let env = 'production' if (typeof process !== 'undefined') { @@ -267,23 +306,22 @@ export function getLogger (name, tags, level) { httpEndpoint = process.env.SN_LOG_HTTP_ENDPOINT || httpEndpoint level = level ?? process.env.SN_LOG_LEVEL } - level = level ?? env === 'development' ? 'TRACE' : 'INFO' + level = level ?? (env === 'development' ? 'TRACE' : 'INFO') - // test - httpEndpoint = 'https://logpipe.frk.wf/write' - - if (SSR) { + if (!isBrowser) { tags.push('backend') } else { tags.push('frontend') } - const logger = new Logger(name, level, tags, globalLoggerTags) - logger.addAttachment(new ConsoleLogAttachment()) + const logger = new Logger(name, LogLevel[level], tags, globalLoggerTags) if (env === 'development') { logger.addAttachment(new ConsoleLogAttachment()) logger.addAttachment(new JSONLogAttachment(httpEndpoint)) + } else { + logger.addAttachment(new ConsoleLogAttachment()) } + return logger } diff --git a/sndev b/sndev index a0fc1dcc8..e77f57d6c 100755 --- a/sndev +++ b/sndev @@ -139,6 +139,10 @@ sndev__logs() { docker__compose logs "$@" } +sndev__logview() { + docker__compose exec -it logpipe bash /app/scripts/stream.sh +} + sndev__help_logs() { help=" get logs from sndev env @@ -593,6 +597,7 @@ COMMANDS restart restart env status status of env logs logs from env + logview stream logs from the app delete delete env sn: