From 4fb575e3ab85c24d6ec332c76eff843dafcb3f88 Mon Sep 17 00:00:00 2001 From: Chris Kalafarski Date: Fri, 18 Oct 2024 12:02:28 -0400 Subject: [PATCH] Get Insights query from resource tag Closes #2 --- .../alarm/single-metric.mjs | 11 +++-- .../builder-alarm.mjs | 6 ++- src/alarm-slack-notifications/builder-ok.mjs | 17 ++++++-- src/alarm-slack-notifications/builder.mjs | 18 ++++++-- src/alarm-slack-notifications/log-groups.mjs | 43 ++----------------- src/alarm-slack-notifications/urls.mjs | 19 ++++++-- 6 files changed, 56 insertions(+), 58 deletions(-) diff --git a/src/alarm-slack-notifications/alarm/single-metric.mjs b/src/alarm-slack-notifications/alarm/single-metric.mjs index 0fddc1e..259d4eb 100644 --- a/src/alarm-slack-notifications/alarm/single-metric.mjs +++ b/src/alarm-slack-notifications/alarm/single-metric.mjs @@ -1,6 +1,7 @@ /** @typedef {import('../index.mjs').EventBridgeCloudWatchAlarmsEvent} EventBridgeCloudWatchAlarmsEvent */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmsOutput} DescribeAlarmsOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmHistoryOutput} DescribeAlarmHistoryOutput */ +/** @typedef {import('@aws-sdk/client-cloudwatch').ListTagsForResourceOutput} ListTagsForResourceOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').MetricAlarm} MetricAlarm */ import { comparison } from "../operators.mjs"; @@ -27,9 +28,10 @@ function precision(a) { * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc * @param {DescribeAlarmHistoryOutput} history + * @param {ListTagsForResourceOutput} tagList * * @returns {Promise} */ -async function started(event, desc, history) { +async function started(event, desc, history, tagList) { if (event.detail.state.reasonData) { const data = JSON.parse(event.detail.state.reasonData); @@ -64,7 +66,7 @@ async function started(event, desc, history) { let console = `*CloudWatch:* <${metricsUrl}| Metrics>`; - const logsUrl = await logsConsoleUrl(event, desc); + const logsUrl = await logsConsoleUrl(event, desc, tagList); if (logsUrl) { console = console.concat(` • <${logsUrl}|Logs>`); } @@ -254,12 +256,13 @@ function cause(event, desc, history) { * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc * @param {DescribeAlarmHistoryOutput} history + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -export async function detailLines(event, desc, history) { +export async function detailLines(event, desc, history, tagList) { return [ ...cause(event, desc, history), - ...(await started(event, desc, history)), + ...(await started(event, desc, history, tagList)), ...datapoints(event, desc), ...last24Hours(history), ]; diff --git a/src/alarm-slack-notifications/builder-alarm.mjs b/src/alarm-slack-notifications/builder-alarm.mjs index 9e3fa80..3ae504c 100644 --- a/src/alarm-slack-notifications/builder-alarm.mjs +++ b/src/alarm-slack-notifications/builder-alarm.mjs @@ -1,6 +1,7 @@ /** @typedef {import('./index.mjs').EventBridgeCloudWatchAlarmsEvent} EventBridgeCloudWatchAlarmsEvent */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmsOutput} DescribeAlarmsOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmHistoryOutput} DescribeAlarmHistoryOutput */ +/** @typedef {import('@aws-sdk/client-cloudwatch').ListTagsForResourceOutput} ListTagsForResourceOutput */ import { detailLines as singleMetricDetailLines } from "./alarm/single-metric.mjs"; @@ -8,11 +9,12 @@ import { detailLines as singleMetricDetailLines } from "./alarm/single-metric.mj * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc * @param {DescribeAlarmHistoryOutput} history + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -export async function detailLines(event, desc, history) { +export async function detailLines(event, desc, history, tagList) { if (event.detail.configuration.metrics.length === 1) { - return singleMetricDetailLines(event, desc, history); + return singleMetricDetailLines(event, desc, history, tagList); } return ["Unknown alarm metric type!"]; diff --git a/src/alarm-slack-notifications/builder-ok.mjs b/src/alarm-slack-notifications/builder-ok.mjs index b4c5e5a..0834293 100644 --- a/src/alarm-slack-notifications/builder-ok.mjs +++ b/src/alarm-slack-notifications/builder-ok.mjs @@ -1,5 +1,6 @@ /** @typedef {import('./index.mjs').EventBridgeCloudWatchAlarmsEvent} EventBridgeCloudWatchAlarmsEvent */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmsOutput} DescribeAlarmsOutput */ +/** @typedef {import('@aws-sdk/client-cloudwatch').ListTagsForResourceOutput} ListTagsForResourceOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmHistoryOutput} DescribeAlarmHistoryOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').GetMetricDataOutput} GetMetricDataOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').CloudWatchClient} CloudWatchClient */ @@ -154,9 +155,10 @@ function duration(event) { * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc * @param {DescribeAlarmHistoryOutput} history + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -async function basics(event, desc, history) { +async function basics(event, desc, history, tagList) { let line = ""; line = line.concat(duration(event)); @@ -167,7 +169,7 @@ async function basics(event, desc, history) { // Not all alarms can be associated with logs, so only add when there // is a URL to use - const logsUrl = await logsConsoleUrl(event, desc); + const logsUrl = await logsConsoleUrl(event, desc, tagList); if (logsUrl) { line = line.concat(` • <${logsUrl}|Logs>`); } @@ -274,12 +276,19 @@ async function datapoints(event, desc, cloudWatchClient) { * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc * @param {DescribeAlarmHistoryOutput} history + * @param {ListTagsForResourceOutput} tagList * @param {CloudWatchClient} cloudWatchClient * @returns {Promise} */ -export async function detailLines(event, desc, history, cloudWatchClient) { +export async function detailLines( + event, + desc, + history, + tagList, + cloudWatchClient, +) { return [ - ...(await basics(event, desc, history)), + ...(await basics(event, desc, history, tagList)), ...(await datapoints(event, desc, cloudWatchClient)), ]; } diff --git a/src/alarm-slack-notifications/builder.mjs b/src/alarm-slack-notifications/builder.mjs index 59b0caf..dc86e84 100644 --- a/src/alarm-slack-notifications/builder.mjs +++ b/src/alarm-slack-notifications/builder.mjs @@ -1,12 +1,14 @@ /** @typedef {import('./index.mjs').EventBridgeCloudWatchAlarmsEvent} EventBridgeCloudWatchAlarmsEvent */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmsOutput} DescribeAlarmsOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmHistoryOutput} DescribeAlarmHistoryOutput */ +/** @typedef {import('@aws-sdk/client-cloudwatch').ListTagsForResourceOutput} ListTagsForResourceOutput */ import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; import { CloudWatchClient, DescribeAlarmsCommand, paginateDescribeAlarmHistory, + ListTagsForResourceCommand, } from "@aws-sdk/client-cloudwatch"; import { ConfiguredRetryStrategy } from "@aws-sdk/util-retry"; import { alarmConsoleUrl } from "./urls.mjs"; @@ -79,16 +81,17 @@ async function cloudWatchClient(event) { * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc * @param {DescribeAlarmHistoryOutput} history + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -async function detailLines(event, desc, history, cwClient) { +async function detailLines(event, desc, history, tagList, cwClient) { switch (event.detail.state.value) { case "INSUFFICIENT_DATA": return ["Details not implemented for `INSUFFICIENT_DATA`"]; case "OK": - return okDetailLines(event, desc, history, cwClient); + return okDetailLines(event, desc, history, tagList, cwClient); case "ALARM": - return alarmDetailLines(event, desc, history); + return alarmDetailLines(event, desc, history, tagList); default: return []; } @@ -124,6 +127,13 @@ export async function blocks(event) { }), ); + // Fetch the resource tags for the alarm + const tagList = await cloudwatch.send( + new ListTagsForResourceCommand({ + ResourceARN: event.resources[0], + }), + ); + // Fetch all state transitions from the last 24 hours const history = { AlarmHistoryItems: [] }; const historyStart = new Date(); @@ -156,7 +166,7 @@ export async function blocks(event) { const lines = []; - lines.push(...(await detailLines(event, desc, history, cloudwatch))); + lines.push(...(await detailLines(event, desc, history, tagList, cloudwatch))); let text = lines.join("\n"); diff --git a/src/alarm-slack-notifications/log-groups.mjs b/src/alarm-slack-notifications/log-groups.mjs index 4f65941..f7f2ebf 100644 --- a/src/alarm-slack-notifications/log-groups.mjs +++ b/src/alarm-slack-notifications/log-groups.mjs @@ -1,14 +1,7 @@ /** @typedef {import('./index.mjs').EventBridgeCloudWatchAlarmsEvent} EventBridgeCloudWatchAlarmsEvent */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmsOutput} DescribeAlarmsOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmHistoryOutput} DescribeAlarmHistoryOutput */ - -import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; -import { - CloudWatchClient, - ListTagsForResourceCommand, -} from "@aws-sdk/client-cloudwatch"; - -const sts = new STSClient({ apiVersion: "2011-06-15" }); +/** @typedef {import('@aws-sdk/client-cloudwatch').ListTagsForResourceOutput} ListTagsForResourceOutput */ // Alarms with certain namespaces can look up a log group from their resource // tags, when there's no way to infer the log group from the alarm's @@ -27,39 +20,15 @@ const TAGGED = [ "PRX/Clickhouse", ]; -/** - * @param {EventBridgeCloudWatchAlarmsEvent} event - */ -async function cloudWatchClient(event) { - const accountId = event.account; - const roleName = process.env.CROSS_ACCOUNT_CLOUDWATCH_ALARM_IAM_ROLE_NAME; - - const role = await sts.send( - new AssumeRoleCommand({ - RoleArn: `arn:aws:iam::${accountId}:role/${roleName}`, - RoleSessionName: "notifications_lambda_reader", - }), - ); - - return new CloudWatchClient({ - apiVersion: "2010-08-01", - region: event.region, - credentials: { - accessKeyId: role.Credentials.AccessKeyId, - secretAccessKey: role.Credentials.SecretAccessKey, - sessionToken: role.Credentials.SessionToken, - }, - }); -} - /** * Returns the name of a log group associated with the alarm that triggerd * and event. * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -export async function logGroupName(event, desc) { +export async function logGroupName(event, desc, tagList) { // For Lambda alarms, look for a FunctionName dimension, and use that name // to construct the log group name if ( @@ -92,12 +61,6 @@ export async function logGroupName(event, desc) { // the tags on the alarm should be inspected to see if an explicit log // group name is specified. If so, use that. else if (TAGGED.includes(desc?.MetricAlarms?.[0]?.Namespace)) { - const cloudwatch = await cloudWatchClient(event); - const tagList = await cloudwatch.send( - new ListTagsForResourceCommand({ - ResourceARN: event.resources[0], - }), - ); const logGroupNameTag = tagList?.Tags?.find( (t) => t.Key === "prx:ops:cloudwatch-log-group-name", ); diff --git a/src/alarm-slack-notifications/urls.mjs b/src/alarm-slack-notifications/urls.mjs index 4bdfd4b..b7777f4 100644 --- a/src/alarm-slack-notifications/urls.mjs +++ b/src/alarm-slack-notifications/urls.mjs @@ -1,6 +1,7 @@ /** @typedef {import('./index.mjs').EventBridgeCloudWatchAlarmsEvent} EventBridgeCloudWatchAlarmsEvent */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmsOutput} DescribeAlarmsOutput */ /** @typedef {import('@aws-sdk/client-cloudwatch').DescribeAlarmHistoryOutput} DescribeAlarmHistoryOutput */ +/** @typedef {import('@aws-sdk/client-cloudwatch').ListTagsForResourceOutput} ListTagsForResourceOutput */ import { ascii } from "./operators.mjs"; import { logGroupName } from "./log-groups.mjs"; @@ -193,10 +194,11 @@ function singleMetricAlarmMetricsConsole(event, desc, history) { * * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -async function logsInsightsConsole(event, desc) { - const logGroup = await logGroupName(event, desc); +async function logsInsightsConsole(event, desc, tagList) { + const logGroup = await logGroupName(event, desc, tagList); if (!logGroup) { return ""; @@ -245,6 +247,14 @@ async function logsInsightsConsole(event, desc) { source: [logGroup], }; + const insightsQueryTag = tagList?.Tags?.find( + (t) => t.Key === "prx:ops:cloudwatch-logs-insights-query", + ); + + if (insightsQueryTag) { + queryPayload.editorString = insightsQueryTag.Value; + } + // The payload is CloudWatch URL encoded const encodedPayload = cwUrlEncode(queryPayload); @@ -310,8 +320,9 @@ export function metricsConsoleUrl(event, desc, history) { /** * @param {EventBridgeCloudWatchAlarmsEvent} event * @param {DescribeAlarmsOutput} desc + * @param {ListTagsForResourceOutput} tagList * @returns {Promise} */ -export async function logsConsoleUrl(event, desc) { - return logsInsightsConsole(event, desc); +export async function logsConsoleUrl(event, desc, tagList) { + return logsInsightsConsole(event, desc, tagList); }