///
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
}
}