Diary of a Perpetual Student

Perpetual Student: A person who remains at university far beyond the normal period

自宅のIoT監視をMackerelで!

arthur-1 Mackerel Advent Calendar 2023ラソン20日目の記事です。Mackerel Meetup #15 が盛況のうちに終わって運営としてはほっと一息。ということで一日遅れてしまっていますがやっていきます。

自宅のIoT機器の監視

Mackerelのdogfoodingの一環として、私は自宅のIoT機器やネットワーク機器の監視をMackerelを利用して行っています。今回はIoT機器の監視の取り組みについてご紹介します。

システム概説

IoT機器のベンダーが提供するAPIにアクセスし、その結果をサービスメトリックとしてMackerelに投稿するアプリケーションを作りました。これはコンテナで動くようになっていて、Raspberry Piを組み合わせて作ったKubernetesクラスタ上でCronJobとして動かしています。

また、これらのアプリケーションを動かすのに必要な基盤の監視も必要です。各ノードにmackerel-agentを入れてシステムメトリックを収集しています。また、最近はOpenTelemetry Collectorを入れて、クラスタのメトリックをMackerelのラベル付きメトリック機能で見られるようにしています。

blog.arthur1.dev

ダッシュボード

これがIoT監視のためのダッシュボードです。現在はスマートロックのSESAMEとスマートリモコンのNature Remoを監視しています。

mackerel-sesame

Arthur1/mackerel-sesameは、SESAME Web APIにアクセスして、スマートロック機器の電池残量や電圧を取得し、サービスメトリックとして投稿するツールです。

github.com

Go言語で作られており、一般的なOSに向けてbuildして動かせるようになっています。また、マルチアーキテクチャのコンテナも用意しており、こちらはGitHubのコンテナレジストリにアップロードしています:Package mackerel-sesame · GitHub

まだREADMEが整備されておらず申し訳ないです。動作させるのには以下の環境変数が必要です。

cmd/mackerel-sesame/main.go#L12-L18

type config struct {
    MackerelAPIKey            string `env:"MACKEREL_API_KEY"`
    MackerelServiceName       string `env:"MACKEREL_SERVICE_NAME"`
    SesameAPIKey              string `env:"SESAME_API_KEY"`
    SesameDeviceUUID          string `env:"SESAME_DEVICE_UUID"`
    SesameDeviceNameForMetric string `env:"SESAME_DEVICE_NAME_FOR_METRIC"`
}

daemonとして動かすようには作っていないので、利用する際にはsystemdのタイマーやKubernetesのCronJobで定期実行させてください。

mackerel-remo

Arthur1/mackerel-remoは、Nature Remo Cloud APIにアクセスして温度や湿度センサーの計測値を取得し、サービスメトリックとしてMackerelに投稿するツールです。

github.com

こちらも動作させるには以下の環境変数が必要です。

runner.go#L7-L13

type RunnerConfig struct {
    MackerelAPIKey          string `env:"MACKEREL_API_KEY"`
    MackerelServiceName     string `env:"MACKEREL_SERVICE_NAME"`
    NatureAccessToken       string `env:"NATURE_ACCESS_TOKEN"`
    RemoDeviceID            string `env:"REMO_DEVICE_ID"`
    RemoDeviceNameForExport string `env:"REMO_DEVICE_NAME_FOR_EXPORT"`
}

温度や湿度をメトリック化しているものの、実はそれらの環境の監視がしたいわけでありません。我が家のNature Remoはたまにインターネットにアクセスできなくなることがある(外出先からエアコンをつけられず困る)ので、サービスメトリックの途切れ監視機能を利用することでNature RemoがCloudと通信できているかを監視できないか試しています。

監視ツールの動作基盤

先ほど紹介したようなツールをKubernetes上で動かしています。KubernetesクラスタにはArgo CDを導入しており、GitOpsでのリリースを実現しています。以下のリポジトリでmanifestを管理しています。

github.com

アプリケーションリポジトリにpushするとコンテナをbuildしてレジストリにpushし、その後さらに上記のマニフェストリポジトリでコンテナのタグを更新するPull Requestを自動で作る機構をGitHub Actionsで構築しています。あとはPull RequestをマージしてしまえばArgo CDの力て自動でクラスタに設定が反映される仕組みです。

CronJobのマニフェストファイルを以下に挙げておきます。Kubernetes上で拙作ツールを動かしたい方は参考にしてみてください。

manifests/iot-monitor/mackerel-remo/cronjob.yml

apiVersion: batch/v1
kind: CronJob
metadata:
  name: mackerel-remo
  namespace: iot-monitor
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: mackerel-remo
              image: ghcr.io/arthur1/mackerel-remo:sha-04ea23f
              env:
                - name: MACKEREL_API_KEY
                  valueFrom:
                    secretKeyRef:
                      name: mackerel-arthur-1
                      key: apikey
                - name: MACKEREL_SERVICE_NAME
                  value: home
                - name: NATURE_ACCESS_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: nature-api
                      key: access-token
                - name: REMO_DEVICE_ID
                  valueFrom:
                    secretKeyRef:
                      name: nature-remo-mini
                      key: device-id
                - name: REMO_DEVICE_NAME_FOR_EXPORT
                  value: remoMini-living
          restartPolicy: OnFailure

クラスタの監視

Raspberry Piで作ったクラスタの監視もMackerelで行っています。

配線をシンプルにするためにPoEで給電しているのですが、PoE+ hatのファンがデフォルトの設定だとうるさくて仕事に困るので、ファンコントローラでかなり絞って運用しています。そこで、CPU温度が上がりすぎていないかを一番気にして監視しています。


いかがでしたか?IoT機器を監視するダッシュボードや自作ツールのご紹介でした。IoT機器に囲まれて暮らしている皆さまもぜひ試してみてください。

mackerel-agentの起動条件

arthur-1 Mackerel Advent Calendar 2023ラソン19日目の記事です。

本日はMackerel Meetup #15です!私は司会進行を務めさせていただきます。当日の申し込みも可能ですので、ぜひお越しください。

mackerelio.connpass.com

Advent CalendarはMeetupの日に合わせた特別なネタを用意していないので、いつも通り書きたいことを書いていきます。

mackerel-agentサービスの起動

Linuxにmackerel-agentをインストールするスクリプトを実行すると、systemdのサービスとして追加されます。

サービスの定義もmackerel-agentのソースコードを見に行くと分かります。例えば、以下のファイルはDebian系のOSで使われるサービスの定義ファイルです。

packaging/deb-systemd/debian/mackerel-agent.service

[Unit]
Description=mackerel.io agent
Documentation=https://mackerel.io/
After=network-online.target nss-lookup.target
...

ここで、Afterの行に着目してください。network-online.targetが含まれていることから、ホストのネットワークが利用可能になるまで、このサービスは起動を待機します。同様に、nss-lookup.targetが含まれているので、DNSが利用可能になるまで待ちます。

mackerel-agentプロセス起動時の処理

なぜmackerel-agentのサービスにはこのような依存関係が必要なのでしょうか?その理由は、mackerel-agentを起動させた後に最初に行う処理が関係していると考えられます。

mackerel-agentは起動時に以下の処理を行います(実際にはもっと複雑なのでかなり簡略化しています):

  • host idファイルがすでに存在する
    • そのホストが現在もMackerelに登録されているかチェックするリクエストを飛ばす
  • host idファイルが存在しない
    • Mackerelにホストを新規登録するリクエストを飛ばす

このチェックが完了し、Mackerel上のホスト情報と結びつけることができてから、常駐ソフトウェアとしての監視のroutineが始まります。もしここでMackerelのAPIにアクセスできないなどの状況になっている場合、mackerel-agentは異常終了します。

ネットワークが利用可能でない状態でサービスが立ち上がってもここの処理で異常終了してしまうため、先ほどのようなサービスの定義になっているというわけです。

agentとしてこの仕様では困る

私は、mackerel-agentが監視エージェントであるからこそ、この仕様は良くないと思っています。

例えば、Mackerelは計画メンテナンスを行うことがあります。すでにmackerel-agentが起動していれば、監視結果を最大6時間分までバッファリングし、Mackerelのメンテナンスが明けてから再送することができます。しかし、メンテ開始時までに起動していない場合は起動時の処理で落ちてしまうので監視を行うことができません。Mackerelのメンテナンス中にスケールアウトするのは避けるべき、ということになります。

メトリックを投稿することとメトリックを取得することは独立した処理であり、メトリックを投稿することができなくてもメトリックは取得できていてほしいです。そういう思想でバッファリングしていると思うのですが、起動時にはそれができていない、ということになります。Receiver・Processor・Exporterとコンポーネントを分けてそれぞれを疎結合に扱おうというOpenTelemetry Collectorの仕組みには改めてよくできているなと思わされます。

もちろん、このような仕様にもメリットがあります。systemctl statusコマンドの結果を見るとmackerel-agentがきちんとメトリックを投稿する準備ができているかわかります。

MackerelのOSSをコミュニティで進化させていきたい

私はmackerel-agentを改修したいと思うものの、これは根本を作り替えてしまうような大工事になることが予想されます。MackerelがOpenTelemetryへと移行していく中、9年以上の歴史があるmackerel-agentの改修に自分がどれだけ手を掛けられるのか分かりませんが、2024年中に解決したい課題の1つとして個人的には捉えています。

mackerel-agentはOSSなので、ユーザーの皆さまからのcontributionも受け付けています。本件に限らず、困り事があればぜひissueやPull Requestを気軽に立ててみてください。基本的に英語でやり取りをしていますが、相手は私か私と同じような血の通った人間ですので、怖がることはないでしょう。

もしどのように改修するか相談したい場合は、Mackerel MeetupやMackerel Drink Upなどのイベントで、実際にOSSのメンテナンスをしている開発者と会って話せます。

チームで監視を育てていくように、コミュニティで監視ツールも育てていきましょう。

なぜMackerelのPromQLでは識別子にドットを使えるのか

arthur-1 Mackerel Advent Calendar 2023ラソン18日目の記事です。明日はいよいよMackerel Meetup #15です!まだ間に合いますのでぜひお越しくださいね。

Mackerelのラベル対応メトリックはPromQLで自在に引ける

Mackerelのラベル対応メトリック機能(現在はプライベートベータ版)を用いると、OpenTelemetry Protocolで投稿したメトリックをPromQLを利用して自在に検索・集計・計算してグラフとして描画することが可能です。PromQLとはOSSの監視アプリケーションであるPrometheusで使われているメトリックのためのクエリ言語です。

最もよく使われるVector Selector記法を用いると、以下の画像のようにラベル名とラベル値を指定してメトリックを絞り込むことができます。

他にも、by修飾子を付与した集約演算子を用いて、単なる集計だけでなくラベルの値ごとの集計ができたり、

メトリックや数量を四則演算できたりと、できることがたくさんあります。

MackerelのPromQLではドットが使える

Prometheusを使ったことがある人はお気づきかもしれませんが、MackerelのPromQLでは、識別子(メトリック名やラベル名)に.を用いることができます。

対して、Prometheusではメトリック名やラベル名に.は使えません。以下の記事の通り、OpenTelemetry MetricsをPrometheusにexportする際にはメトリック名に含まれる.などの文字を_に変換して扱う必要があります。

opentelemetry.io

MackerelのPromQLでドットが使えるのはなぜなのでしょうか?

実装としての理由

技術的な理由を述べると、Prometheusのライブラリをforkし改修しているからです。

以下のようなisAlphaNumericDot関数を作った上で、isAlphaNumeric関数を呼んでいる箇所を置き換えました。

// isAlphaNumeric reports whether r is an alphabetic, digit, underscore, or dot.
func isAlphaNumericDot(r rune) bool {
    return isAlphaNumeric(r) || r == '.'
}

置き換えた場所はいくつかありますが、例を挙げておきます。

promql/parser/lex.go#L723

  func lexIdentifier(l *Lexer) stateFn {
-     for isAlphaNumeric(l.next()) {
+     for isAlphaNumericDot(l.next()) {
          // absorb
      }
      l.backup()
      l.emit(IDENTIFIER)
      return lexStatements
  }

promql/parser/lex.go#L991

  func isLabel(s string) bool {
      if len(s) == 0 || !isAlpha(rune(s[0])) {
          return false
      }
      for _, c := range s[1:] {
-         if !isAlphaNumeric(c) {
+         if !isAlphaNumericDot(c) {
              return false
          }
      }
      return true
  }

テストケースの追加を除くと、差分としては10数行で収まっているはずです。

仕様としての理由

では、なぜforkして改修してまでドットが使えることにこだわったのでしょうか。

答えは、OpenTelemetryのメトリック名や属性名ではドットが使用可能な上、また名前空間を区切る目的でドットを積極的に使う慣習があったことです。例えば、Kubernetesのメトリックをデフォルトの設定のままexportすると、メトリック名や属性名にk8s.pod.phaseのようにドットが含まれています。

そもそも、PromQLを選択した理由の一つは、すでにラベル付きのメトリックのクエリ言語として成熟していて、これを活用することでリリースまでの速度を短縮できるからでした。

我々はPromQLに対応したいのではなく、OpenTelemetryに対応したいのです。OpenTelemetryの規格に沿った標準のメトリックを、素のOTLP exporterでexportできない *1 のは不便だと考え、ドットを使えるように手を入れることにしました。もしその手段さえ取れなかったら、他の言語を選択するか、あるいは自作していたかもしれません。

もっとも、OpenTelemetryの仕様ではメトリック名に-も利用可能ですが、こちらはMackerelのPromQLでも引き続き扱えません。これは減算の算術演算子と衝突してしまうからです。しかし、利用可能とはいえ積極的に使われている様子は見られないため、こちらの対応は見送りました。

forkしたライブラリの継続的な運用が難しいことなどから、実は開発チームの中でドット対応は切っても良いのではないかという議論も起こっていました。私がその時粘っていなかったらドットは使えなくなっていたかもしれませんし、今後も状況によっては対応しなくなるかもしれません。

ラベル付きメトリック機能のプライベートベータ版のユーザーを募集しています

冒頭で述べた通り、ラベル付きメトリック機能は現在プライベートベータ版となっています。OpenTelemetry MetricsをMackerelにエクスポートしてみたい人、PromQLによるメトリックのクエリを試してみたい人は、ぜひ以下のフォームからお申し込みください。

docs.google.com

*1:厳密にはexportしようものならできるがメトリックを引くことができない

SendGrid Event Webhookをcloudwatch-logs-aggregatorでメトリック化する(後編)

arthur-1 Mackerel Advent Calendar 2023ラソン17日目の記事です。(12/17に投稿予定でしたが都合がつかず本日12/18の投稿となります。)

以下の記事の後編として、cloudwatch-logs-aggregatorを使いたくなる用途やその未来、またcloudwatch-logs-aggregatorに求める機能拡張について書きます。

blog.arthur1.dev

1分未満の頻度で発生するイベントを集計してメトリック化したい

今回はSendGrid Event Webhookの情報をメトリック化する手法について紹介しました。しかし、ただメトリック化するだけなら、受け取ったWebhookの情報をもとににCloudwatch Logsを挟まずに同期的にメトリックを投稿してしまえば良いのでは、と思った方もおられるでしょう。

Mackerelのメトリックは1分粒度の解像度となっています。仮に1分間に2度メトリックを投稿しても、片方の情報は失われてしまい、すでに投稿された値との平均値や合計として投稿されるような挙動もありません。

ではSendGrid Event Webhookはどうかというと、Event1回につき1リクエストというほどではないですが、流量が多いと1分間の間にたくさんのリクエストが送られるようです。

Event Webhook - ドキュメント | SendGrid

イベントは現状30秒毎またはバッチサイズが768KBに達するか、いずれか早いタイミングでPOSTされます。これはサーバごとのため、Webhook URLは毎秒数十回~数百回のPOSTを受信する場合があります。

そこで、1分間に起こる多数のイベントを1分間の集計値としてMackerelに投稿するために、値をある程度保持しておく必要があります。Redisなどのデータベース持っておいて定期的にflushしつつ集計してMackerelに送るのも一つの手段ですが、Redisを用意するのは単純に高コストです。また、Webhookを受けるサーバーのインメモリに持っておくと、スケールアウト時に困ります。

そのためのバッファとして今回Cloudwatch Logsを選択したというわけです。cloudwatch-logs-aggregatorは単に既存のログを集計してメトリック化する目的だけでなく、高頻度で起こるイベントなどの情報を1分間隔で集計してMackerelに投稿するための手段としてもとても便利に使えます。

同様の手段は他にもある

同様に、1分粒度の集計値にしてからMackerelに投稿する手段はいくつかあります。

CloudWatchではないログ基盤でも同様に利用できる方法として、fluentd-plugin-mackerelがあります。バッファリングの役割をFluentdが担う形で、基本的な雰囲気はcloudwatch-logs-aggregatorと変わらないと思います。

github.com

2022年秋に開催されたMackerelチームの開発合宿で生まれたmackerel-statsdというアプリケーションもあります。 StatsDプロトコルでメトリックを収集し、集計してからMackerelに投稿するものです。

github.com

そして、現代のメトリック収集と言えばOpenTelemetry Collectorでしょう。mackerel-statsd同様にStatsDプロトコルでメトリックを集計するreceiverであるStatsD Receiverがopentelemetry-collector-contribに入っています。

github.com

Mackerelは(現在クローズドベータ版としての提供ですが)OTLP/gRPCプロトコルをサポートしており、OpenTelemetry Collectorで収集したメトリックをラベル対応メトリックとしてexportすることが可能です。

docs.google.com

また、将来はStatsDなどの特定のreceiverに頼らずにメトリックを集計するprocessorが実装されcontribの中に入るかもしれません。GitHubMetric Aggregation ProcessorのProposalが出ています。

cloudwatch-logs-aggregatorのここが惜しい

SendGrid Event Webhookでは、以下のようなイベントのJSON Objectが配列となって送られてきます:

{
  "email":"john.doe@sendgrid.com",
  "timestamp": 1337197600,
  "smtp-id":"<4FB4041F.6080505@sendgrid.com>",
  "sg_event_id":"sendgrid_internal_event_id",
  "sg_message_id":"sendgrid_internal_message_id",
  "event": "processed"
}

ここにtimestampというフィールドがありますが、今回作ったsendgrid-events-to-mackerelではこの値を尊重していません。ログに書き込まれた時刻をもとにCloudwatch Logsに問い合わせを行いメトリックにするための情報を得ています。

しかし、イベントが起こった時刻とWebhookを受け取ってログが書き込まれる時刻は一般に異なります。1分未満の誤差であれば気にすることはないのですが、大きな差が生じた場合には意図しない形でメトリック化されてしまうでしょう。たとえばSendGridの障害でWebhookの送信が滞留した場合、実際にイベントが起こった時刻とログの時刻に大きなずれが生じます。その結果グラフにも影響が現れるというわけです。

我々はログを書き込んだ時刻ではなくイベントが起こった時刻が知りたいです。このように自分たちが制御できない事象でメトリックが乱れてしまうと監視対象にするには心許ないです。

さて、cloudwatch-logs-aggregatorが時刻でログを絞り込む実装は以下のようなコードになっています。

mackerelio-labs/mackerel-monitoring-modules/cloudwatch-logs-aggregator/lambda/main.go#L270-L281

func withFilterByTimeRange(query string, timeRange *QueryTimeRange) string {
    var b strings.Builder
    b.WriteString("filter ")
    startMillis := timeRange.StartTime.UnixNano() / int64(time.Millisecond)
    b.WriteString(strconv.FormatInt(startMillis, 10))
    b.WriteString(" <= tomillis(@timestamp) and tomillis(@timestamp) < ")
    endMillis := timeRange.EndTime.UnixNano() / int64(time.Millisecond)
    b.WriteString(strconv.FormatInt(endMillis, 10))
    b.WriteString(" | ")
    b.WriteString(query)
    return b.String()
}

@timestampでフィルタしているところを拡張し、@timestampである程度のフィルタをかけて検索対象を絞った上で、実際のタイムスタンプは構造化ログの他のフィールドを見にいくようにして分ごと集計するような機能拡張ができたら、より便利に使えるかもしれません。

SendGrid Event Webhookをcloudwatch-logs-aggregatorでメトリック化する(前編)

arthur-1 Mackerel Advent Calendar 2023ラソン16日目の記事です。(12/16に投稿予定でしたが都合がつかず本日12/18の投稿となります。)

cloudwatch-logs-aggregator

Mackerelはcloudwatch-logs-aggregatorというモニタリングのための便利ツールを公開しています。Amazon Cloudwatch Logsのログを集計してMackerelにサービスメトリックとして投稿できるツールです。

github.com

Terraformのmoduleとして公開されているので、どんなログをどのように集計してメトリック化するか決めてしまえば、あとはTerraform moduleに具体的な設定をvariableとして渡してあげるだけで必要なリソースや設定を作成して動かすことが可能です。

今回はこのツールの具体的な利用例として、SendGrid Event Webhookの情報をサービスメトリック化してMackerelに投稿する事例をご紹介します。

作ったもの

SendGridのEvent Webhookとは、メールの配信やユーザーの行動に関するイベントを指定したURLに送信し続けることができる機能です。

sendgrid.kke.co.jp

このWebhookの情報から、以下のように配信に関するイベントの発生件数をメトリック化するものを作りました。

Mackerelにメトリックとして投稿することで、メール送信が一定数滞留しているときにアラートを出すといったことが可能になります。また、SendGridのダッシュボードではこういった統計情報が1時間ごとの粒度でしか見れないのですが、この仕組みでは1分粒度のメトリックとして閲覧することが可能になります。よりリアルタイムにモニタリングできるというわけです。

ソースコードはこちらになります:

github.com

構成

今回のアプリケーションは主に2つのmoduleで構成されています:

sendgrid-webhook-receiverはSendGridのWebhookを受け取るHTTPサーバで、受け取ったイベントごとの総数をCloudwatch Logsに構造化ログとして書き込む役割を担っています。今回はAPI Gateway (HTTP API)とLambdaを利用して作りました。

sendgrid-webhook-logs-aggregatorは、書き込まれたログを一定間隔で集計してMackerelにサービスメトリックとして投稿する役割を担っています。この部分は特に新しくアプリケーションを書き起こしたわけでなく、mackerelio-labs/mackerel-monitoring-modulesで配布されているcloudwatch-logs-aggregatorのmoduleに設定を加えてapplyしただけのものになっています。

もし監視したい先のアプリケーションがすでにあって、同じようにcloudwatch-logs-aggregatorを使いたいだけなら、差分としてはリポジトリの以下のファイルだけがあれば同じような仕組みを作れるはずです。

  • terraform/main.tf
  • terraform/sendgrid-webhook-logs-aggregator/main.tf
  • terraform/sendgrid-webhook-logs-aggregator/variables.tf

使い方

本アプリケーションを動かしてみたい方は以下のようにすると良いでしょう。Go 1.21.4及びTerraform 1系が必要です。(実験的な実装であり本番環境での利用はおすすめしません。)

リポジトリのclone

git clone https://github.com/Arthur1/sendgrid-events-to-mackerel.git
cd sendgrid-events-to-mackerel

Lambdaファイルのbuild

make build/sendgrid-webhook-receiver-lambda/lambda.zip

Terraformの適用

cd terraform
vim main.tf

「# 以下の値は利用者の環境に合わせて設定すること」の部分を書き換えます。

module "sendgrid_webhook_logs_aggregator" {
  source                = "./sendgrid-webhook-logs-aggregator"
  region                = local.region
  target_log_group_name = module.sendgrid_webhook_receiver.lambda_log_group_name
  # 以下の値は利用者の環境に合わせて設定すること
  # メトリックとして投稿する先のMackerelのサービス名
  mackerel_service_name = "Hoge"
  # MackerelのAPIキーが格納されているAWS Systems Managerパラメータストアのパラメータ名
  mackerel_api_key_name = "/mackerel.io/hogehoge/apikey"
}

その後、terraform applyでリソースが作成されます。

terraform init
terraform apply

AWSのコンソールなどから、API GatewayのURLを取得しておきましょう。

SendGrid Event Webhookの送り先に登録

SendGridのコンソールの左メニューから「Mail Settings」→Webhook Settingsブロックの中の「 Event Webhooks」と遷移します。

Create new webhooksボタンを押して、以下のように設定します。URLを入力するところでは、先ほど取得したAPI GatewayのURLに /sengrid/events とパスをつけて入力してください。

ここまでできたら、配信イベントが起きるたびに定期的にWebhookが送信され、最終的にイベントをメトリックにしてMackerel上で閲覧できるようになるはずです。

実装の簡単な解説

sendgrid-webhook-receiverのサーバーの中で、受け取ったWebhookのデータの中身を見て、イベント種別ごとのイベント数を数えて構造化ログとして出力しています。Go言語のバージョン1.21以降ではgolang.org/x/exp/slogパッケージを利用することで構造化ログの出力ができます。

internal/server/handler.go#L45-L52

logger.Info(
  "sendgrid delivery events count",
  "processedCount", eventsCountByType.DeliveryProcessed,
  "droppedCount", eventsCountByType.DeliveryDropped,
  "deliveredCount", eventsCountByType.DeliveryDelivered,
  "deferredCount", eventsCountByType.DeliveryDeferred,
  "bounceCount", eventsCountByType.DeliveryBounce,
)
{
  "time":"2023-12-18T00:00:00.0000+09:00",
  "level":"INFO",
  "msg":"sendgrid delivery events count",
  "processedCount":3,
  "droppedCount":0,
  "deliveredCount":3,
  "deferredCount":0,
  "bounceCount":0
}

(読みやすいように改行やインデントを入れています。)

これを集計するCloudwatch Logs Insightsのクエリは以下の通りです。

terraform/sendgrid-webhook-logs-aggregator/main.tf#L25-L28

filter level = "INFO" and msg = "sendgrid delivery events count"
| stats sum(processedCount) as `~processed`, sum(droppedCount) as `~dropped`, sum(deliveredCount) as `~delivered`, sum(deferredCount) as `~deferred`, sum(bounceCount) as `~bounce`

たとえば、先ほど挙げた例と同じログが2件あったら

{
  "~processed": 6,
  "~dropped": 0,
  "~delivered": 6,
  "~deferred": 0,
  "~bounce": 0
}

という結果が得られるでしょう。このような集計を1分ごと行ってMackerelにメトリックとして投稿しているというわけです。

また、Webhookからイベントが全く送信されずログが存在しない場合、メトリックが途切れたのではなくイベントの数が0件だとみなしたいはずです。そうしたい場合には以下のようにメトリックのデフォルト値を指定して0埋めすることができます。

terraform/sendgrid-webhook-logs-aggregator/main.tf#L30-L36

  default_metrics = {
    "${local.metric_name_prefix}.delivery_events.processed" = 0
    "${local.metric_name_prefix}.delivery_events.dropped"   = 0
    "${local.metric_name_prefix}.delivery_events.delivered" = 0
    "${local.metric_name_prefix}.delivery_events.deferred"  = 0
    "${local.metric_name_prefix}.delivery_events.bounce"    = 0
  }

次回予告

次回の後編では、なぜcloudwatch-logs-aggregatorを使うのか、cloudwatch-logs-aggregatorに求める改良案などを書いていきます。

Mackerel AzureインテグレーションSDK移行の裏側

arthur-1 Mackerel Advent Calendar 2023ラソン15日目の記事です。(12/15に投稿予定でしたが都合がつかず本日12/17の投稿となります。)

Azure SDKライブラリの更新が必要になった

MackerelはAzureインテグレーションという機能を提供しています。我々が運用するクローラーで定期的にAzureのAPIにアクセスし、そこから得たリソースやモニタリング情報をMackerel上のホストやメトリックとして投稿するサービスです。この機能によって、mackerel-agentを導入できないようなフルマネージドのPaaSなどもMackerel上で監視することができます。

このクローラーはその役割として求められる性質から、非同期処理をスマートに扱えるGo言語で作られており、Azureが提供するGo言語向けのライブラリを利用しておりました。しかし、使っていたライブラリが2023年9月にサポート終了となることが発表されました。

azure.microsoft.com

Mackerel開発チームでは株式会社アイレット様のご協力をいただきながら、利用しているライブラリを新しいものに置き換えることにしました。

cloudpack.jp

今回は、サービスを無停止のままSDK移行を完了したその裏側について、具体的なモニタリング手法に着目してご紹介します。

SDK移行の進め方

Azureクローラーが行う処理のうち、SDKを置き換えることで書き換えなければいけないものは主に以下の3つです:

  • 認証
  • リソースの取得(サービスごと別個の処理)
  • リソースのメトリックの取得(サービスに関わらず共通)

このうち、認証とメトリックの取得に関してはAzureのサービスに依らず共通のため、まずこちらの部分を新しいSDKを用いた実装に切り替えることにしました。それが完了してから、Azureのサービスごと実装しているリソースの取得部分について、1サービスずつ新しい実装に置き換えていくことにしました。

リリースフラグの導入

きちんと動作テストを行なってからリリースに臨むものの、ライブラリを乗り換えることでバグが埋め込まれてしまう可能性があります。また、リリース後すぐにバグに気づければ直ちにロールバックすれば良いのですが、特定の条件でのみ発生する場合にはリリース後すぐに気付けない場合があります。気づいた頃にはすでに他のサービスの実装についても改修が進んでいるため単純なロールバックは難しく、またrevertするには時間が掛かるということもあるでしょう。

そこで、サービスごとのリリースフラグを導入することにしました。サービスごとに旧実装を用いるか新実装を用いるかを切り替えられる制御盤を作り、この制御盤が更新されたらSlackに通知するようにしました。

不具合が起きてもフラグ更新通知の時刻を遡ることで、SDK移行のリリースが原因であることが分かりやすくなります。また、不具合が起きた時に分かりやすいGUIでフラグを変更することですぐに特定のサービスだけ実装を巻き戻すことができます。

さらに、単なるON/OFFだけでなく、オーガニゼーションの範囲を絞ってリリースすることもできるようにしました。本番環境で仮リリースして社内のAzureのリソースで様子を見てからユーザー全体に向けてリリースすることが可能になりました。

サービスごと影響を確認できるダッシュボードを作成

従来からサービスメトリックとして、Azureのサービスごとのクローラーに関する様々なメトリックを投稿してグラフとしてダッシュボードに表示していました。

画像はイメージです

しかし、今回のSDK移行ではサービスごと作業を進めていく関係で、特定のサービスにのみ着目したいことがありました。たとえば、グラフに全ての系列が表示されていると、大きい数量のメトリックのせいで本来見たいサービスの微小な変化に気付けないことがあるでしょう。そこで、以下のようにサービスを1つ絞って見たいメトリックが見れるようなダッシュボードを作りました。

サービスごと同じようにグラフがダッシュボードに並ぶ(メトリックのキーだけが異なる)という性質から、Terraform Provider Mackerelを活用して機械的に作って行きました。以下の記事でdynamic blockを活用してウィジェットを複製する手法を紹介しています。

blog.arthur1.dev

ログやトレースも充実させる

SDK移行に伴う書き換えの影響で、一部の処理が遅く満足にjobを捌けなくなるなどの問題も起こっていました。そこで、SDK移行作業をトラブルなく完遂するだけでなく今後も詳細な調査に役立つだろうということで、ログやトレースを充実させていきました。大きな変化やアラートはメトリックで気付き、その後の調査にはログやトレースを用いるという役割分担です。


いかがでしたか?こういった事例を詳細に表に出す機会があまりないので、今回ご紹介させていただきました。Mackerel開発チームではMackerelの信頼性を落とさずに機能開発や保守を行うための工夫を凝らしており、またそういった工夫がMackerelに新機能を追加するアイデアが生まれるきっかけになっています。

ドリルダウンの最初の一歩を支援するサービスマップを作りたい

arthur-1 Mackerel Advent Calendar 2023ラソン14日目の記事です。

Mackerelにサービスマップ機能が欲しい

私はSRE NEXT 2023で当日スタッフを務めており、その関係でいくつかのセッションを拝聴させていただきました。

そのうち、Sansan株式会社様の「勘に頼らず原因を⾒つけるためのオブザーバビリティ」という発表は、監視サービスの作り手である身として非常に参考になりました。

この発表では、複雑なシステムでデバッグを行う理想的な手段としてドリルダウン探索を挙げています。勘と経験に頼るのではなく、ある情報を見て全体から原因となる場所を絞り込むというのをどんどん続け、最終的に原因に辿り着くという手法です。具体的として、まず最初にメトリクスのアラートで問題を認識してから、サービスマップを見ることで問題の起こったサービスを特定する(その後は特定したサービスでさらにドリルダウンしていく)という方法が述べられていました。

また、Mackerel Drink Up #12ではユーザーの皆さまにLTをしていただき、とても盛況でした。

mackerel.io

その中で特に盛り上がったのが、問題の発生源を特定するために作られた自作ツールのデモでした。その体験に感動したとともに、Mackerelの機能として提供できていないことをとても悔しく思いました。

現在のMackerelにはトレースを溜め込み分析する機能がないのですが、メトリックに関しては頑張れると思っています。一般にLatencyやAvailabilityには依存関係があるはずです。ロールやサービス同士の関係が図として描画され、そこにアラートやメトリックが表示できれば、複雑なシステムでも問題となる箇所を速やかに特定できるのではないかと考えています。

当然自分もプロトタイプを作ってみたくなる(失敗)

こんな機能として作れないかなと思って、Advent Calendarに合わせてプロトタイプを作ってみようとしましたが、残念ながら現在はまだできていません。

実装の方針として以下のようなものを考えていました。

  • サービス・ロール・ホスト間の関係を図式するためにMermaidを用いる
  • Mermaid記法に独自記法を埋め込むことで、図形をサービス・ロール・ホストと紐付けられる
  • MackerelのAPIからメトリックやアラート情報を取得し、Mermaidの図定義を改変して情報を掲載したりスタイルを変えたりする

しかし、現在のmermaid-jsには抽象構文木を操作させるようなAPIが用意されておらず、やむなく断念したという形です。こんな方法ならすぐ作れるんじゃないかというアイデアをお持ちの方はぜひ私に入れ知恵してください。