如何在AWS CodeBuild完成时一次性发送完整CloudWatch日志到Slack
解决CodeBuild完成后一次性发送完整日志到Slack的方案
问题背景
已实现Lambda函数将AWS CodeBuild的CloudWatch日志发送至Slack,但当前使用CloudWatch日志触发器,会在CodeBuild执行过程中每产生一条日志就发送一次Slack消息,造成消息轰炸。需求是仅在CodeBuild执行完成后,一次性发送完整的构建日志。
当前触发器配置:
原Lambda代码(Node.js):
const zlib = require("zlib"); const https = require("https"); const SLACK_ENDPOINT ="/services/000000000000000000000000000000000000" const SLACK_BOT = "deploy-notifications"; function doRequest(content) { // formatting the message according Slack API const payload = { username: SLACK_BOT, blocks: [ { type: "header", text: { type: "plain_text", text: "Whoops, looks like something went wrong 😞🤕", emoji: true, }, }, { type: "section", fields: [ { type: "mrkdwn", text: "<!here> the API is running into an issue", }, ], }, { type: "section", fields: [ { type: "mrkdwn", text: "*Environment: * Production", }, ], }, { type: "section", fields: [ { type: "mrkdwn", text: "*Message:* _" + content.message + "_" , }, ], }, { type: "section", fields: [ { type: "mrkdwn", text: "*Stacktrace:*", }, ], }, { type: "section", text: { type: "mrkdwn", text: "```" + JSON.stringify(content.original ? content.original : content) + "```", }, }, { type: "divider", }, ], }; const payloadStr = JSON.stringify(payload); const options = { hostname: "hooks.slack.com", port: 443, path: SLACK_ENDPOINT, channel: "#deploy-notifications", method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payloadStr), }, }; const postReq = https.request(options, function (res) { const chunks = []; res.setEncoding("utf8"); res.on("data", function (chunk) { return chunks.push(chunk); }); res.on("end", function () { if (res.statusCode < 400) { console.log("sent!!!"); } else if (res.statusCode < 500) { console.error( "Error posting message to Slack API: " + res.statusCode + " - " + res.statusMessage ); } else { console.error( "Server error when processing message: " + res.statusCode + " - " + res.statusMessage ); } }); return res; }); postReq.write(payloadStr); postReq.end(); } function main(event, context) { context.callbackWaitsForEmptyEventLoop = true; // always returns the last event const payload = Buffer.from(event.awslogs.data, "base64"); const log = JSON.parse(zlib.gunzipSync(payload).toString("utf8")); // the log is an object that contains an array of events called `logEvents` and we need access it bypassing the index 0 doRequest(log.logEvents[0]); const response = { statusCode: 200, body: JSON.stringify("Event sent to Slack!") }; return response; } exports.handler = main;
解决方案
1. 替换触发器:用EventBridge替代CloudWatch日志触发器
CloudWatch日志触发器是实时触发的,无法等待构建完成。改用EventBridge(原CloudWatch Events)监听CodeBuild的构建完成事件,只有当构建进入终态(SUCCEEDED/FAILED/STOPPED)时才触发Lambda。
操作步骤:
- 打开AWS EventBridge控制台,创建新规则
- 规则类型选择事件模式,事件源选AWS服务,服务名称选CodeBuild,事件类型选CodeBuild Build State Change
- (可选)添加事件模式匹配条件,比如只针对特定CodeBuild项目,或特定状态:
{ "source": ["aws.codebuild"], "detail-type": ["CodeBuild Build State Change"], "detail": { "project-name": ["你的CodeBuild项目名"], "build-status": ["SUCCEEDED", "FAILED", "STOPPED"] } } - 目标选择你的Lambda函数,完成规则创建
- 删除原有的CloudWatch日志触发器
2. 修改Lambda代码:拉取完整日志并发送
原代码只处理单条日志事件,现在需要调整为:从EventBridge事件中获取构建信息,拉取该构建的完整CloudWatch日志,整理后发送到Slack。
首先给Lambda添加以下IAM权限(附加到执行角色):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "codebuild:BatchGetBuilds", "logs:GetLogEvents" ], "Resource": [ "arn:aws:codebuild:你的区域:你的账号ID:project/你的项目名", "arn:aws:logs:你的区域:你的账号ID:log-group:/aws/codebuild/你的项目名:*" ] } ] }
修改后的Lambda代码:
const https = require("https"); const { CodeBuildClient, BatchGetBuildsCommand } = require("@aws-sdk/client-codebuild"); const { CloudWatchLogsClient, GetLogEventsCommand } = require("@aws-sdk/client-cloudwatch-logs"); const SLACK_ENDPOINT = "/services/000000000000000000000000000000000000"; const SLACK_BOT = "deploy-notifications"; const SLACK_CHANNEL = "#deploy-notifications"; const ENVIRONMENT = "Production"; const codebuildClient = new CodeBuildClient({ region: "你的AWS区域" }); const logsClient = new CloudWatchLogsClient({ region: "你的AWS区域" }); // 拉取完整日志流内容 async function getFullLog(logGroupName, logStreamName) { let allEvents = []; let nextToken = null; do { const params = { logGroupName, logStreamName, nextToken, startFromHead: true }; const command = new GetLogEventsCommand(params); const response = await logsClient.send(command); allEvents = [...allEvents, ...response.events]; nextToken = response.nextForwardToken; } while (nextToken); // 拼接日志内容 return allEvents.map(event => event.message).join("\n"); } // 发送消息到Slack function sendToSlack(buildInfo, logContent) { const buildStatus = buildInfo.buildStatus; const headerText = buildStatus === "SUCCEEDED" ? `构建成功 🎉` : `构建失败 😞`; const statusColor = buildStatus === "SUCCEEDED" ? "#36a64f" : "#ff0000"; const payload = { username: SLACK_BOT, channel: SLACK_CHANNEL, attachments: [ { color: statusColor, blocks: [ { type: "header", text: { type: "plain_text", text: `${headerText} - ${buildInfo.projectName}`, emoji: true } }, { type: "section", fields: [ { type: "mrkdwn", text: `*构建状态:* ${buildStatus}` }, { type: "mrkdwn", text: `*环境:* ${ENVIRONMENT}` }, { type: "mrkdwn", text: `*构建ID:* ${buildInfo.buildId}` }, { type: "mrkdwn", text: `*开始时间:* ${new Date(buildInfo.startTime).toLocaleString()}` }, { type: "mrkdwn", text: `*结束时间:* ${new Date(buildInfo.endTime).toLocaleString()}` } ] }, { type: "section", text: { type: "mrkdwn", text: "*完整构建日志:*\n```\n${logContent}\n```" } } ] } ] }; return new Promise((resolve, reject) => { const payloadStr = JSON.stringify(payload); const options = { hostname: "hooks.slack.com", port: 443, path: SLACK_ENDPOINT, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payloadStr) } }; const postReq = https.request(options, (res) => { let responseBody = ""; res.setEncoding("utf8"); res.on("data", (chunk) => responseBody += chunk); res.on("end", () => { if (res.statusCode < 400) { resolve("消息发送成功"); } else { reject(new Error(`Slack API错误: ${res.statusCode} - ${res.statusMessage}`)); } }); }); postReq.on("error", (err) => reject(err)); postReq.write(payloadStr); postReq.end(); }); } exports.handler = async (event) => { try { // 从EventBridge事件中获取Build ARN const buildArn = event.detail.buildArn; // 获取构建详情 const buildCommand = new BatchGetBuildsCommand({ ids: [buildArn] }); const buildResponse = await codebuildClient.send(buildCommand); const build = buildResponse.builds[0]; if (!build.logs || !build.logs.logGroupName || !build.logs.logStreamName) { throw new Error("无法获取构建日志信息"); } // 拉取完整日志 const logContent = await getFullLog(build.logs.logGroupName, build.logs.logStreamName); // 发送到Slack await sendToSlack({ projectName: build.projectName, buildId: build.id, buildStatus: build.buildStatus, startTime: build.startTime, endTime: build.endTime }, logContent); return { statusCode: 200, body: JSON.stringify("完整日志已发送到Slack") }; } catch (error) { console.error("处理失败:", error); return { statusCode: 500, body: JSON.stringify(`错误: ${error.message}`) }; } };
代码说明
- 使用AWS SDK v3调用CodeBuild和CloudWatch Logs API,需要在Lambda层或代码包中安装
@aws-sdk/client-codebuild和@aws-sdk/client-cloudwatch-logs依赖 getFullLog函数循环拉取日志流的所有事件,拼接成完整日志内容sendToSlack函数根据构建状态生成不同样式的Slack消息,包含构建基本信息和完整日志- 从EventBridge事件中提取构建ARN,进而获取日志组和日志流信息
内容的提问来源于stack exchange,提问作者Santosh
相关产品推荐
相关产品推荐

