|
2019-05-17
import log4js from 'log4js';
开发自定义 appender,向机器人输出日志
function robotAppender(layout, timezoneOffset) { return (loggingEvent) => { const logCtx = loggingEvent.context; // 如果日志等级在 error 以上,高级 if ((loggingEvent.level as Level).isGreaterThanOrEqualTo(levels.ERROR)) { // 调用机器人告警 sendAlert(`[${msgObj.level}]${projectName}`, { path: loggingEvent.context.path || '', // path ctx: ctxStr.length > ctxStrLimit ? requestDataStr : ctxStr, // ctx msg: (layout(loggingEvent, timezoneOffset) as string)?.slice(0, ctxStrLimit) || '', // 日志内容 trace: loggingEvent.context.trace || '', // trace_id }); return true; } }; }; export function wxConfigure(config: any, layouts: any) { let layout = layouts.colouredLayout; if (config.layout) { layout = layouts.layout(config.layout.type, config.layout); } return robotAppender(layout, config.timezoneOffset); } // 配置到 log4js log4js.configure({ appenders: { console: { type: 'console', }, // 企业微信机器人通知 wx: { type: { configure: wxConfigure }, layout: { type: 'basic' }, }, }, categories: { default: { appenders: ['console', 'wx'], level: 'debug' }, }, });
async function sendAlert(title: string, data: Record<string, any>, chatid?: string) { // 计算告警信息标识,取 msg 的前 100 字节 const msgId = getMsgId(data.msg); // 先判断有没有锁 const lockKey = `${msgId}_lock`; // 这里使用 ioredis,跳过 redisClient 的封装 const lock = await defaultRedisClient.get(lockKey); if (lock) { console.log('lock exsit, skip alert', title, data); return; } // 进行计数 let rawCounter = await defaultRedisClient.get(msgId); // 如果之前没有发送过,初始化 if (!rawCounter) { rawCounter = '0'; } const counter = parseInt(rawCounter, 10); // 如果已经发送 3次或以上,加锁,禁止此次发送 if (counter > 2) { // rm counter // 要先 rm,可以 rm 失败,下次还会进入告警计数 await defaultRedisClient.del(msgId); // add lock await defaultRedisClient.setex(lockKey, 1 * 24 * 60 * 60 * 1000, data?.trace); // 可以推送提示: // (`三次未处理告警: ${msgId} \n\n\n // 已终止该告警推送,24h 时后恢复! // `, undefined, chatid); return; } // 否则仅仅是计数加一,注意加过期时间 await defaultRedisClient.setex(msgId, 1 * 24 * 60 * 60 * 1000, String(counter + 1)); const copyedData = { env, ...data, }; let content = `### ${title} \n`; Object.keys(copyedData).forEach((key) => { content += `> **${key}**: <font color="comment">${copyedData[key]}</font> \n\n\n`; }); const msgObj = { chatid, msgtype: 'markdown', markdown: { content, // 注意这里:搜集反馈的按钮 attachments: [{ callback_id: 'alert_feedback', actions: [{ name: `reject_${data?.trace}`, text: '拒绝', type: 'button', // 这里使用 消息的标识:msg 的 前 100 字节 value: msgId, replace_text: '已拒绝', border_color: '2EAB49', text_color: '2EAB49', }, { name: `accept_${data?.trace}`, text: '接受', type: 'button', value: msgId, replace_text: '已接受', border_color: '2EAB49', text_color: '2EAB49', }, ], }, ], }, }; // url 为机器人回调地址 return axios.post(url, msgObj, { headers: { 'Content-Type': 'application/json', }, }); }
特别注意调用机器人接口传入的 attachments,可以为每个告警附加反馈按钮 ,效果:
{ name: `accept_${data?.trace}`, value: msgId, },
{ From: { UserId: 'xxxxxxx', Name: 'fjywan', Alias: 'fjywan' }, WebhookUrl: 'http://in.qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx', ChatId: 'xxxx', GetChatInfoUrl: 'http://in.qyapi.weixin.qq.com/cgi-bin/webhook/get_chat_info?code=xxxxx', MsgId: 'xxxxx', ChatType: 'group', MsgType: 'attachment', Attachment: { CallbackId: 'alert_feedback', Actions: { Name: 'accept-traceidxxx', Value: 'msgidxxxx', Type: 'button' } }, TriggerId: 'xxxx', }
下面处理这条消息:
function getLockKey(msgId: string) { return `${msgId}_lock`; } enum BugStatus { Created = 1, Processing = 2, Done = 3 } export async function alertFeedBack(payload: AttachmentMsg) { const { From: { Alias, }, Attachment: { Actions: { Name, Value, }, } } = payload; const lockKey = getLockKey(Value); const [actualName, trace] = Name.split('_'); // 如果存在 counter,先移除 await defaultRedisClient.del(Value); try { // 接受告警的处理 if (actualName === 'accept') { // 加不失效锁 await defaultRedisClient.setnx(lockKey, Name); const now = Date.now(); // 这里使用 ORM prisma 往 MYSQL 数据插一条 bug 数据 await prisma.bug_list.create({ data: { assign: Alias, trace, msgId: Value, status: BugStatus.Created, updatedAt: now, createdAt: now, }, }); } else { // 拒绝告警的处理 // redis 加锁,3天有效期,后面都不在提醒 // 如果推送连续三条,用户不处理,加锁一天 await defaultRedisClient.setex(lockKey, 3 * 24 * 60 * 60 * 1000, Name); } } catch (e) { console.error('执行加锁出错', e); } }
CREATE TABLE `bug_list` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `msgId` VARCHAR(191) NOT NULL, `trace` VARCHAR(60) NOT NULL, `assign` VARCHAR(30) NOT NULL, `status` TINYINT(2) NOT NULL, `remark` LONGTEXT, `updatedAt` BIGINT(20) NOT NULL, `createdAt` BIGINT(20) NOT NULL, PRIMARY KEY (`id`), unique key (msgId), unique key (trace) )
// 返回当前开发的 Bug 列表 export async function buglist(payload: WxMsg) { const { From: { Alias }, Text: { Content: raw }, ChatId } = payload; const title = `To: ${Alias}`; const result = await prisma.bug_list.findMany({ where: { assign: Alias, status: { in: [1, 2], }, }, }); if (!result.length) { // 回消息 sendBack(title, { 提示: '恭喜你名下没有待处理 Bug,继续保持!', }, ChatId); return; } // 生成 Bug 列表的消息体 let content = `### ${title} \n`; const attachments = [{ callback_id: 'bug_status_change', actions: [], }] as unknown as Attachments; result.forEach((one) => { content += `> **[全链路日志:${one.trace}](xxxx)**: <font color="comment">${one.msgId}</font> \n\n\n`; // important: 这里为每个 Bug 单生成对应处理按钮 attachments[0].actions.push({ name: String(one.id), text: one.status === 1 ? `${one.id}:转为处理中` : `${one.id}:关单`, type: 'button', // 这里使用 消息的标识:msg 的 前 100 字节 value: one.status === 1 ? '2' : '3', replace_text: one.status === 1 ? '处理中' : '处理完成', border_color: '2EAB49', text_color: '2EAB49', }); }); sendBack(content, attachments, ChatId); }
当 @ 机器人时,效果如下:
export async function bugStatusChange(payload: AttachmentMsg) { const { From: { Alias, }, Attachment: { Actions: { Name, Value, }, } } = payload; try { const theBug = await prisma.bug_list.update({ data: { status: parseInt(Value, 10), }, where: { id: parseInt(Name, 10), }, }); // 移除锁 const { msgId } = theBug; const lockKey = getLockKey(msgId); defaultRedisClient.del(lockKey); } catch (e) { console.error('更新 bug 状态出错', e); } }
效果如下:
编辑:航网科技 来源:腾讯云 本文版权归原作者所有 转载请注明出处
微信扫一扫咨询客服
全国免费服务热线
0755-36300002