@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
157 lines (154 loc) • 6.09 kB
JavaScript
/**
* @package @bitrix24/b24jssdk
* @version 2.2.0
* @copyright (c) 2026 Bitrix24
* @license MIT
* @see https://github.com/bitrix24/b24jssdk
* @see https://bitrix24.github.io/b24jssdk/
*/
import { LogLevel } from '../types/logger.mjs';
import { AbstractLogger } from './abstract-logger.mjs';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
class Logger extends AbstractLogger {
static {
__name(this, "Logger");
}
channel;
handlers = [];
processors = [];
constructor(channel) {
super();
this.channel = channel;
}
// region static methods for creation ////
static create(channel) {
return new Logger(channel);
}
// endregion ////
// region config ////
pushHandler(handler) {
this.handlers.push(handler);
return this;
}
popHandler() {
return this.handlers.pop() || null;
}
setHandlers(handlers) {
this.handlers = handlers;
return this;
}
pushProcessor(processor) {
this.processors.push(processor);
return this;
}
// endregion ////
/**
* **Never throws and never rejects.** Logging is a side channel: a failure in
* it must degrade observability, not the operation being observed. Every
* callsite in the SDK invokes this without `await` (`this.getLogger().info(…)`
* as a statement), so a rejected promise would surface as an *unhandled
* rejection* — which terminates the Node process by default. A handler doing
* network or file I/O (Telegram, a stream, a third-party adapter) rejects for
* ordinary operational reasons, so that path is reachable in normal operation,
* not just in principle (#346).
*
* A processor or handler that fails is skipped and reported via
* {@link reportLoggingFailure}; the remaining handlers still receive the
* record.
*
* This covers failures *inside* the logger. It does not cover an exception
* raised while a caller builds its log arguments — those are evaluated eagerly
* at the callsite, before `log()` is reached (see `truncateForLog`, #338).
*
* ### Deliberately outside this guarantee
*
* Three gaps sit outside `log()` and were each weighed and left open on
* purpose (#346). They are recorded here so they are not re-opened as
* oversights:
*
* 1. **A third-party `LoggerInterface` is not isolated.** This guarantee
* belongs to this class, not to the interface. Every SDK callsite is
* written `…info(…).catch(() => {})`, which absorbs a *rejected promise*;
* an implementation that throws *synchronously*, before returning one,
* escapes into the caller. `setLogger(...)` warns about the shape it can
* check without calling anything (see `warnOnNonPromiseLogger`); returning
* promises is the implementor's side of the contract. Wrapping every
* installed logger defensively was considered and rejected: it would make
* the SDK responsible for code it does not own, on every one of ~94
* callsites, to cover a case TypeScript already rejects at compile time.
*
* 2. **A handler that fails forever is never detached.** Each failure is
* reported, every time — see {@link reportLoggingFailure}. Auto-detaching
* after N failures was considered and rejected: it silently changes a
* configuration the application made, and "N failures" is a policy the SDK
* has no basis to pick on the application's behalf.
*
* 3. **The synchronous half — argument construction — stays the caller's.**
* Making it total would mean wrapping the argument list at every callsite,
* which trades a narrow, findable failure (#338 was one expression in one
* helper) for noise at every call. Individual helpers on the hot path are
* made total instead, as `truncateForLog` was.
*
* @inheritDoc
*/
async log(level, message, context) {
const record = {
channel: this.channel,
level,
levelName: LogLevel[level],
message,
context: context ?? {},
extra: {},
timestamp: /* @__PURE__ */ new Date()
};
let processedRecord = record;
for (const processor of this.processors) {
try {
processedRecord = processor(processedRecord);
} catch (error) {
this.reportLoggingFailure(processor, error);
}
}
for (const handler of this.handlers) {
try {
if (!handler.isHandling(level)) {
continue;
}
const handled = await handler.handle(processedRecord);
if (handled && !handler.shouldBubble()) {
break;
}
} catch (error) {
this.reportLoggingFailure(handler, error);
}
}
}
/**
* Report a processor/handler that threw.
*
* Reported on every failure, deliberately: suppressing repeats would hide how
* often a sink is failing, and a sink that has been broken for an hour looks
* identical to one that failed once. The volume is the signal — if it is
* noisy, the sink is failing that often. Filtering belongs to whoever reads
* the output, not to the SDK.
*
* `console` is used rather than the logger — routing a logging failure back
* through the logger that just failed is how this turns into recursion.
*
* The handler is **not** detached, however many times it fails. Doing so would
* silently discard part of a configuration the application built, and the
* threshold that would trigger it is a policy call the SDK cannot make for the
* application. A sink that is broken stays wired and stays loud; whoever reads
* the output decides what to do about it (#346).
*/
reportLoggingFailure(source, error) {
const name = source?.constructor?.name ?? "processor";
console.warn(
`[b24jssdk] logger channel "${this.channel}": ${name} failed; the record was skipped. Logging continues through the remaining handlers, and the operation being logged is unaffected.`,
error
);
}
}
export { Logger };
//# sourceMappingURL=logger.mjs.map