CloudWatch Logs Insights を使って Mackerel 上にアプリケーションのメトリック監視環境を手早く構築する

この記事は Mackerel Advent Calendar 2021 の 15 日目の記事です. 昨日は id:kazeburo さんの mkr plugin install 時の403 API rate limit exceededエラーを回避する方法 でした.


こんにちは id:susisu です. 普段は Mackerel 開発チームでアプリケーションエンジニアをしています.

先日 mackerelio-labs にて cloudwatch-logs-aggregator という Terraform モジュールをひっそりと公開しました.

github.com

この記事では, このモジュールについてと, これを使って AWS 環境で動作するアプリケーションのメトリックの監視環境を手早く Mackerel 上に構築する方法を紹介します.

アプリケーションのメトリックについて

(ここでアプリケーションとは, 汎用的なシステムやミドルウェアとは区別された, ユーザー独自のプログラムを指すこととします.)

動作しているアプリケーションの様子を把握するためには, アプリケーションの可観測性 (observability) を高めることが重要です. 可観測性を成り立たせるための要素としては, 一般的にはログ・メトリック・トレースの 3 つが挙げられますが, このうちメトリックは, 時系列で傾向を把握したり, 定量的な指標を元にアラートを上げたりするのに最も適しています.

典型的なアプリケーションのメトリックとしては, 以下のようなものが当てはまります.

  • 処理されたデータの数
  • データの処理にかかった速度
  • 発生したエラー数

こういったものはログを用いても簡単な監視を行うことはできますが, 割合のようにより複雑な指標に基づいて監視を行ったり, 時間的な変化を把握したりするためには, やはりメトリックの形が便利です.

アプリケーションからメトリックを出力する

アプリケーションからメトリックを出力して利用するためには, 通常アプリケーションに対してなんらかの計装 (instrumentation) が必要になってきます.

例えば最も単純に, アプリケーションから直接 Mackerel にメトリックを投稿するのであれば,

  • メトリックを記録するための仕組みを作る
  • 定期的に Mackerel の API を呼び出してメトリックを投稿する

といった変更をアプリケーションに対して加えることになるはずです.

一方でこういったメトリック出力のための計装は, ログの出力の場合と比較するといくらか複雑です. メトリックの場合, 記録のためにアプリケーションに状態を持たせたり, 投稿のために API の呼び出しのような実装が必要になりますが, ログであれば状態は必要なく, 出力先も標準出力で良いなど, いくぶん簡単な実装で済むはずです.

CloudWatch Logs にアプリケーションのメトリックを記録する

ここではできるだけ手っ取り早くプリケーションのメトリックを記録したい, ということでログの仕組みを利用してしまいましょう.

まずメトリック (特に counter や histogram と呼ばれるような種類のもの) は, 値を含んだ構造化ログを計測のサンプルごとに出力することとします.

例として, なんらかのバッチ処理で処理されたデータの件数のようなメトリックを記録するのであれば, 処理の実行ごとに一行ずつ以下のようなログの出力が想定されます.

{"level":"info","msg": "data processed","count":42}
{"level":"info","msg": "data processed","count":94}
...

このようにして出力されたログは, CloudWatch Logs へ集約することとします. アプリケーションが AWS 環境, 特に ECS や Lambda などで動作している場合であれば, このための設定は簡単に行えるはずです.

CloudWatch Logs に出力されたログに対しては, Insights のクエリを使って高度な集計や分析を行うことができます. このクエリは上記のようなログを集計してメトリックに変換するのに十分な力を持っています.

ここで特に重要なのが stats コマンドで, これはログに含まれる値を使って, count(), sum(), avg() といった統計的な値を計算することができます. 上のバッチ処理のログの例であれば, 例えば期間中の合計処理件数を以下のようなクエリで求められます.

filter msg = "data processed"
| stats sum(count) as processed_count

あとはこのようなクエリを定期的に実行し, Mackerel にメトリックとして投稿するようにすれば, アプリケーションのメトリックの監視・可視化のための準備が整うはずです.

cloudwatch-logs-aggregator を使ってサービスメトリックを投稿する

ここではじめに紹介した cloudwatch-logs-aggregator の出番です.

cloudwatch-logs-aggregator は 2 つの Terraform モジュールから構成され, それぞれ以下のような役割を持っています.

  • cloudwatch-logs-aggregator/lambda: CloudWatch Logs Insights のクエリを発行し, Mackerel にサービスメトリックを投稿するための Lambda 関数の作成
  • cloudwatch-logs-aggregator/rule: 上記の Lambda 関数を実行するパラメータの設定と, 定期的に実行するための EventBridge (CloudWatch Events) のルールの作成

詳しい利用方法については上記リポジトリの README に譲りますが, これらのモジュールを用いることで, CloudWatch Logs のログからメトリックを生成し, Mackerel にサービスメトリックとして投稿する仕組みを簡単に構築することができます.

# Lambda 関数
module "cw_logs_aggregator_lambda" {
  source = "github.com/mackerelio-labs/mackerel-monitoring-modules//cloudwatch-logs-aggregator/lambda?ref=v0.1.0"

  # ...
}

# EventBridge のルール
module "cw_logs_aggregator_rule_my_batch_job" {
  source = "github.com/mackerelio-labs/mackerel-monitoring-modules//cloudwatch-logs-aggregator/rule?ref=v0.1.0"
  
  function_arn = module.cw_logs_aggregator_lambda.function_arn

  # 指定したクエリを発行し
  query = "filter msg = \"data processed\" | stats sum(count) as processed_count"

  # Mackerel にサービスメトリックとして投稿
  service_name = "my-service"

  # ...
}

これで Mackerel にメトリックが投稿できたら, あとはで監視ルールを設定したり, カスタムダッシュボードを作って可視化するだけです!

cloudwatch-logs-aggregator について

この CloudWatch Logs からメトリックを生成する仕組みは Mackerel 開発チームでも実際の運用に使われており, cloudwatch-logs-aggregator はそれを再利用可能な形にして公開したものです.

上ではアプリケーションのメトリックの監視についての使い方を紹介しましたが, ミドルウェアのログからメトリックを生成したり, チェックプラグインを使ったログ監視の代替といった用途にも利用できるかと思います.

現在 cloudwatch-logs-aggregator はアルファ版という形で公開しています. Mackerel の活用により役に立つ機能を提供できるようブラッシュアップしていきたいと考えていますので, 是非ご試用いただき, リポジトリの Issue や Mackerel ユーザーグループの Slack チャンネル #cloudwatch-logs-aggregator などからフィードバックをいただけるとありがたいです.

利用にあたっては以下の点にご留意ください.

  • 上記の通りアルファ版として提供されます. 仕様は今後のバージョンアップで大きく変更される可能性があります
  • CloudWatch Logs Insights のクエリ発行時に AWS の利用料金が発生します (2021 年 12 月現在, 東京 (ap-northeast1) リージョンでは 1 GB あたり 0.0076 USD)

まとめ

  • cloudwatch-logs-aggregator というモジュールを公開しました
  • これを使って, アプリケーションのメトリックの監視のための準備を手早く行う方法を紹介しました
  • 機能をより良くしていくため, フィードバックを募集しています 🙏

TypeScript をより安全に使うために まとめ

こういう一連の記事を書きました.

susisu.hatenablog.com susisu.hatenablog.com susisu.hatenablog.com

TypeScript の型安全性

TypeScript の型システムは健全ではありません. TypeScript Design Goals にある通り, そもそも言語設計の段階で完璧な型安全性は目標になっておらず, 既存の JavaScript の言語仕様や資産を活用しやすいように, 生産性や利便性とのバランスをとることを目標としています.

では TypeScript の型システムではどの程度の安全性が保証されるのでしょうか?

型安全性を第一の目標に置いていないとはいえ, TypeScript がまったく見当違いな型検査をしているというわけではありません. したがって, ある制限された範囲内であれば, 安全性を担保するのに十分な効果を発揮します.

TypeScript で書かれたプログラムがどの程度安全になるのかは, このような安全性の確保された範囲をいかに見極めて逸脱しないかという部分に大きく依存することになるでしょう.

型システムの落とし穴

TypeScript によって安全性が保証される範囲を知っているというのは, 裏を返せばどういった箇所に危険な落とし穴があるのかを知っているということでもあります. 落とし穴をすべて避けて歩くことができれば, 自然と安全なプログラムが完成しているはずです.

一方で落とし穴を個別に把握していくというのは大変な作業です. というのも, TypeScript の落とし穴は他言語と比較しても数が多く, また JavaScript の構文やライブラリを再利用している都合上, 必ずしも危険なものが危険には見えないといったこともあるためです. 実際に中〜上級者レベルであっても把握していない落とし穴があることは珍しくなく, このことはしばしば TypeScript が難しい言語だと評価される原因にもなっているように思います.

落とし穴の多い機能のカテゴリ

ここで重要になってくるのが, 落とし穴は TypeScript の言語機能にまばらに存在しているのではなく, 基本的には特定のカテゴリに集中して存在しているということです. したがって, すべての落とし穴を個別に把握していなくても, 落とし穴の多い機能のカテゴリのことを知っていれば, それらを避けることでプログラムの安全性を高めることができるはずです.

落とし穴の多いカテゴリは, 目に見えて危険な anyas といった脱出ハッチを除けば, あまり認識されていないように個人的には感じています. そこでこれまでの記事では, 私個人が比較的よく見かける (注意せず入っていく人をよく見る) 3 つのカテゴリを紹介してきました.

もちろんこれらが全てではなく, マイナーな安全でない例などを挙げればキリがないのですが, プログラムの安全性を高めていく上で anyas を避ける次の段階として, これらのカテゴリに注意するというのは悪い選択ではないと思います.

というわけで

JavaScriptThe Good Parts を選んで使わなければいけない言語であるのと同様に, TypeScript もまた The Good Parts を選んで使っていかなければその力を最大限活かすことができません. やっていきましょう.

おまけ (追記)

こちらもあわせてどうぞ.

susisu.hatenablog.com