Skip to content

Logging

Every part of the platform logs through one structured logger, reached as manablox.logger. Where those records go is configuration: a list of adapters, each one a named destination with its own level.

logging: {
level: 'info',
adapters: [
{ type: 'console' },
{ type: 'file', path: './data/logs/api.log', level: 'debug' },
{ type: 'http', url: 'https://logs.example.com/ingest', headers: { authorization: 'Bearer ...' } },
],
}

With no adapters the logger writes to the console alone, which is what a container wants. logLevel still works and means logging.level.

Levels

trace, debug, info (the default), warn, error, fatal. logging.level is the floor for every adapter; an adapter’s own level narrows or widens it, so a file can keep debug while the console stays at info. The record is created once and each adapter decides whether to write it.

Built-in adapters

console

OptionDefaultPurpose
prettyon unless NODE_ENV=productionColourised, human-readable lines instead of JSON
destinationstdoutstdout or stderr

file

OptionDefaultPurpose
pathrequiredFile the JSON lines are appended to
mkdirtrueCreate the parent directory when it is missing
appendtrueSet false to truncate at boot

Rotation is left to the system that already does it (logrotate, the platform’s own collector); the adapter holds the file open and writes to it.

http

Posts batches to any endpoint that accepts them, which is the shape most hosted providers take.

OptionDefaultPurpose
urlrequiredWhere batches are posted
methodPOST
headersnoneAPI keys and source tags
formatndjsonndjson for one record per line, json for an array
batchSize100Records per request
flushInterval5000Milliseconds before a partial batch is sent
maxQueue10000Records held while the endpoint is unreachable
timeout10000Per-request timeout in milliseconds

Logging never blocks or fails the request that produced it. A failed batch is retried on the next flush, and once the queue is full the oldest records are dropped so the newest survive; the count of dropped records rides along with the next successful batch in the x-manablox-dropped header. On shutdown the queue is drained once, without retries, so a dead collector cannot hang a deploy.

Redaction

req.headers.authorization, req.headers.cookie, *.password, *.refreshToken and *.secret are replaced with [redacted] before a record reaches any adapter. Setting logging.redact replaces that list, so include the defaults you still want.

Fields on every record

logging.base stamps fields on every line, which is how records from several processes stay apart in one collector:

logging: { base: { service: 'management-api', release: process.env.GIT_SHA } }

Environment variables

loggingConfigFromEnv() builds the list above from the environment, so an operator can add a destination without editing the config file. It is what the configs manablox create writes use.

VariableDefaultPurpose
LOG_LEVELinfoThe floor for every adapter
LOG_PRETTYby NODE_ENVtrue or false, overriding pretty console output
LOG_CONSOLE_LEVELLOG_LEVELConsole level
LOG_FILEAdds a file adapter writing to this path
LOG_FILE_LEVELLOG_LEVELFile level
LOG_HTTP_URLAdds an HTTP adapter posting to this URL
LOG_HTTP_HEADERSauthorization: Bearer abc, x-source: api
LOG_HTTP_BATCH_SIZE100Records per request
LOG_HTTP_LEVELLOG_LEVELHTTP level

Writing an adapter

An adapter is a name and a writable stream that receives one JSON line per record. A package registers a type once, at import time, and a config then names it:

import { registerLogAdapter } from '@manablox/core/node';
registerLogAdapter('acme', (options) => {
const client = new AcmeClient(options.apiKey as string);
return {
name: 'acme',
stream: new Writable({
write(chunk, _encoding, callback) {
client.send(JSON.parse(String(chunk)));
callback();
},
}),
close: () => client.flush(),
};
});
logging: { adapters: [{ type: 'console' }, { type: 'acme', apiKey: process.env.ACME_KEY }] }

An adapter object, or a function returning one, can also be placed in adapters directly, which is the shortest path for a one-off destination in a project’s own config. An unknown type is refused at boot rather than dropped quietly, because a logger that lost a destination is how an incident becomes invisible.

close() is called when the instance stops, before the rest of the shutdown, so an adapter that buffers gets its chance to flush.