TypeScript Logging category style
Logging library for Typescript projects. This is one of the available flavors, please see here for all
available styles.
This flavor differentiates itself from the alternatives by allowing applications to do topological logging
(by creating a tree structure of loggers). The corner stone is the Category
from which child categories can be created
recursively. Each category is a logger as well and can be used to log.
As an example let's look at performance logging. We could use a Category
named "Performance" which would be accessible
throughout the application. Any messages logged by it will be categorized as to belong to "Performance"
and all logging can easily be enabled/disabled for this Category. For more fine-grained control, child categories can be
created if needed, creating a topology/tree of loggers.
Using typescript-logging version 1? Please
visit https://github.com/vauxite-org/typescript-logging/tree/release-1.x
for more details. Consider upgrading to the latest version.
Getting started
This section describes how to get started quickly.
Installation
To install this flavor issue the following commands:
npm install --save typescript-logging
npm install --save typescript-logging-category-style
The first command installs the "core" of typescript logging and is a required dependency. The second command installs
the flavor (category) to use.
Quick start
The following code defines a configuration file which creates a single CategoryProvider
, which is configured with a
log level of Debug
(default is Error
). We also create a couple of root categories that will be used by the modules
below to either log directly or create child categories from.
Note that we give a CategoryProvider a unique name, this is useful for external control (
see Dynamic control)
for more details about that.
import {LogLevel} from "typescript-logging";
import {CategoryProvider, Category} from "typescript-logging-category-style";
const provider = CategoryProvider.createProvider("ExampleProvider", {
level: LogLevel.Debug,
});
export const rootModel = provider.getCategory("model");
export const rootService = provider.getCategory("service");
export const rootMain = provider.getCategory("main");
The modules below use the categories created in LogConfig.ts in various ways.
import {rootService} from "../config/LogConfig";
const log = rootService.getChildCategory("Account");
export interface Account {
name: string;
}
export function createAccount(name: string): Account {
log.debug(() => `Creating new account with name '${name}'.`);
return {name};
}
import {rootService} from "../config/LogConfig";
import {Account} from "../model/Account";
const log = rootService.getChildCategory("AccountService");
export async function saveAccount(account: Account) {
log.debug(() => `Will save account '${account.name}'.`);
try {
await someUpdateHere();
}
catch (e) {
log.error(() => `Failed to save account '${account.name}'.`, e);
throw e;
}
}
import {rootMain} from "./config/LogConfig";
import {createAccount} from "./model/Account";
import {saveAccount} from "./service/Account";
const account = createAccount("My Account");
saveAccount(account)
.then(() => {
rootMain.debug("Successfully created account.", account.name);
})
.catch(e => {
rootMain.error("Ooops...", e);
});
Now let's assume Main.ts is executed above. It successfully creates and saves the account. The logging will then roughly
end up like below.
2021-12-31 23:14:26,273 DEBUG [model.Account] Creating new account with name 'My Account'.
2021-12-31 23:14:26,275 DEBUG [service.Account] Will save account 'My Account'.
2021-12-31 23:14:27,192 DEBUG [main] Successfully created account. ["My Account"]
Logging
The quick start above gives an overview on how to create a provider, get categories from it and then log using a
category.
To be clear about this from the beginning: a Category
is a Logger
. In fact a Category extends the CoreLogger
interface. Category has a few additional properties, such as a name an optional parent (category) and children (child
categories).
So a category in this flavor can be used to log.
A logger can log on different LogLevels
:
- Trace
- Debug
- Info
- Warn
- Error
- Fatal
- Off
Each level except 'Off' has a matching function on the Logger, for example trace
for Trace and debug
for Debug. The
signatures of the function are the same for all of them. Note that the level 'Off' turns logging off completely.
This shows the signatures for debug.
interface CoreLogger {
debug(message: LogMessageType, ...args: unknown[]): void;
debug(message: LogMessageType, error: ExceptionType, ...args: unknown[]): void;
}
The above may look confusing, but we merge the functions in the implementation (and this is how to declare this in
TypeScript).
Essentially it allows us to log messages in various formats like this:
log.debug("This is a simple message");
log.debug("This is a simple message and we log an Error (normally you'd catch it and then log it)", new Error("Some Exception"));
log.debug(() => "Simple message as lambda");
log.debug(() => "Simple message as lambda with Error", () => new Error("SomeOther"));
log.debug("Simple message with some random arguments", 100, "abc", ["some", "array"], true);
log.debug(() => "Simple message as lambda with Error and some random arguments", new Error("Some Exception"), 100, "abc", ["some", "array"], true);
The example of debug
applies to any of the available functions (trace, debug, info, warn, error and fatal). What is
logged exactly how will depend on the configuration and channel in use.
Configuration
This section provides more details on how to configure the category style logging.
Options
A CategoryProvider created without any options logs on LogLevel.Error
and uses a default channel that logs to the
console. Messages are formatted in a default sane format (see above for an example).
This code snippet creates a CategoryProvider with default settings:
import {CategoryProvider} from "typescript-logging-category-style";
const provider = CategoryProvider.createProvider("ProviderWithDefaultSettings");
const rootLogger = provider.getCategory("Root");
The following configuration options are available and can be passed as optional argument when creating the
CategoryProvider. The documentation for each option can be read below
(this is from the source code so your IDE will help you with that too).
type CategoryConfigOptional = {
readonly level?: LogLevel;
readonly channel?: LogChannel | RawLogChannel;
readonly argumentFormatter?: ArgumentFormatterType;
readonly dateFormatter?: DateFormatterType;
readonly allowSameCategoryName?: boolean;
}
With this knowledge we can create for example a provider that logs on a different log level, uses a custom date
formatter and logs to a custom LogChannel like the following code snippet.
import {LogLevel} from "typescript-logging";
import {CategoryProvider} from "typescript-logging-category-style";
const provider = CategoryProvider.createProvider("ProviderWithCustomSettings", {
level: LogLevel.Info,
dateFormatter: millisSinceEpoch => `${millisSinceEpoch}`,
channel: {
type: "LogChannel",
write: logMessage => console.log(`This is a custom channel: ${logMessage.message}`),
},
});
The example is a bit silly to keep it simple. It writes the millis out literally as a string
(you probably want to convert it to a Date and then return a custom string instead). It also specifies a custom channel
which writes to the console pre-fixed by some text. However, all the options can be fully customized to your needs, so
you can for example write to something else completely.
Channels
In the previous section we already briefly touched on the concept of channels. In this section we will look into more
detail what it means and how they can be used to customize your logging if needed.
Using the standard settings a provider uses a predefined LogChannel
, which logs to console (from DefaultChannels
to be exactly).
There are two type of channels that can be used for configuration:
The LogChannel is best used if you only want to write a pre-formatted message (and possibly error stack) to another
place than the default console. The LogChannel is defined as follows.
export interface LogChannel {
readonly type: "LogChannel";
readonly write: (msg: LogMessage) => void;
}
Essentially it expects you to provide the write
function. The type
(a discriminating union) is solely there to
distinguish between the different channels and is always "LogChannel"
for a LogChannel
. The write function is called
with a LogMessage
when it must be logged, and contains a message and optional error (formatted as stack). We have
already seen how to create a custom LogChannel in the previous section (have a look above if you didn't read it yet).
The RawLogChannel can be used for more advanced scenarios. It allows us to completely format the message in any way
we want as well as writing it elsewhere. The RawLogChannel is defined as follows.
export interface RawLogChannel {
readonly type: "RawLogChannel";
readonly write: (msg: RawLogMessage, formatArg: (arg: unknown) => string) => void;
}
Similar to LogChannel it expects you to provide the write
function. The main difference here is that the
passed RawLogMessage
contains a lot of information on what is supposed to be logged. It is up to the implementer of
the function what to do with it and how to output it.
The RawLogMessage
is defined as follows:
export interface RawLogMessage {
readonly level: LogLevel;
readonly timeInMillis: number;
readonly logNames: string | ReadonlyArray<string>;
readonly message: string;
readonly exception?: Error;
readonly args?: ReadonlyArray<unknown>;
}
As can be seen there are various data fields present. There are some utility format functions present to help out if
needed. These are formatArgument
and formatDate
which respectively format an argument and a time stamp as the
library does for a LogChannel
.
The following code snippet creates a provider which uses a RawLogChannel
.
import {LogLevel} from "typescript-logging";
import {CategoryProvider} from "typescript-logging-category-style";
const provider = CategoryProvider.createProvider("ProviderWithCustomSettings", {
channel: {
type: "RawLogChannel",
write: (msg, formatArg) => {
const date = new Date(msg.timeInMillis);
const dateStr = `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`;
console.log(`${dateStr} ${LogLevel[msg.level]} ${msg.message}`);
},
},
});
The code above logs only the LogLevel
and message of the incoming RawLogMessage
and creates a custom date format for
it (this logs the date in dd-MM-yyyy format). The example is of course simplified, but what you should pick up from this
is that you can fully control how to format the raw data and where to write to.
For clarity, the examples inline the channels for clarity, but you can of course write separate functions and/or classes
for them as well if they get complex.
Dynamic control
This section gives an overview on how to dynamically control the log levels of your application. This means to change
the levels after the normal setup/configuration, at a certain point in time.
This is very useful if your application for a user encounters an issue. In order to find out what is wrong you can ask
for more information and tell the customer to enable certain parts of the logging and reproduce their problem with debug
logging enabled.
There are two ways to achieve this.
- [Programmatically by your application](#programmatic control)
- [Externally using the CategoryControl](#external control)
Programmatic control
This allows your application to take matters in its own hands. You could add some option to change logging for example
from the user interface of the application. You would do that then by changing the runtime settings of various
categories.
The CategoryProvider exposes two functions for this purpose:
interface CategoryProvider {
readonly updateRuntimeSettings: (settings: RuntimeSettings) => void;
readonly updateRuntimeSettingsCategory: (category: Category, settings: CategoryRuntimeSettings) => void;
}
The function updateRuntimeSettings
allows changing the level (or channel) for all categories. This applies to
already created categories and new ones by default.
The function updateRuntimeSettingsCategory
allows a change of log level only, for given category
(and it's children recursively).
The following snippet will change the log level to Trace
for all existing categories and new ones.
provider.updateRuntimeSettings({level: LogLevel.Trace});
External control
This allows you to control the logging dynamically, but externally through the browser console running your client
application.
This is available as function CATEGORY_LOG_CONTROL
and can be imported from the library. This can then be used like:
import {CATEGORY_LOG_CONTROL} from "typescript-logging-category-style";
const control = CATEGORY_LOG_CONTROL();
control.help();
const controlProvider = control.getProvider("MyProviderName");
controlProvider.help();
controlProvider.showSettings();
controlProvider.update("debug");
controlProvider.update("trace", 5);
controlProvider.save();
controlProvider.reset();
controlProvider.restore();
This is by default not available to the client as it needs to be your choice whether to expose this in the browser
console or not. In order to do this, it comes down to one of:
- Expose it in the index.ts(x) of your application and make sure your bundler exports it by prefixing with a variable
for example (recommended).
- Expose it on the window in some fashion
Expose using Webpack example
The following section shows how to export the CATEGORY_LOG_CONTROL
function using Webpack, and make it available in
the browser console. This allows it to be used in a client application when you need to debug something that is already
delivered to a customer.
export {CATEGORY_LOG_CONTROL} from "typescript-logging-category-style";
Now make sure Webpack creates a bundle that exposes the index (simplified as ts-loaders etc. are left out, but it shows
the relevant part on how to expose it).
const path = require("path");
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "myapp.js",
library: "myapp",
},
};
Now when you'd open your application in your browser, open the console - you can type:
const control = myapp.CATEGORY_LOG_CONTROL();
control.help();
Expose using Rollup example
The following section shows how to export the CATEGORY_LOG_CONTROL
function using Rollup, and make it available in the
browser console. This allows it to be used in a client application when you need to debug something that is already
delivered to a customer.
export {CATEGORY_LOG_CONTROL} from "typescript-logging-category-style";
Now make sure Rollup creates a bundle that exposes the index (simplified as plugins are left out, but it shows the
relevant part on how to expose it).
export default {
input: "src/index.ts",
output: [
{
file: "dist/myapp.js",
name: "myapp",
format: "iife",
},
],
};
Now when you'd open your application in your browser, open the console - you can type:
const control = myapp.CATEGORY_LOG_CONTROL();
control.help();
Expose on window manually
import {CATEGORY_LOG_CONTROL} from "typescript-logging-category-style";
if (window !== undefined) {
(window as any).appLogControl = CATEGORY_LOG_CONTROL;
}
The above code exposes "appLogControl" on the global window object if it exists. When it does the logging can be
accessed in the browser's console as follows:
const control = appLogControl();
control.help();