You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在AWS CodeBuild完成时一次性发送完整CloudWatch日志到Slack

解决CodeBuild完成后一次性发送完整日志到Slack的方案

问题背景

已实现Lambda函数将AWS CodeBuild的CloudWatch日志发送至Slack,但当前使用CloudWatch日志触发器,会在CodeBuild执行过程中每产生一条日志就发送一次Slack消息,造成消息轰炸。需求是仅在CodeBuild执行完成后,一次性发送完整的构建日志。

当前触发器配置:
CloudWatch日志触发器

原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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 12:48:09