App Logs

Prev Next

App logs can be viewed from the app’s Logs tab. Here, the app’s activities are displayed, allowing admins to analyze what happened and debug issues. For an overview of the types of logs available, refer to the App Development Lifecycle document.

In this document, we will learn how to:

  • Access the logger correctly.

  • Structure slash commands, endpoints, and helper classes.

  • Handle the special case of scheduler processors.

How to access the logger

The ILogger interface is used to log events at different levels to the system and the database. It is automatically imported into your app’s main class. Refer to the Rocket.Chat Apps TypeScript Definition for details on the interface’s methods and properties.

Let’s learn how to access the logger in different cases.

Rocket.Chat version 8.2.0 fixes an issue where app logs were lost in nested requests. The PR #38374 ensures that the logger is persisted throughout the request handling with no mix-ups. This creates some requirements for app developers that should be kept in mind. This document guides you on how to access the logs correctly.

Inside the App subclass

The following code snippet shows you how to access the logger:

import {
    IAppAccessors,
    ILogger,
} from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';

export class HelloWorldApp extends App {
    constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
        super(info, logger, accessors);
        this.getLogger().debug('some_message', extraData);
        this.getLogger().error('something_went_wrong', { err });
    }
}

Use this.getLogger() from within your App subclass. Call the logger only when you need it. For example:

// ✅ Recommended
export class MyApp extends App {
	public async initialize(/* ... */) {
		this.getLogger().debug('init');
	}
}

The engine will provide a correctly scoped logger for every request.

Do not cache the logger across executions as shown in the following code snippet:

// ❌ Anti‑pattern: caching the logger instance
export class MyApp extends App {
	private logger = this.getLogger();

	public async initialize(/* ... */) {
		// This may use a logger from a different request context
		this.logger.debug('init');
	}
}

Inside external classes

External classes refer to the classes that don’t extend App, such as slash commands, API endpoints, providers, and helpers. In these classes, the logger must be called in the following way:

  • Keep a reference to the app object internally.

  • Use that reference to obtain the logger.

The following sample code snippet shows the recommended pattern:

// ✅ Recommended
import type { App } from '@rocket.chat/apps-engine/definition/App';
import type { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import type { ISlashCommand, ISlashCommandPreview, ISlashCommandPreviewItem, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands';

export class MySlashcommand implements ISlashCommand {
	/* Required properties omitted for brevity */

	constructor(private readonly app: App) {} // This is the reference

	public async executor(context: SlashCommandContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise<void> {
		this.app.getLogger().debug('Hello!'); // Here the reference is called for logging
	}
}

Here, MySlashcommand is a separate class that implements ISlashCommand. It does not extend App. To reach the logger, it uses a reference to the app instance it stores internally. Keeping the app reference (this.app) allows the runtime to replace that app with a request-scoped proxy when executing your code, so that this.app.getLogger() always maps to the right logger.

Do not capture the logger itself at the time of construction as shown in the following example:

// ❌ Anti‑pattern
export class MySlashcommand implements ISlashCommand {
	/* Required properties omitted for brevity */
	private logger: ILogger;

	constructor(app: App) {
		this.logger = app.getLogger();
	}

	public async executor(context: SlashCommandContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise<void> {
		this.app.getLogger().debug('Hello!');
	}
}

Endpoints, outbound providers, and other composable objects

Any “provider” object that the engine executes for you (API endpoints, slash command classes, outbound communication providers, etc.) should follow the same pattern:

  • Keep a reference to the app.

  • Log via this.app.getLogger().

The following code snippet shows an example:

import type {
	ApiEndpoint,
	IApiRequest,
	IApiResponse,
} from '@rocket.chat/apps-engine/definition/api';
import type { App } from '@rocket.chat/apps-engine/definition/App';

export class MyEndpoint implements ApiEndpoint {
	public path = 'my-endpoint';

	constructor(private readonly app: App) {} // The reference to App

	public async get(request: IApiRequest): Promise<IApiResponse> {
		this.app.getLogger().debug('my_endpoint_get_called', { query: request.query }); // The reference is called for logging

		return {
			status: 200,
			content: { ok: true },
		};
	}
}

Scheduler processors

Scheduler processors are a special case for logging because of how they are registered. Take a look at the following example:

public async extendConfiguration(configuration: IConfigurationExtend) {
	configuration.scheduler.registerProcessors([
		{
			id: 'first',
			// `this` is lexically bound at definition time, not set by the runtime
			processor: async (jobData) => this.getLogger().debug(`[${ new Date() }] this is a task`, jobData),
		},
	]);
}

It is common to register the processor function as an arrow function. But this poses a challenge for us to inject the correct app reference to its execution. In this case, you have two options for logging:

  1. Register a reference to a method inside your App class without binding this. This is the most ergonomic option. Refer to the following code sample:

export class SimpleMessageEndpointApp extends App {
	public async extendConfiguration(configuration: IConfigurationExtend) {
		configuration.scheduler.registerProcessors([
			{
				id: 'MyProcessor',
				// NOTE: make sure NOT TO `.bind(this)` !
				processor: this.myProcessorHandler,
			},
		]);
	}

	private async myProcessorHandler(jobData): Promise<void> {
		this.getLogger().debug('Hello there!')
	}
}
  1. The second option is to register a regular function, either named or anonymous, and use this as a reference to your App instance (which the runtime injects later on). While this is possible, it may be confusing for eventual maintenance. Refer to the following code sample:

public async extendConfiguration(configuration: IConfigurationExtend) {
	configuration.scheduler.registerProcessors([
		{
			id: 'MyProcessor',
			processor: async function MyProcessorHandler(jobData) {
				this.getLogger().debug(`[${ new Date() }] this is a task`, jobData);
			},
		},
	]);
}