@nivinjoseph/n-log
Version:
Logging framework
322 lines (277 loc) • 10.6 kB
text/typescript
import { given } from "@nivinjoseph/n-defensive";
import { Exception } from "@nivinjoseph/n-exception";
import { Disposable, Duration } from "@nivinjoseph/n-util";
import Slack from "@slack/bolt";
import { StringIndexed } from "@slack/bolt/dist/types/helpers.js";
import { BaseLogger } from "./base-logger.js";
import { LogRecord } from "./log-record.js";
import { Logger } from "./logger.js";
import { LoggerConfig } from "./logger-config.js";
export type SlackLoggerConfig = Pick<LoggerConfig, "logDateTimeZone" | "logInjector"> & {
slackBotToken: string;
slackBotChannel: string;
slackUserName?: string;
slackUserImage?: string;
filter?: ReadonlyArray<"Info" | "Warn" | "Error">;
logFilter?(record: LogRecord): boolean;
fallback?: Logger;
};
export class SlackLogger extends BaseLogger implements Disposable
{
private readonly _includeInfo: boolean;
private readonly _includeWarn: boolean;
private readonly _includeError: boolean;
private readonly _logFilter: (record: LogRecord) => boolean;
private readonly _fallbackLogger: Logger | null;
private readonly _app: Slack.App;
private readonly _channel: string;
private readonly _userName: string;
private readonly _userImage: string = ":robot_face:";
private readonly _userImageIsEmoji: boolean;
private _messages = new Array<SlackMessage>();
private readonly _timer: NodeJS.Timeout;
private _isDisposed = false;
private _disposePromise: Promise<void> | null = null;
public constructor(config: SlackLoggerConfig)
{
super(config);
// eslint-disable-next-line @typescript-eslint/unbound-method
const { slackBotToken, slackBotChannel, slackUserName, slackUserImage, logFilter } = config;
given(slackBotToken, "slackBotToken").ensureHasValue().ensureIsString();
this._app = new Slack.App({
receiver: new DummyReceiver(),
token: slackBotToken
});
given(slackBotChannel, "slackBotChannel").ensureHasValue().ensureIsString();
this._channel = slackBotChannel;
given(slackUserName, "slackUserName").ensureIsString();
if (slackUserName != null && slackUserName.isNotEmptyOrWhiteSpace())
this._userName = slackUserName;
else
this._userName = this.service;
given(slackUserImage, "slackUserImage").ensureIsString();
if (slackUserImage != null && slackUserImage.isNotEmptyOrWhiteSpace())
this._userImage = slackUserImage.trim();
this._userImageIsEmoji = this._userImage.startsWith(":") && this._userImage.endsWith(":");
const allFilters = ["Info", "Warn", "Error"];
const filter = config.filter ?? allFilters;
given(filter, "filter").ensureIsArray().ensure(t => t.every(u => allFilters.contains(u)));
this._includeInfo = filter.contains("Info");
this._includeWarn = filter.contains("Warn");
this._includeError = filter.contains("Error");
given(logFilter, "logFilter").ensureIsFunction();
this._logFilter = logFilter ?? ((_: LogRecord): boolean => true);
this._fallbackLogger = config.fallback ?? null;
this._timer = setInterval(() =>
{
this._flushMessages()
.catch(e => this._fallbackLogger?.logError(e).catch(e => console.error(e)) ?? console.error(e));
}, Duration.fromSeconds(30).toMilliSeconds());
}
public async logDebug(debug: string): Promise<void>
{
if (this.env === "dev")
{
let log: SlackMessage = {
source: this.source,
service: this.service,
env: this.env,
level: "Debug",
message: debug,
dateTime: this.getDateTime(),
time: new Date().toISOString(),
color: "#F8F8F8"
};
if (this.logInjector)
log = this.logInjector(log) as SlackMessage;
this._messages.push(log);
}
}
public async logInfo(info: string): Promise<void>
{
if (!this._includeInfo)
return;
let log: SlackMessage = {
source: this.source,
service: this.service,
env: this.env,
level: "Info",
message: info,
dateTime: this.getDateTime(),
time: new Date().toISOString(),
color: "#259D2F"
};
if (!this._logFilter(log))
return;
if (this.logInjector)
log = this.logInjector(log) as SlackMessage;
this._messages.push(log);
}
public async logWarning(warning: string | Exception): Promise<void>
{
if (!this._includeWarn)
return;
let log: SlackMessage = {
source: this.source,
service: this.service,
env: this.env,
level: "Warn",
message: this.getErrorMessage(warning),
dateTime: this.getDateTime(),
time: new Date().toISOString(),
color: "#F1AB2A"
};
if (!this._logFilter(log))
return;
if (this.logInjector)
log = this.logInjector(log) as SlackMessage;
this._messages.push(log);
}
public async logError(error: string | Exception): Promise<void>
{
if (!this._includeError)
return;
let log: SlackMessage = {
source: this.source,
service: this.service,
env: this.env,
level: "Error",
message: this.getErrorMessage(error),
dateTime: this.getDateTime(),
time: new Date().toISOString(),
color: "#EF401D"
};
if (!this._logFilter(log))
return;
if (this.logInjector)
log = this.logInjector(log) as SlackMessage;
this._messages.push(log);
}
public dispose(): Promise<void>
{
if (!this._isDisposed)
{
this._isDisposed = true;
clearInterval(this._timer);
this._disposePromise = this._flushMessages();
}
return this._disposePromise!;
}
private async _flushMessages(): Promise<void>
{
if (this._messages.isEmpty)
return;
const messagesToFlush = this._messages;
this._messages = new Array<SlackMessage>();
await this._postMessages(messagesToFlush);
}
private async _postMessages(messages: ReadonlyArray<SlackMessage>): Promise<void>
{
try
{
await this._app.client.chat.postMessage({
username: this._userName,
icon_emoji: this._userImageIsEmoji ? this._userImage : undefined,
icon_url: !this._userImageIsEmoji ? this._userImage : undefined,
channel: this._channel,
text: `${this.service} [${this.env}]`,
attachments: messages.map(log =>
{
return {
color: log.color,
blocks: [
{
type: "section",
text: {
type: "plain_text",
text: log.message
}
},
{
type: "context",
elements: [{
type: "plain_text",
text: log.dateTime
}]
}
]
};
})
});
}
catch (error)
{
if (this._fallbackLogger != null)
{
await this._fallbackLogger.logWarning("Error while posting to slack.");
await this._fallbackLogger.logError(error as any);
await this._fallbackLogger.logWarning("Original messages below");
await messages.forEachAsync(async log =>
{
switch (log.level)
{
case "Debug":
await this._fallbackLogger!.logDebug(log.message);
break;
case "Info":
await this._fallbackLogger!.logInfo(log.message);
break;
case "Warn":
await this._fallbackLogger!.logWarning(log.message);
break;
case "Error":
await this._fallbackLogger!.logError(log.message);
break;
default:
await this._fallbackLogger!.logError(log.message);
}
}, 1);
}
else
{
console.warn("Error while posting to slack.");
console.error(error as any);
console.warn("Original messages below");
messages.forEach(log =>
{
switch (log.level)
{
case "Debug":
console.info(log.message);
break;
case "Info":
console.info(log.message);
break;
case "Warn":
console.warn(log.message);
break;
case "Error":
console.error(log.message);
break;
default:
console.error(log.message);
}
});
}
}
}
}
type SlackMessage = LogRecord & { color: string; };
class DummyReceiver implements Slack.Receiver
{
// @ts-expect-error: not used atm
public init(app: App<StringIndexed>): void
{
// no-op
}
// @ts-expect-error: not used atm
public start(...args: Array<any>): Promise<unknown>
{
return Promise.resolve();
}
// @ts-expect-error: not used atm
public stop(...args: Array<any>): Promise<unknown>
{
return Promise.resolve();
}
}