import { BASE_URL } from './Common.js'
export const TRACE = 10
export const DEBUG = 20
export const INFO = 30
export const WARN = 40
export const ERROR = 50
export const FATAL = 60
/**
* @constant
* @type {Object.<string, number>}
*
*/
export const LOG_LEVELS = {
trace: TRACE,
debug: DEBUG,
info: INFO,
warn: WARN,
error: ERROR,
fatal: FATAL
}
/**
* @classdesc Plugins write
* {@link https://github.com/trentm/node-bunyan?tab=readme-ov-file#log-record-fields|Bunyan}
* formated logs to separate log files. This class provides
* utilities for viewing and following those logs as they're written.
*/
export class Logs {
baseUrl
#reader
#controller
/**
* Instantiate a Log Object
* @constructor
* @param {string} [baseURL] - Protocol, domain, and port for the service.
*/
constructor (baseUrl = BASE_URL) {
this.baseUrl = baseUrl
}
/**
* End the log stream and stop following logs.
*/
end () {
this.#reader?.cancel()
this.#controller?.abort()
}
/**
* Add a handler for following logs. Note that each invocation of this method
* opens a connection to the server and holds it open. Web browsers limit the
* number of connections to a single domain. Avoid using this method more than
* once per client.
* @param {string[]} plugins - Follow logs for the provided Plugin IDs.
* @param {string|number} level - Log Level.
* @param {function} callback - Invoked with two arguments. The first argument
* is an Error object (if an error occurred) or null. The second argument is
* an log object.
*/
follow (plugins, level, callback) {
let pluginList
if (typeof plugins === 'object' && plugins.length > 0) {
pluginList = plugins.join(',')
} else {
throw new Error('Invalid value provided for plugins argument')
}
const logLevel = (typeof level === 'number') ? level : LOG_LEVELS[level]
if (!logLevel) {
throw new Error('Invalid value provided for log level argument')
}
if (typeof callback !== 'function') {
throw new Error('Invalid value provided for callback argument')
}
const url = `${this.baseUrl}/plugin/logs/follow?plugins=${pluginList}&level=${logLevel}`
this.#controller = new AbortController()
const signal = this.#controller.signal
fetch(url, { signal })
.then(({ body }) => {
let buffer = ''
if (!body) {
return callback(new Error('No response body'))
}
const readData = data => {
if (!data.done) {
callback(null, data.value)
this.#reader.read().then(readData).catch(e => callback(e))
}
}
this.#reader = body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform (chunk, controller) {
buffer += chunk
const parts = buffer.split('\n')
parts.slice(0, -1).forEach(part => controller.enqueue(part))
buffer = parts[parts.length - 1]
},
flush (controller) {
if (buffer) {
controller.enqueue(buffer)
}
}
}))
.pipeThrough(new TransformStream({
transform (chunk, controller) {
controller.enqueue(JSON.parse(chunk))
}
}))
.getReader()
this.#reader
.read()
.then(readData)
.catch(e => callback(e))
}).catch(e => {
// Don't emit an error when aborting the fetch operation
if (e.name !== 'AbortError') {
callback(e)
}
})
}
}