A JavaScript/TypeScript logger that implements Syslog severitiy levels.
goals are:
- be lightweight/small
- can be used in browser and node.js
- have as few as possible dependencies (currently just 1)
- (almost) ready to use if you just want to use
console.log
and do not want to log debug messages in production - easily extendable
- functional code and immutable data
A Logger consists of 3 parts:
- Filter (optional) - should a message be logged at all
- Formatter - how to format log entries
- Transporter - where to trasport log entries to
These are packed together into a Handler.
npm install --save @pabra/logger
# or
yarn add @pabra/logger
This works in both, browser and node.js environments.
// import
import getLogger from '@pabra/logger';
// init and use root logger
const rootLogger = getLogger('myProject');
rootLogger.info("I'm using a simple logger now!");
Results in the following console output:
2020-08-13T13:55:32.327Z [myProject] INFORMATIONAL - I'm using a simple logger now!
Pass any additional data after the log message.
rootLogger.warning(
'something unexpected happened',
{ some: ['data', true] },
'23',
42,
);
Results in the following console output:
2020-09-06T07:29:05.356Z [myProject] WARNING - something unexpected happened { some: [ 'data', true ] } 23 42
Call getLogger
on your rootLogger to get a child logger.
// import
import getLogger from '@pabra/logger';
// init root logger
const rootLogger = getLogger('myProject');
// init and use child logger in your modules/components/etc.
const moduleLogger = rootLogger.getLogger('myModule');
moduleLogger.info('Logging from within a module!');
Results in the following console output:
2020-09-06T07:39:08.677Z [myProject.myModule] INFORMATIONAL - Logging from within a module!
Set up a custom Handler to only show log messages starting at 'warning' level in production:
import getLogger, { handlers } from '@pabra/logger';
const logLevel = process.env.NODE_ENV === 'development' ? undefined : 'warning';
const logHandler = handlers.getConsoleRawDataHandler(logLevel);
const rootLogger = getLogger('myProject', logHandler);
// in some module
const moduleLogger = rootLogger.getLogger('myModule');
Then, any log messages that are lower than "warning" will be ignored.
rootLogger.info("I'm using a simple logger now!");
moduleLogger.notice("I'm using a simple module logger now!");
rootLogger.err('No such table in db.');
moduleLogger.warning('User entered invalid user name.');
Will only show messages eqal or higher than 'warning' level:
2020-09-06T07:53:40.896Z [myProject] ERROR - No such table in db.
2020-09-06T07:53:40.896Z [myProject.myModule] WARNING - User entered invalid user name.
You should take care that process.env.NODE_ENV
is properly set. This might
also differ if you use it in node.js or browser (there is no global process
in
the browser - webpack
EnvironmentPlugin might
help with that).
type Logger = {
emerg: (message: string, ...data: any[]) => void;
alert: (message: string, ...data: any[]) => void;
crit: (message: string, ...data: any[]) => void;
err: (message: string, ...data: any[]) => void;
warning: (message: string, ...data: any[]) => void;
notice: (message: string, ...data: any[]) => void;
info: (message: string, ...data: any[]) => void;
debug: (message: string, ...data: any[]) => void;
getLogger: GetLogger;
getHandlers: () => Handlers;
};
import getLogger from '@pabra/logger';
// get a main/root logger
const mainLogger = getLogger(loggerName); // default handler will be used
// or
const mainLogger = getLogger(loggerName, handler);
// or
const mainLogger = getLogger(loggerName, handlers);
// get a child/module logger
const moduleLogger = mainLogger.getLogger(loggerName); // parent's handlers will be used
// or
const moduleLogger = mainLogger.getLogger(loggerName, handler);
// or
const moduleLogger = mainLogger.getLogger(loggerName, handlers);
object | type | required | description |
---|---|---|---|
loggerName |
string |
yes | name of your logger |
handler |
type Handler = { |
no | a single Handler |
handlers |
Handler[] |
no | multiple Handler s |
moduleLogger.info(message, ...data);
object | type | required | description |
---|---|---|---|
moduleLogger |
type Logger = { |
the actual Logger Object |
|
message |
string |
yes | a message to log |
data |
any |
no | some kind of data to log |
For each call of a log function the Logger
will pass the message
and data to each of it's Handler
s.
type Handler = {
readonly filter?: Filter;
readonly formatter: Formatter;
readonly transporter: Transporter;
};
A Handler
keeps all 3 parts together that are needed to handle a
log entry - hence the name. Whereas the filter is optional.
import { handlers, Handler } from '@pabra/logger';
const myHandler: Handler = handlers.getConsoleTextHandler(logLevelName);
const myHandler: Handler = handlers.getConsoleRawDataHandler(logLevelName);
const myHandler: Handler = handlers.getConsoleJsonHandler(logLevelName);
object | type | required | description |
---|---|---|---|
handlers |
{ |
an object of common handlers | |
logLevelName |
type LogLevelName = |
no | The name of the maximal log level to handle (low log levels are more urgent than higher ones). If none is passed (or undefined ) that Hanlder won't filter - means everything get's logged. |
getConsoleRawDataHandler |
( |
This is the default Handler if you don't pass one to getLogger . It mostly works like console.log . It doesn't has a Formatter and just passes the raw data to console. |
|
getConsoleTextHandler |
( |
This Handler will be best for human readability. |
|
getConsoleJsonHandler |
( |
This Handler will be best for machine readability as it will be one big strigified JSON line. |
import { Handler } from '@pabra/logger';
const myHandler: Handler = {
filter: myFilter,
formatter: myFormatter,
transporter: myTransporter,
};
type Filter = (logger: InternalLogger, message: Message) => boolean;
type InternalLogger = {
readonly name: string;
readonly nameChain: string[];
readonly handlers: Handler[];
};
interface Message {
readonly raw: string;
readonly data: any[];
readonly level:
| 'emerg'
| 'alert'
| 'crit'
| 'err'
| 'warning'
| 'notice'
| 'info'
| 'debug';
}
The Filter
function decides if a log entry should be handled at
all. If it returns false
the log entry handling immediately ends for this
handler.
If there is no Filter
provided in a Handler
, every
log entry gets handled. So no Filter
behaves the same as a
Filter
that's always returning true
.
import { filters, Filter } from '@pabra/logger';
const myFilter: Filter = filters.getMaxLevelFilter(logLevelName);
object | type | required | description |
---|---|---|---|
filters |
{ getMaxLevelFilter } as const; |
an object of common filters | |
logLevelName |
type LogLevelName = |
yes | The name of the maximal log level to handle (low log levels are more urgent than higher ones). |
getMaxLevelFilter |
( |
This Filter decides based on the severity of the log entry weather it should be logged/handled or not (low levels are more urgent - see Syslog severitiy levels). |
A Filter
is a function that gets the InternalLogger
object and
the Message
object passed as arguments and needs to return a boolean.
If you want to have a Handler
that should only handle error
log
entries, your Filter
could look like this:
import { Filter } from '@pabra/logger';
const myFilter: Filter = (_logger, message) => message.level === 'err';
// or if you only want to handle log entries from your "auth" module
const myFilter: Filter = (logger, _message) => logger.name === 'auth';
type Formatter = (logger: InternalLogger, message: Message) => string;
type InternalLogger = {
readonly name: string;
readonly nameChain: string[];
readonly handlers: Handler[];
};
interface Message {
readonly raw: string;
readonly data: any[];
readonly level:
| 'emerg'
| 'alert'
| 'crit'
| 'err'
| 'warning'
| 'notice'
| 'info'
| 'debug';
}
The Formatter
function produces the formatted message (string
)
that finally appears in your log file/console/etc. It might add a time stamp and
than somehow join the severity level/name, logger name, raw log message and log
data into one string.
import { formatters, Formatter } from '@pabra/logger';
const myFormatter: Formatter = formatters.jsonFormatter;
const myFormatter: Formatter = formatters.textFormatter;
const myFormatter: Formatter = formatters.textWithoutDataFormatter;
const myFormatter: Formatter = formatters.getJsonLengthFormatter(maxLength);
const myFormatter: Formatter = formatters.getTextLengthFormatter(maxLength);
object | type | required | description |
---|---|---|---|
formatters |
{ |
an object of common formatters | |
maxLength |
undefined | number |
no | The maximum length of your formatted log message. If undefined or omitted the default is 1024^2 (1 MiB). It is there to prevent you from potentially sending huge data objects over the wire. Notice: if used with jsonFormatter the stringified data will end up truncated and not parseable anymore. |
jsonFormatter |
Formatter |
Will return untruncated, stringified JSON like this: { Can handle instances of Error as data. |
|
textFormatter |
Formatter |
Will return untruncated, text like this: 2020-08-16T08:45:08.297Z [main.auth] DEBUG - failed to login {"user":"bob"} Can handle instances of Error as data. |
|
textWithoutDataFormatter |
Formatter |
This Formatter will just return the raw message without trying to serialize data. It's used for getConsoleRawDataHandler to be able to pass arbitrary objects like DOM Nodes or Events to the console which could not be serialized by JSON.stringify otherwise. |
|
getJsonLengthFormatter |
( |
Will return length limited jsonFormatter . |
|
getTextLengthFormatter |
( |
Will return length limited textFormatter . |
A Formatter
is a function that gets the InternalLogger
object
and the Message
object passed as arguments and needs to return a string.
A very simple Formatter
(for the sake of simplicity ignores
data) could look like this:
import { Formatter } from '@pabra/logger';
const myFormatter: Formatter = (logger, message) =>
`${new Date().toISOString()} [${logger.name}] ${message.level}: ${
message.raw
}`;
type Transporter = (logger: InternalLogger, message: MessageFormatted) => void;
type InternalLogger = {
readonly name: string;
readonly nameChain: string[];
readonly handlers: Handler[];
};
interface MessageFormatted {
readonly raw: string;
readonly data: DataArgs;
readonly level: LogLevelName;
readonly formatted: string;
}
The Transporter
"transports" the formatted message to its
destination. That might be the console
, a file, some http endpoint, etc.
import { transporters, Transporter } from '@pabra/logger';
const myTransporter: Transporter = transporters.consoleTransporter;
const myTransporter: Transporter = transporters.consoleWithoutDataTransporter;
object | type | required | description |
---|---|---|---|
transporters |
{ |
an object of common transporters | |
consoleTransporter |
Transporter |
It passes the formated message and data to the console. It's used by the default Handler (getConsoleRawDataHandler - used if no Handler is passed to getLogger ). It can be used if you want to pass arbitrary objects (like DOM Nodes, Events, etc.) to the console without having formatter dealt with them. |
|
consoleWithoutDataTransporter |
Transporter |
It passes only the formatted message to the console. A Formatter should have taken care, that data became part of formatted message. It's used by getConsoleTextHandler and getConsoleJsonHandler . |
A Transporter
is a function that gets the InternalLogger
object and the MessageFormatted
object passed as arguments and needs to return
nothing (viod
).
A very simple Tranporter
to POST to your logging server might look like:
import { Transporter } from '@pabra/logger';
const myTransporter: Transporter = (_logger, message) =>
void fetch('https://example.com', {
method: 'POST',
body: message.formatted,
});