API Design #183
Replies: 2 comments 1 reply
-
Current Lumberjack Core APICreating and configuring a lumberjack instanceTo create a lumberjack instance, you need to provide a list of drivers and a The lumberjack instance is generic over the type of the payload of the logs. const drivers = [...];
const isProduction = true;
const options = {};
const config = createLumberjackConfig(isProduction, options);
let lumberjack = createLumberjack<TPayload>({
drivers: drivers,
config: config,
}); We will discuss the different parts of the creation API in the following sections. DriversThe drivers are the plugins used to write the logs. Other logging libraries call them "transports," "sinks," "monitors," etc. A valid driver is an object whose structure matches the /**
* The interface implemented by drivers. Optionally supports a log payload.
*/
export interface LumberjackDriver<
TPayload extends LumberjackLogPayload | void = void,
> {
/**
* Driver settings.
*/
readonly config: LumberjackDriverConfig;
/**
* A critical log and its text representation is passed to this method.
*/
logCritical(driverLog: LumberjackDriverLog<TPayload>): void;
/**
* A debug log and its text representation are passed to this method.
*/
logDebug(driverLog: LumberjackDriverLog<TPayload>): void;
/**
* An error log and its text representation is passed to this method.
*/
logError(driverLog: LumberjackDriverLog<TPayload>): void;
/**
* An info log and its text representation are passed to this method.
*/
logInfo(driverLog: LumberjackDriverLog<TPayload>): void;
/**
* A trace log and its text representation is passed to this method.
*/
logTrace(driverLog: LumberjackDriverLog<TPayload>): void;
/**
* A warning log and its text representation are passed to this method.
*/
logWarning(driverLog: LumberjackDriverLog<TPayload>): void;
} A The driver config can be extended, but in its basic form, it is composed Each log method receives a /**
* The data structure is passed to a driver by Lumberjack. Optionally supports
* a log payload.
*/
export interface LumberjackDriverLog<
TPayload extends LumberjackLogPayload | void = void,
> {
/**
* The text representation of the log.
*/
readonly formattedLog: string;
/**
* The log. Optionally supports a log payload.
*/
readonly log: LumberjackLog<TPayload>;
} We will learn more about the With that said, we can create a driver by using a class or a factory Therefore, creating the driver list to pass to the const drivers = [createConsoleDriver(...), new HttpDriver(...), ...] ConfigThe Lumberjack configuration is an object with two properties: /**
* Settings used internally by Lumberjack services.
*/
export interface LumberjackConfig<
TPayload extends LumberjackLogPayload | void = void,
> {
/**
* The Lumberjack format function used to generate the text representation of
* a log.
*/
readonly format: LumberjackFormatFunction<TPayload>;
/**
* The default log level filter.
*/
readonly levels: LumberjackConfigLevels;
} This object comprises a The format function is used to generate the text representation of a log. The levels property is a list of the globally allowed log levels. It can either be a list of log levels or a list of a single log level: /**
* A set of Levels used to configure Lumberjacks and drivers. Lumberjack filters logs
* passed to the driver based on its configured log levels.
*/
export type LumberjackConfigLevels =
| LumberjackLogLevel[]
| [LumberjackLevel.Verbose]; Lumberjack also provides a helper function to create the config object. /**
* Helps combine the default Lumberjack configurations with custom developer configurations.
* @param isProductionEnvironment - Lumberjack uses different default log levels based on its running environment.
* @param options - LumberjackOptions that overwrite the default configuration.
* @returns - The combination of default configs and custom overwrites.
*/
export function createLumberjackConfig<
TPayload extends LumberjackLogPayload | void = void,
>(
isProductionEnvironment: boolean,
options: LumberjackOptions = {},
): LumberjackConfig<TPayload> {
return {
format: lumberjackFormatLog<TPayload>,
levels: isProductionEnvironment
? defaultProductionLevels
: defaultDevelopmentLevels,
...options,
} as LumberjackConfig<TPayload>;
} This function takes in a boolean indicating if the environment is production or not The Another default set by this function is the /**
* Default function used by Lumberjack to format the Lumberjack logs.
*
* This function formats a LumberjackLog object into a string with a specific format:
* "<level> <timestamp> [<scope>] <message>". If the scope is not provided, it will be omitted.
*
* @example
* const log = {
* scope: 'Application',
* createdAt: new Date(),
* level: 'ERROR',
* message: 'An unexpected error occurred',
* };
*
* console.log(lumberjackFormatLog(log));
* // Outputs: "ERROR 2023-06-22T15:23:42Z [Application] An unexpected error occurred"
*
*/
export function lumberjackFormatLog<
TPayload extends LumberjackLogPayload | void = void,
>({
scope,
createdAt: timestamp,
level,
message,
}: LumberjackLog<TPayload>): string {
const formattedScope = scope ? ` [${scope}]` : "";
return `${level} ${utcTimestampFor(timestamp)}${formattedScope} ${message}`;
} Both the format and the levels can be overwritten by /**
* Shared Lumberjack settings used by `LumberjackModule`.
*/
export type LumberjackOptions = Partial<LumberjackConfig>; Writing logsOnce created, the lumberjack instance can be used to write logs. The basic syntax looks like this: const lumberjack = createLumberjack(...);
lumberjack.log({...}); Where the The
/**
* A Lumberjack log. Optionally supports a payload.
*/
export interface LumberjackLog<
TPayload extends LumberjackLogPayload | void = void,
> {
/**
* Unix epoch ticks in milliseconds, representing when the log was created.
*/
readonly createdAt: number;
/**
* Level of severity.
*/
readonly level: LumberjackLogLevel;
/**
* Log message, for example, describing an event that happened.
*/
readonly message: string;
/**
* Optional payload with custom properties.
*
* NOTE! Make sure that your drivers support these properties.
*/
readonly payload?: TPayload;
/**
* Scope, for example, domain, application, component, or service.
*/
readonly scope?: string;
} If more specific information is needed, it can be passed to the payload object. If we used every property of the lumberjack.log({
createdAt: new Date(dateTime).valueOf(),
level: LumberjackLogLevel.Info,
mesaage: "A log message",
payload: {
correlationId: "123",
email: "[email protected]",
},
scope: "auth.signin",
});
This can be a bit lengthy, so the library provides a set of helpers. The foundation of all log creation assistance is the LubmerjackLogBuilderThe constructor of the
This basic usage looks like this: const logBuilder = new LumberjackLogBuilder(
LumberjackLogLevel.Info,
"A log message",
); The default value for the () => new Date().valueOf(); Once the builder is created, it can be used to create a log object. // Simple log
lumberjack.log(logBuilder.build());
// Enhanced log
lumberjack.log(
logBuilder
.withScope("auth.signin")
.withPayload({
correlationId: "123",
email: "[email protected]",
})
.build(),
);
// Or
lumberjack.log(
logBuilder.withScope("auth.signin").build({
correlationId: "123",
email: "[email protected]",
}),
);
But the builder can be still verbose. For that reason the library provides They all have the same signature, there is one for each log level. // Split into three lines
const infoLogBuilderFactory = createInfoLogBuilder();
const infoLogBuilder = infoLogBuilderFactory("A log message");
lumberjack.log(infoLogBuilder.build());
// Split into two lines
const infoLogBuilder = createInfoLogBuilder()("A log message");
lumberjack.log(infoLogBuilder.build());
// Or in one line
lumberjack.log(createInfoLogBuilder()("A log message").build());
// Complex log
luumberjack.log(
createInfoLogBuilder()
.withScope("auth.signin")
.withPayload({
correlationId: "123",
email: "[email protected]",
})
.build(),
); TypeScriptThe Lumberjack library has a strong focus on type safety and aims The library is generic over the type of the payload of the logs. The default payload type is The specific payload type can be passed to the const lumberjack = createLumberjack<CustomPayload>(...);
lumberjack.log(createInfoLogBuilder<CustomPayload>()("A log message").build({myCustomPayloadObject})); If there is a mismatch between the payload type and the payload object const lumberjack = createLumberjack<CustomPayload>(...);
// Argument of type 'LumberjackLog<void>' is not assignable to parameter
// of type 'LumberjackLog<CustomPayload>'.Type 'void' is not assignable to type 'CustomPayload'.
lumberjack.log(createInfoLogBuilder()("A log message").build()); If the user wants to send a payload but it doesn't want to specify the payload type,
export interface LumberjackLogPayload {
/**
* A custom property for a log.
*
* NOTE! Make sure that this property is supported by your drivers.
*/
readonly [property: string]: unknown;
} Error handlingAlthough nothing is obviously exported as error handling by the library it is worth mentioning about some "hidden behaviors" that the application would perform and that directly affects the users.
|
Beta Was this translation helpful? Give feedback.
-
From my point of view, when compared to winston, js-logger or ngx-logger, lumberjack is lacking of simplicity to log things. The path from setting up the logger to actually logging something is a too complex and might drive people away from lumberjack as a logging library for their projects. However, lumberjack also has several advantages that makes its strength like its flexibility, its bundle size and its design as a framework agnostic tool. The only think I feel left behind is the developer experience while using it. In order to help on that part, we could consider an additional library, acting as an adapter on top of the core. This way, parts that requires the "native API" could still rely on it, while others could use the simpler one. It would be important that this library:
|
Beta Was this translation helpful? Give feedback.
-
This discussion will contain documents focused on helping design Lumberjack's Core following API.
We will start describing the current API followed by other libraries' APIs. This will help us understand the state of the art regarding logging libraries and inspire us to find the best compromise.
Important The Lumberjack Core must be designed to make it easy to use directly, but its primary usage will be through framework-specific clients wrapping it.
The library must find the right balance between ease of use and extensibility.
Since the library will be combined with other libraries, it should be as light as possible.
Beta Was this translation helpful? Give feedback.
All reactions