Logging
Introduction
Configuration
Writing log messages
Formatters
Implementing your own driver
Implementing your own formatter
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:
Name | Description |
---|---|
stack | A wrapper to facilitate creating "multi-channel" channels |
console | A driver that transports the log to the stdout/stderr using process.stdout.write |
file | A driver that transports the log to a file |
null | A driver that discards all log messages |
slack | A driver that transports the log message to a slack channel |
discord | A driver that transports the log message to a discord channel |
telegram | A 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:
Name | Description |
---|---|
cli | A more simple formatter, very useful when building CLI's |
simple | A formatter that only have the level, date, message and the MS |
request | A formatter that focus in logging application http requests |
message | A 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,
}
}
}