用 manager-node 拉云函数日志,失败统一推企业微信
一句话定义:用一个监控用的云函数,定时调
@cloudbase/manager-node的getFunctionLogsV2拉每个目标函数过去 5 分钟的调用日志,把RetCode != 200的视为失败,过阈值 + 去重后推送到企业微信群机器人。预计耗时:40 分钟 | 难度:进阶
适用场景
这篇和 batch 里前两篇运维 recipe 是不同切面,先把分工讲清:
| 场景 | recipe |
|---|---|
| 怎么把消息推到企业微信(基础推送层) | connect-wecom-webhook-cloud-function |
| 单个 cron 任务自己 try/catch,失败时告警 | schedule-cloud-function-cron-job 第五步 |
| 从外部主动拉所有函数的日志,做全局错误监控 | 本篇 |
适用:
- 想要一个「全局监视器」,不用每个云函数都改代码就能监控错误
- 已经有企业微信群,熟悉 webhook 推送流程
- 函数数量在十几个到几十个之间(几百个的话需要再做分组)
不适用:
- 单个函数自己处理失败逻辑就够了的场景。改自己的 try/catch 比拉日志成本低
- 需要秒级实时告警。本方案是分钟级轮询,不是流式
- 想监控函数性能 / 慢日志。这篇只看 RetCode,慢日志用控制台或 searchClsLog 单独查
环境要 求
| 依赖 | 版本 |
|---|---|
@cloudbase/manager-node | ≥ 5.0.0 |
@cloudbase/node-sdk | 3.18.1(写去重状态用) |
@cloudbase/cli | latest |
| Node.js(云函数运行时) | ≥ 16.13 |
另外需要:
- 已部署 connect-wecom-webhook-cloud-function,企业微信 webhook 已通
- 已配置过 schedule-cloud-function-cron-job,熟悉 7 字段 cron 表达式
- 在腾讯云控制台「访问管理 / API 密钥」创建一对
secretId/secretKey,这是 manager-node 鉴权用的,和云开发的 apiKey 不是一回事 - 控制台 → 云开发 → 高级 → CLS 日志服务 已开启(没开的话先
app.log.createLogService()一次)
第一步:定位 manager-node 的日志接口
manager-node 提供两条路:
functions.getFunctionLogsV2({ name, startTime, endTime }):按函数名拉调用日志列表,返回每条调用的RequestId / RetCode / StartTime。要看具体日志正文还要再调一次getFunctionLogDetailapp.log.searchClsLog({ queryString, ... }): 走 CLS 日志服务的搜索,可以跨函数检索,适合复杂条件
本篇用第一种方式,逻辑直观,适合按函数轮询的场景。完整签名见 getFunctionLogsV2。
第二步:写监控函数
新建 cloudfunctions/monitorFnErrors,目录里两个文件。
cloudfunctions/monitorFnErrors/package.json:
{
"name": "monitorFnErrors",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@cloudbase/manager-node": "^5.0.0",
"@cloudbase/node-sdk": "^3.18.1"
}
}
cloudfunctions/monitorFnErrors/index.js:
const CloudBase = require('@cloudbase/manager-node');
const cloudbase = require('@cloudbase/node-sdk');
const https = require('https');
const ENV_ID = process.env.TCB_ENV;
const SECRET_ID = process.env.TENCENT_SECRET_ID;
const SECRET_KEY = process.env.TENCENT_SECRET_KEY;
const WECOM_KEY = process.env.WECOM_WEBHOOK_KEY;
// 监控范围:列出要看的函数。也可以从一个集合里读
const TARGET_FUNCTIONS = (process.env.MONITOR_FN_LIST || '').split(',').filter(Boolean);
// 阈值:5 分钟内同一函数错误数超过这个才告警
const ERROR_THRESHOLD = Number(process.env.ERROR_THRESHOLD || 10);
// 去重 TTL:同指纹告警 1 小时内不重复
const DEDUP_TTL_MS = 60 * 60 * 1000;
const manager = CloudBase.init({
secretId: SECRET_ID,
secretKey: SECRET_KEY,
envId: ENV_ID,
});
const tcbApp = cloudbase.init({
env: ENV_ID || cloudbase.SYMBOL_CURRENT_ENV,
});
const db = tcbApp.database();
exports.main = async (event) => {
const now = new Date();
const fiveMinAgo = new Date(now.getTime() - 5 * 60 * 1000);
const range = {
startTime: fmt(fiveMinAgo),
endTime: fmt(now),
};
console.log('[monitor] window', range);
const reports = [];
for (const name of TARGET_FUNCTIONS) {
try {
const failed = await pullErrors(name, range);
if (failed.length >= ERROR_THRESHOLD) {
reports.push({ name, count: failed.length, sample: failed[0] });
}
} catch (e) {
console.error('[monitor] pull failed for', name, e.message);
}
}
// 阈值过线的,逐个去重 → 推送
for (const r of reports) {
const key = `${r.name}::${(r.sample?.RetMsg || '').slice(0, 80)}`;
if (await hasRecentlyAlerted(key)) {
console.log('[monitor] dedup skip', key);
continue;
}
await sendWecomAlert(r);
await markAlerted(key);
}
return { ok: true, alerted: reports.map((r) => r.name) };
};
async function pullErrors(name, range) {
const list = await manager.functions.getFunctionLogsV2({
name,
startTime: range.startTime,
endTime: range.endTime,
limit: 100,
});
// RetCode 200 是成功,其余视为失败
const failed = (list.LogList || []).filter((item) => Number(item.RetCode) !== 200);
// 取一条详情样本,用来生成告警卡片
if (failed.length === 0) return [];
try {
const detail = await manager.functions.getFunctionLogDetail({
logRequestId: failed[0].RequestId,
startTime: range.startTime,
endTime: range.endTime,
});
failed[0].RetMsg = detail.RetMsg || '';
} catch (e) {
// 详情拉失败不影响主流程
console.warn('[monitor] detail failed', e.message);
}
return failed;
}
async function hasRecentlyAlerted(key) {
const since = Date.now() - DEDUP_TTL_MS;
const res = await db
.collection('alert_state')
.where({ key, alertedAt: db.command.gte(new Date(since)) })
.limit(1)
.get();
return res.data.length > 0;
}
async function markAlerted(key) {
await db.collection('alert_state').add({
key,
alertedAt: db.serverDate(),
});
}
function sendWecomAlert(r) {
const body = {
msgtype: 'markdown',
markdown: {
content:
`## 云函数错误告警\n\n` +
`**函数**:\`${r.name}\`\n` +
`**5 分钟内错误数**:<font color="warning">${r.count}</font>\n` +
`**最近 RequestId**:\`${r.sample?.RequestId || 'n/a'}\`\n` +
(r.sample?.RetMsg ? `**摘要**:${r.sample.RetMsg.slice(0, 200)}\n` : ''),
},
};
return new Promise((resolve, reject) => {
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${WECOM_KEY}`;
const data = JSON.stringify(body);
const req = https.request(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
},
},
(res) => {
let raw = '';
res.on('data', (c) => (raw += c));
res.on('end', () => resolve(raw));
},
);
req.on('error', reject);
req.write(data);
req.end();
});
}
function fmt(d) {
// manager-node 期望 'YYYY-MM-DD HH:mm:ss'
return d.toISOString().replace('T', ' ').slice(0, 19);
}
要点:
RetCode === 200是成功,其他视为失败。这个语义参考 getFunctionLogsV2 返回结构- 时间格式必须是
YYYY-MM-DD HH:mm:ss,且startTime/endTime必须在 1 天之内 getFunctionLogsV2不返回函数返回值正文,要看具体错误信息得再调一次getFunctionLogDetail,这里只取第一条样本省 quota
第三步:配定时触发器
在 cloudbaserc.json 的 functions 数组里加:
{
"name": "monitorFnErrors",
"timeout": 60,
"memorySize": 256,
"runtime": "Nodejs16.13",
"handler": "index.main",
"triggers": [
{
"name": "every-5min",
"type": "timer",
"config": "0 */5 * * * * *"
}
]
}
0 */5 * * * * * 是 7 字段 cron,意思是每 5 分钟跑一次。完整 cron 语法见 schedule-cloud-function-cron-job 第二步。
第四步:部署 + 配环境变量
tcb login --apiKeyId your-key-id --apiKey your-key
tcb fn deploy monitorFnErrors -e your-env-id
控制台 → 云函数 → monitorFnErrors → 环境变量,加这几个:
| 变量名 | 值 | 来源 |
|---|---|---|
TCB_ENV | 当前环境 ID | Cloudbase 控制台首页 |
TENCENT_SECRET_ID | secretId | 腾讯云「访问管理 / API 密钥」 |
TENCENT_SECRET_KEY | secretKey | 同上 |
WECOM_WEBHOOK_KEY | webhook key UUID | 企业微信群机器人,见 connect-wecom-webhook-cloud-function |
MONITOR_FN_LIST | getLoginTicket,wecomNotify,dailyReport | 要监控的函数名,逗号分隔 |
ERROR_THRESHOLD | 10(默认) | 5 分钟内错误数超过这个才告警 |
第五步:误报规避
定时跑起来之后会发现一些「不是真错」的情况,几个常见过滤:
1. 部署期间的瞬时失败
tcb fn deploy 期间会有几秒钟 502。最简单的做法是开一个「维护窗口」环境变量:
const MAINT_END = Number(process.env.MAINT_END_AT || 0); // 时间戳,过这个时间之前不 告警
if (Date.now() < MAINT_END) {
console.log('[monitor] in maintenance window, skip');
return { ok: true, skipped: true };
}
部署前手动改一下 MAINT_END_AT(把它设为部署完成预计时间 + 5 分钟)。
2. 已知错误白名单
某些错误是预期的(用户输入校验失败、某些业务路径就是会抛 400)。在 pullErrors 里加过滤:
const KNOWN_OK = [
'INVALID_INPUT',
'permission denied for read', // 已知用户没登录拿数据,不算 bug
];
const failed = (list.LogList || []).filter((item) => {
if (Number(item.RetCode) === 200) return false;
if (KNOWN_OK.some((k) => (item.RetMsg || '').includes(k))) return false;
return true;
});
3. 第一次部署后的「冷启动毛刺」
新部署的函数前几次调用可能会因为冷启动慢踩到 timeout,这个不算业务问题。可以在告警卡片里加一句「最近发布时间」给值班人参考,不用做特殊处理。
运行验证
- 部署完成后,控制台 → 云函数 → monitorFnErrors → 触发器,看到
every-5min - 故意让一个被监控的函数报错(比如临时改一行代码 throw),触发它 11 次,等下一次 monitor 轮询
- 企业微信群应该收到一条
## 云函数错误告警的 markdown 卡片 - 同样的错误再触发 11 次,1 小时内不应该再收到第二条(去重生效)
- 控制台 → 数据库 → alert_state 集合,有一条
key为<fnName>::<errMsg>、alertedAt是刚才时间的记录
常见错误
| 错误现象 | 原因 | 修复 |
|---|---|---|
getFunctionLogsV2 报 Authentication failure | secretId / secretKey 错,或这对 key 没绑定该云开发环境的角色 | 控制台「访问管理 / 用户」给这对 key 加 QcloudTCBFullAccess 类似的策略 |
start time is more than 1 day before end time | 时间窗口超过 1 天 | manager-node 强制限制,改成 5 分钟窗口 足够 |
| 群里收到一堆重复告警 | alert_state 集合权限不允许写 | 把集合权限改成「仅管理端可读写」(云函数访问没问题) |
MONITOR_FN_LIST 配了但有些函数从来没告警 | 那些函数最近 5 分钟没有调用 = LogList 为空 = 0 个错误 | 正常情况;阈值 10 个错误也才触发,低频函数本来就不容易告警 |
告警里 RetMsg 是空 | getFunctionLogDetail 拿不到详情(日志还没投递到 CLS) | CLS 日志有几秒到 1 分钟的投递延迟,等下一轮再看 |
| 部署后第一次跑卡了几秒 | 冷启动 + manager-node 第一次调腾讯云 API 慢 | 把 timeout 调到 60s 即可 |
相关文档
- getFunctionLogsV2 — 接口签名 + 入参 + 返回
- searchClsLog — 跨函数 / 模块的统一日志搜索,适合更复杂场景
- connect-wecom-webhook-cloud-function — 前置:推送层
- schedule-cloud-function-cron-job — 前置:cron 触发器配置
下一步
- 把告警规则做成可配置(从一个集合读阈值 / 函数列表):参考 secure-database-multi-tenant-rules 的多租户配置思路
- 给关键业务函数加分级告警(P0 / P1 用不同 webhook):在第二步
sendWecomAlert里按函数名路由 webhook key - 监控数据库慢查询:换用
app.log.searchClsLog({ queryString: 'module:database AND eventType:MongoSlowQuery' })