/// import { inspect } from 'util' import { TypeGuards } from '@codeleap/types' import { LoggerConfig } from '../types' type EchoSlack = { label: string data: object options?: EchoSlackOptions module?: string } type OptionInclude = 'version' type SendIn = 'debug' | 'release' type EchoSlackOptions = { sendIn?: SendIn[] include?: OptionInclude[] } const DEFAULT_CHANNEL = '#_dev_logs' const DEFAULT_BASE_URL = 'https://slack.com/api/chat.postMessage' /** * Sends structured log messages to a Slack channel via the Slack Web API. * * The service is inert until {@link setApi} is called with an HTTP client. Messages are * silently suppressed when the API client is absent, `echoConfig.enabled` is `false`, or * the current environment does not match the caller-supplied `sendIn` filter. */ export class SlackService { private echoConfig: LoggerConfig['Slack']['echo'] private isDev: LoggerConfig['Environment']['IsDev'] private appName: LoggerConfig['AppName'] private api: any constructor(private config: LoggerConfig) { this.echoConfig = config.Slack.echo this.isDev = config.Environment.IsDev this.appName = config.AppName } /** * Registers the HTTP client used to POST messages to Slack. Must be called before `echo` * can send anything; calling `echo` without a registered API is a no-op. */ setApi(fetcher: any) { this.api = fetcher } /** * Posts a labelled object to Slack. The message is formatted with `util.inspect` so nested * structures are human-readable in the channel. * * Delivery is conditional on three independent guards: * - `options.sendIn` — restricts to `'debug'` (IsDev) or `'release'` (production) builds; * omitting `sendIn` sends in both environments. * - `echoConfig.enabled` — master switch in the config; defaults to `true` when omitted. * - A registered API client (see {@link setApi}). * * Failures are caught and logged to `console.error` rather than propagated. */ async echo( label: EchoSlack['label'], slackData: EchoSlack['data'], moduleName: EchoSlack['module'] = undefined, messageOptions: EchoSlack['options'] = {} ) { const options = this.parseOptions(messageOptions) const slack = this.parseData(label, slackData, options.info, moduleName) const enabled = TypeGuards.isBoolean(this.echoConfig.enabled) ? this.echoConfig.enabled : true if (!options.send || !this.api || !this.echoConfig || !enabled) return const settingsData = this?.echoConfig?.options ?? {} try { const data = { 'channel': this?.echoConfig?.channel ?? DEFAULT_CHANNEL, 'text': slack, 'username': `${this.appName} Log`, 'icon_url': this?.echoConfig?.icon, ...settingsData, } await this.api.post('', data, { baseURL: this?.echoConfig?.baseURL ?? DEFAULT_BASE_URL, headers: { Authorization: `Bearer ${this?.echoConfig?.token}`, }, }) } catch (err) { console.error('Failed to echo', err, 'logger echoSlack') } } private serializers: Record string> = { version: (IsDev: boolean) => { return IsDev ? 'debug' : 'release' }, } private parseOptions(options: EchoSlackOptions) { const { sendIn = [], include = [], } = options const hasSendIn = sendIn.length >= 1 const isDebug = hasSendIn ? sendIn.includes('debug') : true const isRelease = hasSendIn ? sendIn.includes('release') : true if (!isDebug && this.isDev || !isRelease && !this.isDev) { return { info: '', send: false, } } let str = '' const separator = ' - ' include.forEach(k => { const data = this.serializers[k]?.(this.isDev) str = `${str}${str.length > 0 ? separator : ''}[${data}]` }) return { info: str, send: true, } } private parseData(label: string, data: object, info: string, module?: string) { const obj = !info ? data : { ...data, info, } const args = [`${!module ? '' : `(${module}) `}${label}: `, obj] const slack = args.map(i => { if (typeof i === 'object') { try { return inspect(i, { depth: 5, compact: false, showHidden: true, }) } catch (e) { return `${i} (Unserializable value)` } } return String(i) }).join(' ') return slack } }