CloudWatchのカスタムウィジェットのLambda書いてみた

CloudWatchのダッシュボードのカスタムウィジェットを作ってみました
2022.11.18

どうも。CX事業本部Delivery部のえーたん(@eetann092)です。

先日、CloudWatchのダッシュボードでLambdaの実行結果をカスタムウィジェットとして表示できることを知りました。せっかくなので素振りしました。今回はクリックしたボタンに応じて表示内容を変えるウィジェットを作成しました。

ボタンクリック時には確認画面も表示します。

カスタムウィジェットの作成

LambdaはCDKでNodejsFunctionを使って作成しました。

lib/custom-widget-lambda-sample-stack.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";

export class CustomWidgetLambdaSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new NodejsFunction(this, "CustomWidgetLambda", {
      entry: "lambda/custom-widget-function.ts",
    });
  }
}

以下は、Lambdaに書いた処理の全体が分かるように一部を省略して書いたものです。

lambda/custom-widget-function.ts

const DOCS = `
Markdownでドキュメントが書ける。
`;

export const handler = async (event: Event, context: Context) => {
  if (event.describe) {
    return DOCS;
  }

  // 送信された値を取得する
  const person = event.person || "";
  const go = event.go || false;

  return `
    <div>
      aタグとcwdb-actionを使ってクリック時に送信
      送信された値から作成したテキストを表示
    </div>
  `;
};

長かったのでフルバージョンは以下の折りたたみの中に書きました。

クリックで展開できます
type Event = {
  describe: boolean;
  widgetContext: any;
  person: string;
  go: boolean;
};

type Context = {
  invokedFunctionArn: string;
};

const DOCS = `
## custom-widget-function
Markdownでドキュメントを書くことができます。
**太字**、*イタリック*、~~打ち消し線~~、\`インラインコード\`などなど

* リスト
* リスト
  * ネスト
* リスト


1. 番号付きのリスト
2. 番号付きのリスト
  1. ネスト
3. 番号付きのリスト


[リンク](https://dev.classmethod.jp/author/eetann/)

![画像](https://raw.githubusercontent.com/eetann/choomame/main/public/icons/icon-128x128.png)

---

\`\`\` python
print("piyopiyo")
\`\`\`

### テーブルも書ける
name | ok
---|---
Kerry | true
Johnny | false
Goro | false

| name | ok |
|---|---|
| Kerry | true |
| Johnny | false |
| Goro | false |

`;

export const handler = async (event: Event, context: Context) => {
  if (event.describe) {
    return DOCS;
  }

  const timestamp = new Date();
  const person = event.person || "";
  const go = event.go || false;

  let response = "";

  const people = [
    { name: "Kerry", ok: true, cls: "btn kerry" },
    { name: "Johnny", ok: false, cls: "btn btn-primary" },
    { name: "Goro", ok: false, cls: "btn" },
  ];

  const rows = people
    .map(({ name, ok, cls }) => {
      return `
      <tr>
        <td>${name}</td>
        <td>
          <a class="${cls}">選ぶ</a>
          <cwdb-action
            action="call"
            endpoint="${context.invokedFunctionArn}" 
            confirmation="本当に${name}を選びますか?">
            { "person": "${name}", "go": ${ok} }
          </cwdb-action>
        </td>
        </td>
      </tr>
    `;
      // });
    })
    .join("");

  if (person) {
    if (go) {
      response = `${person}「着いたら連絡する」`;
    } else {
      response = `${person}「行けたら行く」`;
    }
  }

  const _event = event;
  _event.widgetContext.accountId = 123456789012;

  return `
    <div>
      <table>
        ${rows}
      </table>
      <p>${timestamp}</p>
      <p>${response}</p>
      <pre>${JSON.stringify(_event, null, 2)}</pre>
    </div>
    <style>
    .kerry {
      color: white !important;
      background-color: black !important;
    }
    </style>
  `;
};

1つずつ区切って説明します。

まず、最初のevent.describeを使ったif文は、ドキュメントの表示です。

if (event.describe) {
  return DOCS;
}

ドキュメントはMarkdownで記述でき、カスタムウィジェットの追加や編集時に表示されます。

実際に書いてみたところ、文字の装飾やリストの他、画像も普通に表示できました。

次に、Lambdaのハンドラの第1引数eventから、persongoを変数に代入しました。 event.personevent.goは、HTMLのタグcwdb-actionを使って送信した値が入ります。

const person = event.person || "";
const go = event.go || false;

タグcwdb-actionは、直前の要素が選択された時の動作を決めるものです。 今回の例では、直前の要素aタグのクリック時の動作を「カスタムウィジェットを表示するLambda(=同じLambda)の呼び出し」にしています。実際の役割はcwdb-actionタグの中に書いた値の送信です。

<a class="${cls}">選ぶ</a>
<cwdb-action
  action="call"
  endpoint="${context.invokedFunctionArn}" 
  confirmation="本当に${name}を選びますか?">
  { "person": "${name}", "go": ${ok} }
</cwdb-action>

confirmation属性を使うことで確認画面を表示させました。

aタグのclassでは、ボタンとして表示するための指定をしました。

カスタムウィジェットで用意されているスタイルbtnbtn-primaryのほか、styleタグで独自にスタイルを設定できます。

preタグを使って、Lambdaのハンドラの第1引数eventの中身も表示してみました。JSON.stringify()に第2引数、第3引数があることを初めて知りました。

<pre>${JSON.stringify(_event, null, 2)}</pre>

以下が表示された内容です。タグcwdb-actionで送信したpersongoの他、widgetContextキーにたくさんの値が入っています。ウィジェットのサイズも分かるようです。

{
  "person": "Kerry",
  "go": true,
  "widgetContext": {
    "dashboardName": "custom-widget-lambda-sample",
    "widgetId": "widget-1",
    "domain": "https://ap-northeast-1.console.aws.amazon.com",
    "accountId": 123456789012,
    "locale": "ja",
    "timezone": {
      "label": "Local",
      "offsetISO": "+09:00",
      "offsetInMinutes": -540
    },
    "period": 300,
    "isAutoPeriod": true,
    "timeRange": {
      "mode": "relative",
      "start": 1668725221893,
      "end": 1668736021893,
      "relativeStart": 10800004
    },
    "theme": "light",
    "linkCharts": true,
    "title": "これがタイトルです。",
    "params": null,
    "forms": {
      "all": {}
    },
    "width": 1363,
    "height": 764
  }
}

謎のカンマが表示されたので対処

テーブル表示にするために、以下のようにリストからtrタグなどを含む文字列を生成しました。

const rows = people
  .map(({ name, ok, cls }) => {
    return `
    <tr>
      <td>${name}</td>
      <td>
        <a class="${cls}">選ぶ</a>
        <cwdb-action
          action="call"
          endpoint="${context.invokedFunctionArn}" 
          confirmation="本当に${name}を選びますか?">
          { "person": "${name}", "go": ${ok} }
        </cwdb-action>
      </td>
      </td>
    </tr>
  `;
    // });
  })
  .join("");

ここで最後の.join("")で連結し忘れると、以下の画像のようにテーブルの前にカンマが表示されてしまいます。連結を忘れないようにしましょう。

カスタムウィジェットの更新のタイミング

カスタムウィジェットの更新は、ページの読み込み時や更新ボタンを手動で押した時だけではありません。3つのタイミングで更新するかを制御できます。 更新のタイミングの制御は、カスタムウィジェット作成時または編集時に設定できます。

ダッシュボードの自動更新のボタンを押す時

カスタムウィジェットは、ダッシュボードの自動更新の時に更新するか制御できます。

ウィジェットのサイズを変更した時

カスタムウィジェットは、ウィジェットのサイズを変更した時に更新するか制御できます。

ウィジェットのサイズは、Lambdaのハンドラの第1引数eventwidgetContextwidthheightに入っているようです。

ダッシュボードの期間を変更した時

カスタムウィジェットは、ダッシュボードの期間を変更した時に更新するか制御できます。

ダッシュボードの期間は、Lambdaのハンドラの第1引数eventwidgetContext.timeRangeに入っているようです。

ダッシュボードの保存を忘れずに

カスタムウィジェットを作成したらダッシュボード右上に表示されているボタンを押して保存しましょう。筆者はかれこれ5回以上押し忘れています……。

リンク集

サンプルコードのGitHubのリンクは以下です。

参考: