Skip to main content
version 1.0.0

Logging

Introduction

To help you learn more about what's happening within your application, Athenna provides robust logging services that allow you to log messages to file, console, buckets and even to Slack to notify your entire team.

Athenna logging is based on "channels". Each channel represents a specific way of writing log information. For example, the application channel writes log of your application when bootstrapping, while the request channel writes log of the http requests that your application receives.

Configuration

All the configuration options for your application's logging behavior is housed in the config/logging.js configuration file. This file allows you to configure your application's log channels, so be sure to review each of the available channels and their options. We'll review a few common options below.

Available channel drivers

Each log channel is powered by a "driver". The driver determines how and where the log message is actually transported. The following log channel drivers are available in every Athenna application. An entry for most of these drivers is already present in your application's config/logging.js configuration file, so be sure to review this file to become familiar with its contents:

NameDescription
stackA wrapper to facilitate creating "multi-channel" channels
consoleA driver that transports the log to the stdout/stderr using process.stdout.write
fileA driver that transports the log to a file
nullA driver that discards all log messages
slackA driver that transports the log message to a slack channel
discordA driver that transports the log message to a discord channel
telegramA driver that transports the log message to a telegram chat

Channel prerequisites

Almost all channels doesn't need any additional configuration to work. But some of than need a couple of changes in config/logging.js file to be able to use. They are slack, discord and telegram.

Configuring the Slack channel

The slack channel only requires the url property inside options. This URL should match a URL for an incoming webhook that you have configured for your Slack team.

Configuring the Discord channel

The discord channel requires the url and username properties inside options. The URL should match a URL for an incoming webhook that you have configured for your Discord server. And the username is just the name of the bot that will deliver the message.

Configuring the Telegram channel

The telegram channel requires the token and chatId properties inside options. The token should match a Telegram bot token that you have created talking with Bot Father. And the chatId is just the id of the chat that the bot will deliver the message.

Writing log messages

You may write information to the logs using the Log facade. The logger provides the five logging levels:

import { Log } from '@athenna/logger'

const message = 'Hello Athenna!'

Log.trace(message)
Log.debug(message)
Log.info(message)
Log.success(message)
Log.warn(message)
Log.error(message)
Log.fatal(message)

You may call any of these methods to log a message for the corresponding level. By default, the message will be written to the default log channel as configured by your logging configuration file:

export class WelcomeController {
/** @type {import('#app/Services/WelcomeService').WelcomeService} */
#welcomeService

/**
* Create a new controller instance.
*
* @param {any} welcomeService
*/
constructor(welcomeService) {
this.#welcomeService = welcomeService
}

/**
* Show the welcome payload.
*
* @param {import('@athenna/http').ContextContract} ctx
*/
async show({ request, response }) {
const data = await this.#welcomeService.findOne()

Log.info(`Showing the welcome message for ip: ${request.ip}`)

return response.status(200).send(data)
}
}

Writing to specific channels

Sometimes you may wish to log a message to a channel other than your application's default channel. You may use the channel method on the Log facade to retrieve and log to any channel defined in your configuration file:

Log.channel('slack').info('Hello from Athenna!')

Building log stacks

As mentioned previously, the stack driver allows you to combine multiple channels into a single log channel for convenience. To illustrate how to use log stacks, let's take a look at an example configuration that you might see in a production application:

Take note of the level configuration option present in slack channel of your config/logging.js file:

channels: {
stack: {
driver: 'stack',
channels: ['application', 'slack'],
},

application: {
driver: 'console',
level: 'trace',

formatter: 'simple',
},

slack: {
driver: 'slack',
level: 'error',
url: 'your-slack-webhook-url',

formatter: 'message',
},
},

Let's dissect this configuration. First, notice our stack channel aggregates two other channels via its channels option: application and slack. So, when logging messages, both of these channels will have the opportunity to log the message. However, as we will see below, whether these channels actually log the message may be determined by the message's severity / "level".

Log levels

Take note of the level configuration option present on the application and slack channel configurations in the example above. This option determines the minimum "level" a message must be in order to be logged by the channel. In descending order of severity, these log levels are: fatal, error, warn, success, info, debug and trace.

So, imagine we log a message using the debug method:

Log.debug('An informational message.')

Given our configuration, the application channel will write the message to the system log; however, since the error message is not error or above, it will not be sent to Slack. However, if we log an fatal message, it will be sent to both the system log and Slack since the fatal level is above our minimum level threshold for both channels:

Log.fatal('The application is down!')

Runtime configurations

It is also possible to set runtime configurations when using the Log facade. This way you will never be total dependent from config/logging.js configuration file. To accomplish this, you may pass a configuration object to the config method of Log facade and then call the channel method again to set up the configurations for the specified driver:

const config = { url: 'other-slack-webhook-url' }

// GOOD!! Configuration is now set for drivers.
Log.config(config).channel('slack').info('Hello from Athenna!')

// BAD!! Configuration are not going to be set for drivers.
Log.config(config).info('Hello from Athenna!')

Formatters

Available channel formatters

Each log channel is powered by a "formatter". The formatter determines how the log message is actually formatted. The following log channel formatters are available in every Athenna application. An entry for most of these formatters is already present in your application's config/logging.js configuration file, so be sure to review this file to become familiar with its contents:

NameDescription
cliA more simple formatter, very useful when building CLI's
simpleA formatter that only have the level, date, message and the MS
requestA formatter that focus in logging application http requests
messageA formatter that focus in logging for some messenger application such as discord

Writing using specific formatter

It is also possible to set formatter and formatterConfig in runtime configurations when using the Log facade:

import chalk from 'chalk'

const config = {
formatter: 'simple',
formatterConfig: { chalk: chalk.green },
}

Log.config(config).channel('slack').info('Hello from Athenna!')

Implementing your own driver

You can implement your own Log driver using the DriverFactory class, but your class needs to implement the transport method and extends Driver class:

import { Driver } from '@athenna/logger'

export class ConsoleLogDriver extends Driver {
/**
* Creates a new instance of ConsoleLogDriver.
*
* @param {any} configs
* @return {ConsoleLogDriver}
*/
constructor(configs) {
super(configs)
}

/**
* Transport the log.
*
* @param {string} level
* @param {any} message
* @return {any}
*/
transport(level, message) {
/**
* Verify if this log could be transported.
*/
if (!this.couldBeTransported(level)) {
return
}

let formatted = this.format(level, message)

if (this.driverConfig.addBar) {
formatted = formatted.concat('- BAR')
}

console.log(formatted)
}
}

Now we just need to use the DriverFactory to register our new driver and set a name for him:

import { DriverFactory } from '@athenna/logger'

DriverFactory.createDriver('consoleLog', ConsoleLogDriver)

Finally, we can start using our new driver in channels of config/logging.js:

channels: {
consoleLogChannel: {
driver: 'consoleLog',
addBar: true,

formatter: 'simple'
}
}

Implementing your own formatter

You can implement your own Log formatter using the FormatterFactory class, but your class needs to implement the format method and extends Formatter class:

import { Formatter } from '@athenna/logger'

export class ConsoleLogFormatter extends Formatter {
/**
* Format the message.
*
* @param {string} message
* @return {string}
*/
format(message) {
const level = this.simpleLevel()

let messageFormatted = this.clean(
`${level} - ${this.timestamp()} - (${this.pid()}) ${this.applyColors(
message,
)}`,
)

if (this.configs.addFoo) {
messageFormatted = messageFormatted.concat('- FOO')
}

return messageFormatted
}
}

Now we just need to use the FormatterFactory to register our new formatter and set a name for him:

import { FormatterFactory } from '@athenna/logger'

FormatterFactory.createFormatter('consoleLog', ConsoleLogFormatter)

Finally, we can start using our new formatter in channels of config/logging.js:

channels: {
consoleLogChannel: {
driver: 'consoleLog',
addBar: true,

formatter: 'consoleLog',
formatterConfig: {
addFoo: true,
}
}
}