New RelicによるAWSの監視をTerraformで自動化する(第3回: ダッシュボード)

# 前回

https://qiita.com/neruneruo/items/643e27942b93a2acdb01

# はじめに
New Relic記事第3弾。

前回、前々回でAmazon CloudWatchのメトリクスを取り込んでアラート監視をするところまで作成した。

今回は、この手のSaaSを使う醍醐味であるイケてるダッシュボードを作ってみる。
なお、イケてるのはダッシュボード機能そのものであり、今回紹介するダッシュボードの例がイケてるわけではないので、読者の方のデザインセンスを活かしてイケてる内容に仕上げていただきたい。

# ダッシュボードの構成要素
ダッシュボードはTerraformの```newrelic_one_dashboard```のリソースを使用する。

ダッシュボードは以下の構成要素の包含関係となっている。

&nbsp;&nbsp;&nbsp;&nbsp;ダッシュボード全体 ＞ ページ ＞ ウィジェット

文字だけだと分かりにくいのでイメージを貼っておく。
画面全体がダッシュボード全体として、ページとウィジェットは以下のように配置された画面構成となる。

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/b423ac0f-04be-60d0-6a1c-e917583a4c8a.png)

たとえば、一つのダッシュボードで、2つのLambda関数を横並びで確認したいような場合、ページを分けて複数ウィジェットのレイアウトを揃えておくとか、サマリのページを追加で用意しておく等の工夫をすることで見やすいダッシュボードを作れるだろう。

:::note info
ダッシュボードの作成については、Terraformの```newrelic_one_dashboard_json```のリソースを使ってJSONで記載する方法もあるが、NRQLの途中やウィジェットのタイトル等でTerraformの変数や参照を使いたくなるケースが多々発生し、テンプレートのIaCが複雑化するため、今回はこちらの方法は割愛する。
:::

# ウィジェットの種類
ダッシュボードでは様々な種類のウィジェットを扱うことができる。

今回は以下の3種類を使用する。

- ビルボード(billboard)
![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/2428fc97-c6c2-09bf-ec75-a7fcb8c5f7eb.png)
- テーブル(table)
![キャプチャ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/9739a66a-6448-eed4-80eb-37307fa750c5.png)
- 折れ線グラフ(line)
![キャプチャ4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/5d42fea6-8f75-9f0f-5323-a4db537576b4.png)
- 棒グラフ(stacked_bar)
![キャプチャ5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/ebe499c4-67c2-310b-a76d-d14ea8cdf96c.png)

他のウィジェットの概要については、以下の公式ドキュメントを参照。
なぜか棒グラフに関する記載がない。「バー」はおそらくバーチャートを指している。

https://docs.newrelic.com/jp/docs/apis/nerdgraph/examples/create-widgets-dashboards-api/

# ダッシュボードをIaCで書く
さて、今回のIaCは非常に長い。ダッシュボードに記載する内容が非常に多いためだ。
今回はサンプルなので、```page```をベタ書きしているが、上記のように、複数の類似の機能を```page```で束ねる場合は、dynamicブロック等を使って効率よく書くべきだろう。

<details><summary>IaC全体</summary>

```HCL
resource "newrelic_one_dashboard" "example" {
  name        = "Dashboard Example"
  description = "Dashboard Example"
  permissions = "public_read_only"

  page {
    name        = "Page Example1"
    description = "Dashboard Page Example1"

    widget_billboard {
      title = "aws.lambda.issues"

      row    = 1
      height = 2

      column = 1
      width  = 2

      legend_enabled = false

      refresh_rate      = 60000
      ignore_time_range = true

      critical = 0


      nrql_query {
        query = <<EOQ
SELECT
  count(*) as 'Issues'
FROM
  NrAiIssue
WHERE
  contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
  event = 'activate' AND
  issueId NOT IN (
    SELECT
      issueId
    FROM
      NrAiIssue
    WHERE
      contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
      event = 'close'
    SINCE last month
    UNTIL now
    LIMIT MAX
  )
SINCE last month
UNTIL now
EOQ
      }
    }

    widget_table {
      title = "aws.lambda.issues"

      row    = 1
      height = 2

      column = 3
      width  = 10

      legend_enabled = true

      refresh_rate      = 60000
      ignore_time_range = true

      nrql_query {
        query = <<EOQ
SELECT
  timestamp,
  issueId,
  title,
  issueLink
FROM
  NrAiIssue
WHERE
  contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
  event = 'activate' AND
  issueId NOT IN (
    SELECT
      issueId
    FROM
      NrAiIssue
    WHERE
      contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
      event = 'close'
    SINCE last month
    UNTIL now
    LIMIT MAX
  )
SINCE last month
UNTIL now
LIMIT 20
EOQ
      }
    }

    widget_line {
      title = "aws.lambda.Url5xxCount"

      row    = 3
      height = 3

      column = 1
      width  = 6

      legend_enabled = true

      refresh_rate = 60000

      y_axis_right {
        y_axis_right_zero = false
        y_axis_right_min  = 0
        y_axis_right_max  = 1
        y_axis_right_series = [
          "aws.lambda.Url5xxRate"
        ]
      }

      units {
        series_overrides {
          series_name = "aws.lambda.Url5xxRate"
          unit        = "percentage"
        }
      }

      colors {
        series_overrides {
          series_name = "aws.lambda.Not.Url5xxCount"
          color       = "#0000ff"
        }
        series_overrides {
          series_name = "aws.lambda.Url5xxCount"
          color       = "#ff0000"
        }
        series_overrides {
          series_name = "aws.lambda.Url5xxRate"
          color       = "#bbbbbb"
        }
      }

      nrql_query {
        query = <<EOQ
SELECT
  count(`aws.lambda.UrlRequestCount`) - count(`aws.lambda.Url5xxCount`) as 'aws.lambda.Not.Url5xxCount',
  count(`aws.lambda.Url5xxCount`) as 'aws.lambda.Url5xxCount',
  if( count(`aws.lambda.UrlRequestCount`) = 0, 0, count(`aws.lambda.Url5xxCount`) / count(`aws.lambda.UrlRequestCount`) ) as 'aws.lambda.Url5xxRate'
FROM
  Metric
WHERE
  `collector.name` = 'cloudwatch-metric-streams' AND
  `aws.accountId` = '${data.aws_caller_identity.self.account_id}' AND
  `aws.lambda.functionName` = '${aws_lambda_function.example1.function_name}'
SINCE 6 hours AGO
TIMESERIES 1 minute
EOQ
      }
    }

    widget_stacked_bar {
      title = "aws.lambda.InsidentCount"

      row    = 3
      height = 3

      column = 7
      width  = 6

      legend_enabled = false

      refresh_rate = 60000

      colors {
        series_overrides {
          series_name = "Nr Ai Incidents"
          color       = "#ff0000"
        }
      }

      nrql_query {
        query = <<EOQ
SELECT
  count(*)
FROM
  NrAiIncident
WHERE
  conditionName = '${newrelic_nrql_alert_condition.example.name}' AND
  event = 'open'
SINCE 6 hours AGO
TIMESERIES 1minute
EOQ
      }
    }
  }

  page {
    name        = "Page Example2"
    description = "Dashboard Page Example2"

    widget_line {
      title = ""

      row    = 1
      column = 1
      width  = 4
      height = 3

      refresh_rate = 60000

      nrql_query {
        query = "SELECT count(`aws.lambda.Url5xxCount`) FROM Metric WHERE `collector.name` = 'cloudwatch-metric-streams' AND `aws.accountId` = '${data.aws_caller_identity.self.account_id}' SINCE 6 HOURS AGO TIMESERIES"
      }
    }
  }
}
```

</details>

## 共通のプロパティ
ダッシュボード全体や、pageで共通のプロパティについて解説する。

と言っても難しいことはなく、ダッシュボード全体のプロパティは以下しかない。
```permissions```は、

- ```private```: 自分しか参照できない
- ```public_read_only```: 自分以外でも参照可能
- ```public_read_write```: 自分以外でも参照更新可能

の値を取ることができるが、通常はチーム内で参照が出来ればよいので、```public_read_only```を設定する。


```HCL
resource "newrelic_one_dashboard" "example" {
  name        = "Dashboard Example"
  description = "Dashboard Example"
  permissions = "public_read_only"
  // 後略
}
```

また、pageには以下の共通設定がある。

- ```row```/```column```/```height```/```width```: ウィジェットの位置を定義する。縦位置/横位置/高さ、幅を示す
- ```legend_enabled```: 見出しの表示有無
- ```refresh_rate```: 各情報の更新頻度。ミリ秒単位
- ```ignore_time_range```: ダッシュボード右上の全体のタイムレンジに合わせて表示範囲を変更するか、NRQLで設定したタイムレンジで表示するか。全体のタイムレンジがdefaultの場合、またはこの項目をtrueに設定するとNRQLのSINCE, UNTIL句で設定したタイムレンジ固定になる。たとえば、過去のインシデントをすべて表示したいときはここをtrueに設定しないと、直近のものだけが表示されてしまう。

## ウィジェット固有の定義
### Lambdaエラー件数の折れ線グラフ
折れ線グラフでは、以下の設定を行うことができる。

- y_axis_right: グラフの縦軸の第2軸の設定。```y_axis_right_series```で指定した情報に対して適用される。
- units: グラフの単位。今回の例では、少数をパーセント表示に変更している。```series_name```で指定した情報に対して適用される。
- colors: グラフの折れ線の色。```series_name```で指定した情報に対して適用される。

NRQLも書いてある通りで特に難しいことはない。通常のSQLと同様に、SELECT句にはcount()も使えるし、asで別名を付けることもできる(asで指定した別名を、他の設定項目の識別子として```series_name```に指定することも可能)。

```HCL
resource "newrelic_one_dashboard" "example" {
  // 中略
  page {
    // 中略
    widget_line {
      title = "aws.lambda.Count"

      // 共通の設定ここから
      row    = 3
      height = 3

      column = 1
      width  = 6

      legend_enabled = true

      refresh_rate = 60000
      // 共通の設定ここまで

      y_axis_right {
        y_axis_right_zero = false
        y_axis_right_min  = 0
        y_axis_right_max  = 1
        y_axis_right_series = [
          "aws.lambda.Url5xxRate"
        ]
      }

      units {
        series_overrides {
          series_name = "aws.lambda.Url5xxRate"
          unit        = "percentage"
        }
      }

      colors {
        series_overrides {
          series_name = "aws.lambda.Not.Url5xxCount"
          color       = "#0000ff"
        }
        series_overrides {
          series_name = "aws.lambda.Url5xxCount"
          color       = "#ff0000"
        }
        series_overrides {
          series_name = "aws.lambda.Url5xxRate"
          color       = "#bbbbbb"
        }
      }

      nrql_query {
        query = <<EOQ
SELECT
  count(`aws.lambda.UrlRequestCount`) - count(`aws.lambda.Url5xxCount`) as 'aws.lambda.Not.Url5xxCount',
  count(`aws.lambda.Url5xxCount`) as 'aws.lambda.Url5xxCount',
  if( count(`aws.lambda.UrlRequestCount`) = 0, 0, count(`aws.lambda.Url5xxCount`) / count(`aws.lambda.UrlRequestCount`) ) as 'aws.lambda.Url5xxRate'
FROM
  Metric
WHERE
  `collector.name` = 'cloudwatch-metric-streams' AND
  `aws.accountId` = '${data.aws_caller_identity.self.account_id}' AND
  `aws.lambda.functionName` = '${aws_lambda_function.example1.function_name}'
SINCE 6 hours AGO
TIMESERIES 1 minute
EOQ
      }
    }
  // 中略
  }
}
```

### インシデント発生の棒グラフ
棒グラフでは、以下の設定を行うことができる。
なお、このグラフでは、インシデントの状態ではなくて、インシデント発行された時系列を確認することを意図している。

- colors: グラフの棒の色。```series_name```で指定した情報に対して適用される。

こちらもNRQLは至って素直なものだ。```NrAiIncident```のテーブルが、発行やacknowledge、Closeというイベントでもレコード挿入されてカウントされてしまうため、WHERE句で```event```項目を使って絞り込みを行っている。

```HCL
resource "newrelic_one_dashboard" "example" {
  // 中略
  page {
    // 中略
    widget_stacked_bar {
      title = "aws.lambda.InsidentCount"

      // 共通の設定ここから
      row    = 3
      height = 3

      column = 7
      width  = 6

      legend_enabled = false

      refresh_rate = 60000
      // 共通の設定ここまで

      colors {
        series_overrides {
          series_name = "Nr Ai Incidents"
          color       = "#ff0000"
        }
      }

      nrql_query {
        query = <<EOQ
SELECT
  count(*)
FROM
  NrAiIncident
WHERE
  conditionName = '${newrelic_nrql_alert_condition.example.name}' AND
  event = 'open'
SINCE 6 hours AGO
TIMESERIES 1minute
EOQ
      }
    }
  }
  // 中略
  }
}
```

### クローズされていないIssueの概況ビルボード
ビルボードでは、以下の設定を行うことができる。

- critical: 閾値の設定。取得される値がこれを超える場合はビルボードの背景色が赤くなる。

このNRQLは少し複雑で、副問い合わせを使ってクローズされたIssueのIDを集計している。
```NrAiIncident```同様、```NrAiIssue```も状態が変更される都度、レコードが出力される。
このため、全体の中から、クローズされたIDを含まないものを選択するようにしている。

```HCL
resource "newrelic_one_dashboard" "example" {
  // 中略
  page {
    // 中略
    widget_billboard {
      title = "aws.lambda.issues"

      // 共通の設定ここから
      row    = 1
      height = 2

      column = 1
      width  = 2

      legend_enabled = false

      refresh_rate      = 60000
      ignore_time_range = true
      // 共通の設定ここまで

      critical = 0

      nrql_query {
        query = <<EOQ
SELECT
  count(*) as 'Issues'
FROM
  NrAiIssue
WHERE
  contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
  event = 'activate' AND
  issueId NOT IN (
    SELECT
      issueId
    FROM
      NrAiIssue
    WHERE
      contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
      event = 'close'
    SINCE last month
    UNTIL now
    LIMIT MAX
  )
SINCE last month
UNTIL now
EOQ
      }
    }
  // 中略
  }
}
```

### クローズされていないIssueの詳細テーブル
テーブルには、特に固有の設定は無い。
※実際には色々設定可能だが、今回は利用していない。

NRQLは、クローズされていないIssueの概況ビルボードと同じ条件で、SELECT句で出力項目を絞っている。
なお、こちらは複数件の情報が取得できるので、LIMIT句を設定している。

```HCL
resource "newrelic_one_dashboard" "example" {
  // 中略
  page {
    // 中略
    widget_table {
      title = "aws.lambda.issues"

      // 共通の設定ここから
      row    = 1
      height = 2

      column = 3
      width  = 10

      legend_enabled = true

      refresh_rate      = 60000
      ignore_time_range = true
      // 共通の設定ここまで

      nrql_query {
        query = <<EOQ
SELECT
  timestamp,
  issueId,
  title,
  issueLink
FROM
  NrAiIssue
WHERE
  contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
  event = 'activate' AND
  issueId NOT IN (
    SELECT
      issueId
    FROM
      NrAiIssue
    WHERE
      contains(entity.names, '${aws_lambda_function.example1.function_name}') AND 
      event = 'close'
    SINCE last month
    UNTIL now
    LIMIT MAX
  )
SINCE last month
UNTIL now
LIMIT 20
EOQ
      }
    }
  // 中略
  }
}
```

# いざ、動かす！
さて、上記が出来上がったら```terraform apply```してみよう。

以下の通り、ダッシュボード一覧に作成したダッシュボードが表示されるので、ダッシュボード名をクリックする。

![キャプチャ6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/1c44d27a-26fd-642a-1039-4b3a53aacff9.png)

以下のようにダッシュボードで設定した4種類のウィジェットが表示されるはずだ。

![キャプチャ7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/c67def84-fd25-7386-c064-9ec4142cd146.png)

さらに、この状態から、第1回で作成したスクリプトを流してアラートを発生させると、

![キャプチャ8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/a95cd13e-a99a-ace9-14da-c9a3f02f08bf.png)

ビルボードとテーブルにインシデントの状況が載った！
なお、「Page Example2」のタブをクリックすると、テキトーに作成した折れ線グラフも表示される。

これで、メトリクスとインシデントを統合的に監視するためのダッシュボードが作れるようになった！