Skip to content

Commit

Permalink
Get Insights query from resource tag
Browse files Browse the repository at this point in the history
Closes #2
  • Loading branch information
farski committed Oct 18, 2024
1 parent d6b9882 commit 4fb575e
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 58 deletions.
11 changes: 7 additions & 4 deletions src/alarm-slack-notifications/alarm/single-metric.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -27,9 +28,10 @@ function precision(a) {
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {DescribeAlarmHistoryOutput} history
* @param {ListTagsForResourceOutput} tagList
* * @returns {Promise<String[]>}
*/
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);

Expand Down Expand Up @@ -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>`);
}
Expand Down Expand Up @@ -254,12 +256,13 @@ function cause(event, desc, history) {
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {DescribeAlarmHistoryOutput} history
* @param {ListTagsForResourceOutput} tagList
* @returns {Promise<String[]>}
*/
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),
];
Expand Down
6 changes: 4 additions & 2 deletions src/alarm-slack-notifications/builder-alarm.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
/** @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";

/**
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {DescribeAlarmHistoryOutput} history
* @param {ListTagsForResourceOutput} tagList
* @returns {Promise<String[]>}
*/
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!"];
Expand Down
17 changes: 13 additions & 4 deletions src/alarm-slack-notifications/builder-ok.mjs
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -154,9 +155,10 @@ function duration(event) {
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {DescribeAlarmHistoryOutput} history
* @param {ListTagsForResourceOutput} tagList
* @returns {Promise<String[]>}
*/
async function basics(event, desc, history) {
async function basics(event, desc, history, tagList) {
let line = "";

line = line.concat(duration(event));
Expand All @@ -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>`);
}
Expand Down Expand Up @@ -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<String[]>}
*/
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)),
];
}
18 changes: 14 additions & 4 deletions src/alarm-slack-notifications/builder.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -79,16 +81,17 @@ async function cloudWatchClient(event) {
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {DescribeAlarmHistoryOutput} history
* @param {ListTagsForResourceOutput} tagList
* @returns {Promise<String[]>}
*/
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 [];
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");

Expand Down
43 changes: 3 additions & 40 deletions src/alarm-slack-notifications/log-groups.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<String>}
*/
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 (
Expand Down Expand Up @@ -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",
);
Expand Down
19 changes: 15 additions & 4 deletions src/alarm-slack-notifications/urls.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -193,10 +194,11 @@ function singleMetricAlarmMetricsConsole(event, desc, history) {
*
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {ListTagsForResourceOutput} tagList
* @returns {Promise<String>}
*/
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 "";
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -310,8 +320,9 @@ export function metricsConsoleUrl(event, desc, history) {
/**
* @param {EventBridgeCloudWatchAlarmsEvent} event
* @param {DescribeAlarmsOutput} desc
* @param {ListTagsForResourceOutput} tagList
* @returns {Promise<String>}
*/
export async function logsConsoleUrl(event, desc) {
return logsInsightsConsole(event, desc);
export async function logsConsoleUrl(event, desc, tagList) {
return logsInsightsConsole(event, desc, tagList);
}

0 comments on commit 4fb575e

Please sign in to comment.