v2.1.0

Logger

Logger inspired by PHP Monolog, provides a structured logging system with support for channels, handlers, processors, and formatters. Implementation follows PSR-3 principles and the chain of responsibility pattern.

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.

LevelValueWhen to UsePractical Example
DEBUG0Detailed debugging information for developers
Default in development
Loaded 1523 records in 45ms
INFO1Information about normal application operation
Tracking business logic
User #123 added item to cart
NOTICE2Important but non-critical events
Successful operations with consequences
System settings changed by administrator
WARNING3Potential issues
Application works but requires attention
Cache almost full (95%)
ERROR4Runtime errors requiring intervention
Part of functionality unavailable
Failed to connect to database
CRITICAL5Critical component failures
Require urgent intervention during working hours
Payment gateway unavailable for more than 5 minutes
ALERT6Serious problems requiring immediate resolutionDisk space exhausted at 99%
EMERGENCY7System inoperable
Highest level of urgency
Server farm completely unavailable

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 usage
  • pidProcessor - 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

  1. Asynchronous: All logging methods return a Promise.
  2. Chain of responsibility: Handlers are called sequentially.
  3. Bubbling: If a handler returns true and shouldBubble() === false, the chain is interrupted.
  4. Channels: Logs are grouped by channels for filtering.
  5. Context: Support for structured data via the context parameter.
  6. 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 awaitthis.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.createForBrowserDevelopment with level DEBUG
  • Add processors for debugging information

Production

  • Use LoggerFactory.createForBrowserProduction with level ERROR or 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}`)