Logging Levels
Logging levels are arranged in increasing order of severity — from detailed debugging (0) to critical failures (7). Filtering works on an inclusive principle: when a certain level is set, messages of that level and all higher levels are logged.
How Level Filtering Works
import { Logger, ConsoleHandler, LogLevel } from '@bitrix24/b24jssdk'
// Example 1: ERROR level
const $logger = new Logger('app');
$logger.pushHandler(new ConsoleHandler(LogLevel.ERROR));
// These logs WILL be output:
$logger.error('Loading error', {}); // ✓ level 4 >= 4
$logger.critical('Critical error'); // ✓ level 5 >= 4
$logger.alert('Alert'); // ✓ level 6 >= 4
$logger.emergency('Emergency'); // ✓ level 7 >= 4
// These logs WILL NOT be output:
$logger.debug('Debug'); // ✗ level 0 < 4
$logger.info('Info'); // ✗ level 1 < 4
$logger.warning('Warning'); // ✗ level 3 < 4
Main Types and Interfaces
LogRecord Log record structure:
{
channel: string, // Logger channel name
level: LogLevel, // Numeric level
levelName: LogLevelName, // Level name (e.g., 'DEBUG')
message: string, // Message text
context: Record<string, any>, // Contextual data
extra: Record<string, any>, // Additional data (added by processors)
timestamp: Date // Timestamp
}
LoggerInterface Main logger interface:
log(level: LogLevel, message: string, context?: Record<string, any>): Promise<void>
debug(message: string, context?: Record<string, any>): Promise<void>
info(message: string, context?: Record<string, any>): Promise<void>
notice(message: string, context?: Record<string, any>): Promise<void>
warning(message: string, context?: Record<string, any>): Promise<void>
error(message: string, context?: Record<string, any>): Promise<void>
critical(message: string, context?: Record<string, any>): Promise<void>
alert(message: string, context?: Record<string, any>): Promise<void>
emergency(message: string, context?: Record<string, any>): Promise<void>
Handler Log handler:
handle(record: LogRecord): Promise<boolean> // Process record
isHandling(level: LogLevel): boolean // Check level support
shouldBubble(): boolean // Whether to continue chain
setFormatter(formatter: Formatter): void // Set formatter
getFormatter(): Formatter | null // Get formatter
Formatter for converting LogRecord:
format(record: LogRecord): any
Processor for modifying LogRecord:
(record: LogRecord) => LogRecord
Main Classes
AbstractLogger Abstract base class implementing convenience methods (debug, info, etc.).
Logger Main logger implementation:
constructor(channel: string) // Create logger with specified channel
pushHandler(handler: Handler): this // Add handler
popHandler(): Handler | null // Remove last handler
setHandlers(handlers: Handler[]): this // Set list of handlers
pushProcessor(processor: Processor): this // Add processor
log(level, message, context): Promise<void> // Main logging method
NullLogger Stub for cases when logging is not required. Performs no operations.
Logger Factory
LoggerFactory Static methods for creating loggers:
createNullLogger(): LoggerInterface
createForBrowser(channel: string, isDevMode: boolean = false): LoggerInterface
createForBrowserDevelopment(channel: string, level: LogLevel = LogLevel.DEBUG): LoggerInterface
createForBrowserProduction(channel: string, level: LogLevel = LogLevel.ERROR): LoggerInterface
forcedLog(logger, action, message, context): Promise<void> // Forced logging
Processors
Processors modify log records before processing:
Built-in processors:
memoryUsageProcessor- adds memory usagepidProcessor- adds process ID
Creating a custom processor:
import { Processor } from '@bitrix24/b24jssdk'
const customProcessor: Processor = (record) => {
record.extra.customField = 'value'
return record
}
Formatters
AbstractFormatter Base formatter class with date formatting support.
LineFormatter Formats logs into a string with template support:
// Default: '[{channel}] {levelName}: {message} {context} {extra} {date}'
new LineFormatter(formatString, dateFormat)
Available placeholders:
{channel}- channel name{levelName}- level name{message}- message{context}- context in JSON{extra}- extra data in JSON{timestamp}- unix timestamp{date}- date in specified format
JsonFormatter: Formats logs into a JSON string.
TelegramFormatter: Formats a log entry for sending to Telegram. Supports HTML markup with escaped special characters.
Handlers
AbstractHandler Base handler class with logging level and bubbling support.
ConsoleHandler Outputs logs to browser console with styling support for different levels.
ConsoleV2Handler Improved version of ConsoleHandler with more readable output.
MemoryHandler Stores logs in memory (useful for testing and debugging):
import { MemoryHandler, LogLevel } from '@bitrix24/b24jssdk'
const handler = new MemoryHandler(LogLevel.DEBUG, { limit: 1000 })
const records = handler.getRecords() // Get all records
handler.clear() // Clear records
ConsolaAdapter Adapter for Consola.
WinstonAdapter Adapter for Winston.
StreamHandler Node.js stream handler for writing logs to streams.
TelegramHandlerSends logs to Telegram chat. The browser displays a warning in the console. In server-side, sends a message via the Telegram Bot API.
Usage
Basic Example
import { LoggerFactory } from '@bitrix24/b24jssdk'
const devMode = !!(typeof import.meta !== 'undefined' && (import.meta.dev || import.meta.env?.DEV))
const $logger = LoggerFactory.createForBrowser('Example:getCrmItem', devMode)
$logger.info('User logged in', { userId: 123 })
Advanced Configuration
import { Logger, ConsoleV2Handler, LineFormatter, LogLevel, memoryUsageProcessor, pidProcessor } from '@bitrix24/b24jssdk'
const $logger = new Logger('app')
const handler = new ConsoleV2Handler(LogLevel.DEBUG)
handler.setFormatter(new LineFormatter('[{levelName}] {message}'))
$logger
.pushHandler(handler)
.pushProcessor(memoryUsageProcessor)
.pushProcessor(pidProcessor)
$logger.warning('Low memory', { freeMemory: '10MB' })
Creating a Custom Handler
// @check-ignore: AbstractHandler is not re-exported from the SDK main index
import { AbstractHandler, LogRecord } from '@bitrix24/b24jssdk'
class CustomHandler extends AbstractHandler {
async handle(record: LogRecord): Promise<boolean> {
// Send to server, write to file, etc.
return true // Returns true if record was processed
}
}
Features
- Asynchronous: All logging methods return a Promise.
- Chain of responsibility: Handlers are called sequentially.
- Bubbling: If a handler returns
trueandshouldBubble() === false, the chain is interrupted. - Channels: Logs are grouped by channels for filtering.
- Context: Support for structured data via the
contextparameter. - Isolated from your code: a failing handler or processor cannot break the operation being logged — see below.
Failure isolation
Logger.log() never throws and never rejects. A handler or processor that fails
is skipped, and the remaining handlers still receive the record.
This matters because logging is fire-and-forget. The SDK calls the logger as a
bare statement, without await — this.getLogger().info('post/response', { … }).
An unawaited promise that rejects is an unhandled rejection, and Node
terminates the process on those by default. Handlers do real I/O — Telegram, a
writable stream, a third-party adapter — so rejection is an ordinary operational
event: an unreachable endpoint, a closed stream, a full disk. Without isolation,
a Telegram outage could take down a server-side app that merely wired the
handler.
Every failure is reported to console.warn — not deduplicated, so how often a
sink is failing stays visible. The report goes to console rather than through
the logger, since routing a logging failure back into the logger that just
failed is how it becomes recursion.
One limit worth knowing: isolation covers failures inside the logger, not the
arguments you build for it. Arguments are evaluated at the callsite before
log() is reached, so an expression that throws while assembling context
still throws in your code.
Writing your own logger
If you pass your own implementation to setLogger(...), every logging method
must return a promise. This is not a style preference — the SDK depends on it.
Each of the SDK's ~90 logging callsites is written as fire-and-forget, and says
so explicitly — this.getLogger().info('post/response', { … }).catch(() => {}).
The trailing .catch(() => {}) is what declares "the outcome does not matter here".
Without it an unawaited rejection becomes an unhandled rejection, which Node
terminates the process on by default. But .catch only exists on a promise: a
method returning undefined raises TypeError: ... .catch is not a function at
every logging callsite in the SDK — turning a logging detail into a broken
operation.
TypeScript enforces this for you, since LoggerInterface declares every level as
Promise<void>. The risk is in plain JavaScript, or behind an as any:
// Correct — an async method always returns a promise, even if it throws inside
class MyLogger {
async info(message: string, context?: Record<string, any>) {
await fetch('https://logs.example.com/ingest', {
method: 'POST',
body: JSON.stringify({ message, context })
})
}
// ...and every other level
}
setLogger(...) runs a shallow check when you install a logger and warns to
console if a level is missing or is not callable. It deliberately does not call
your methods to inspect what they return — doing so would emit spurious log
records just from installing the logger — so a method that exists but returns a
non-promise is not caught. Returning promises is your side of the contract.
Usage Recommendations
Development
- Use
LoggerFactory.createForBrowserDevelopmentwith levelDEBUG - Add processors for debugging information
Production
- Use
LoggerFactory.createForBrowserProductionwith levelERRORor higher - Limit the number of handlers for performance
- Consider creating custom handlers for sending logs to server
Context
// @check-ignore: illustrative snippet — orderId, userId, error are not declared
// Good:
$logger.error('Failed to process order', {
orderId: 123,
userId: 456,
error: error.message
})
// Bad:
$logger.error(`Failed to process order ${orderId} for user ${userId}: ${error.message}`)