コンテンツへスキップ

Alertmanager の Discord 通知が空の embed で 400 になっていた:監視は5分で気づいたのに1件も届かなかった話

Alertmanager の Discord 通知が空の embed で 400 になっていた:監視は5分で気づいたのに1件も届かなかった話 のアイキャッチ
Contents

はじめに

2026-11-01 の未明、自宅の回線で IPv4 だけが約 7 時間半止まりました。IPv6 は生きていたので、気づいたのは朝になってからです。それも通知ではなく、「なんだか回線が死んでいる気がする」という体感でした。

あとで調べると、自宅の監視(Prometheus と Alertmanager)は、ルールどおり障害が 5 分続いた時点で正しく検知していました。それなのに、Alertmanager からの通知は Discord に 1 件も届いていません。原因は 2 つ重なっていました。

  • 不通のあいだは、通知そのものがインターネット経由なので送れない
  • 復旧したあとも、Alertmanager の通知を Discord が HTTP 400 で拒否していた。こちらは障害と関係がなく、3 か月前に入れた設定の誤りがずっと潜んでいた

この記事では、2 つ目の原因を Alertmanager のソースまで追った経緯と、その修正、そして修正しても残る穴を書きます。回線が止まった原因(ゲートウェイの自動更新)は、メーカーのサポートの回答を待っているので、別の記事で書く予定です。

なお、この記事は調査・事実確認に AI(Claude)を使い、AI が書いた下書きを僕が校正する形で執筆しています。

想定読者

  • Prometheus と Alertmanager で、自宅や小さなチームのシステムを監視している人
  • Alertmanager の discord_configs で、Discord に通知を飛ばしている人。特に、embed を使わずに本文だけで送ろうとしている人
  • 「監視は動いているはずなのに、通知が来ない」という状況を一度でも疑ったことがある人

Prometheus のルールの書き方や、Alertmanager の基本は説明しません。

監視の構成

自宅の Proxmox 上に監視用の LXC を 1 台置き、Prometheus、blackbox_exporter、Alertmanager を動かしています。回線が生きているかは、blackbox_exporter で 2 種類の疎通確認(Prometheus では probe と呼びます)をして判断しています。

  • ICMP: 1.1.1.1 と 8.8.8.8、それにゲートウェイ(192.168.10.1)への ping
  • HTTPS: 1.1.1.1 と dns.google へのリクエスト

回線が死んだと判断するアラート WanDown は、この 2 種類を組み合わせています。

- alert: WanDown
expr: |
max(probe_success{job="blackbox-icmp", instance=~"1\\.1\\.1\\.1|8\\.8\\.8\\.8"}) == 0
and on()
max(probe_success{job="blackbox-https-public"}) == 0
and on()
max(probe_success{job="blackbox-icmp", instance="192.168.10.1"}) == 1
for: 5m
labels:
severity: critical

外への ICMP も HTTPS も全滅していて、ゲートウェイだけは応答する。この状態が 5 分続いたら発火します。ゲートウェイまで応答しないなら、それは回線ではなく家の中の問題なので、別のアラートに任せています。

ICMP と HTTPS の 2 本立てにしたのは、以前 ICMP の疎通確認の仕組みだけが壊れ、このアラートが半日ほど誤報し続けたからです。1 種類の疎通確認だけでは、「回線が死んだ」のか「測る側が死んだ」のかを区別できません。

疎通確認は、すべて IPv4 に固定しています(preferred_ip_protocol: ip4)。HTTPS の宛先のうち dns.google はホスト名なので、IPv6 でも引けます。今回の障害は IPv6 が生きていて IPv4 だけが止まる形だったので、固定していなければ dns.google への疎通確認が IPv6 で成功し、WanDown の条件が崩れていた可能性があります(監視用の LXC から IPv6 で外に出られたかは確かめていません)。回線は v6 プラス(MAP-E)で、IPv4 は IPv6 のトンネルの上を通っています。その構成は ISP を乗り換えた記事 に書きました。

監視の構成図。監視用の LXC の中で、Prometheus が blackbox_exporter に疎通確認を依頼し、WanDown を Alertmanager に渡す。疎通確認(ICMP と HTTPS、IPv4 固定)も Discord への通知も、ゲートウェイから同じ 1 本の回線を通って外に出る。Discord には AAAA レコードが無く、IPv4 でしか届かない

図のとおり、外への疎通確認も Discord への通知も、同じ 1 本の回線を通ります。この構成が、後で書く「通知が外に出ていけない」問題につながります。

検知は正しかった

Prometheus の ALERTS を見ると、WanDown は 4 時半ごろに発火し、正午の少し前に解消していました。ゲートウェイの自動更新が走ってから、僕が手で直すまで、という障害の区間とほぼ一致します。

障害中は、外への ICMP と HTTPS が全滅し、ゲートウェイだけが応答していました。ルールが想定していたとおりの形です。

2026-11-01 の検知と通知のタイムライン。IPv4 の不通と WanDown の発火はどちらも 04 時台から 11 時台まで続き、そのあいだ Discord への送信は 5 分おきにタイムアウトしていた。11:53 ごろの復旧後は 400 で拒否され、届いたのは別経路の Home Assistant からの Recovered だけだった

監視は、正しい時刻に、正しい理由で鳴っていました。問題はその先にあります。

不通のあいだに届かないのは当然

Alertmanager のログには、発火の 5 分後から、5 分おきに次の行が並んでいました。

Notify for alerts failed ... WanDown ... context deadline exceeded

Discord への送信がタイムアウトしています。IPv4 が止まっているのだから、これは当然です。

ただ、IPv6 は生きていました。それなら IPv6 で Discord に届かないのか、と考えて DNS を引いてみると、discord.com には AAAA レコードがありません。

Terminal window
$ dig +short discord.com AAAA
$ dig +short discord.com A
162.159.x.x
...

Discord には IPv4 でしか届かないので、IPv4 が止まった時点で通知の経路は切れていました。

復旧したあとも Discord が 400 で拒否した

おかしいのは、正午の少し前に IPv4 が戻ったあとです。復旧すれば、溜まっていた通知や解消の通知が届くはずでした。ところが、ログにはタイムアウトの代わりに別のエラーが出ていました。

unexpected status code 400: {"embeds": ["0"]}

Discord が、Alertmanager の送ったリクエストを 400 で拒否しています。{"embeds": ["0"]} は、「0 番目の embed が不正」と読めます。

Alertmanager の送信カウンターを見ると、状況はもっと悪いことが分かりました。コンテナを起動した 10-27 以降の数字です。

指標(integration="discord")値
alertmanager_notifications_total117
alertmanager_notifications_failed_total117
うち reason="contextDeadlineExceeded"108
うち reason="other"(400) 1 9

117 回送って、117 回とも失敗していました。成功は 0 回です。 400 が出始めたのは復旧の直後なので、それより前の 108 回はすべて障害中のタイムアウトです。一方、400 の 9 回はどれも復旧後のもので、回線と関係がありません。回線が正常でも、Discord への通知は届かない状態だったことになります。

この間、Discord に届いたのは、Home Assistant の ping の監視が Alertmanager とは別の経路で送っている通知だけでした。復旧の 2 分後に、回線が戻ったことを知らせる「Recovered」が 1 件届いています。障害が始まったことを知らせる通知は、どの経路からも届いていません。

いつから届かなかったのかは分かりません。設定を変えたのは 8 月なので、そこからずっと届いていなかった可能性があります。ただ、それ以前のカウンターはコンテナの再起動で消えていて、確かめられませんでした。

原因は Alertmanager が必ず embed を送ること

8 月の変更では、Discord の通知を embed を使わずに本文(content)だけで送ろうとしていました。Discord のスマホアプリで、embed の中の文字を選択できず、アラートの内容をコピーできなかったからです。

そのときの設定は次のとおりです。

discord_configs:
- title: ''
message: ''
content: |-
{{ if eq .Status "firing" }}🔴 FIRING{{ else }}✅ RESOLVED{{ end }} · {{ .CommonLabels.alertname }}{{ with .CommonLabels.severity }} · {{ . }}{{ end }}
...

設定のコメントには、「title と message を空にすれば embed が消える」と書いていました。これが誤りでした。

Alertmanager v0.33.1 の Discord 通知の実装 2 を読むと、送る JSON の構造体は次のようになっています。

type webhookEmbed struct {
Title string `json:"title"`
Description string `json:"description"`
Color int `json:"color"`
}

title と description の JSON タグに omitempty がありません。そして送信のたびに、embed を 1 つ、条件なしに組み立てています。この部分は、執筆時点の最新版の v0.34.2 でも変わっていません。

w := webhook{
Content: content,
Username: n.conf.Username,
Embeds: []webhookEmbed{{
Title: title,
Description: description,
Color: color,
}},
}

color は、発火中なら赤、解消なら緑、それ以外なら灰色が必ず入ります。つまり title と message を空にしても embed は消えず、title と description が空文字の embed が送られます。 Discord はこれを 400 {"embeds": ["0"]} で拒否していました。Discord のドキュメントには、空の embed を拒否するとは書かれていません。ただ、後で書くとおり title だけを変えたら受理されたので、中身が空なことが拒否の理由だと考えています。

3 つの JSON の比較。8 月の設定は embed なしで送るつもりだったが、v0.33.1 は title と description が空の embed を付けて送り、Discord が 400 で拒否した。title に 1 行入れた修正後の JSON は受理された

設定を変えたときに、テストのアラートを 1 本流していれば、その場で気づけたはずです。8 月の時点では、amtool check-config で文法は確かめていました。ただ、本文だけで届くはずだという思い込みのまま、届くところまでは見ていませんでした。

修正して、テストのアラートで確かめる

embed は消せないので、中身を入れて有効な embed にします。title にアラート名を 1 行だけ入れ、コピーしたい本文は content に残しました。

title: '{{ if eq .Status "firing" }}🔴{{ else }}✅{{ end }} {{ .CommonLabels.alertname }}'
message: ''

amtool check-config で設定の文法を確かめ、Alertmanager に設定を読み直させました。すると、その時点で発火中だった別のアラートが再送され、送信の成功回数(notifications_total から failed_total を引いた値)が 0 から 1 に増えました。続けてテストのアラートを入れると 2 になり、失敗は増えていません。

テストのアラートは、DiscordDeliveryTest という名前で Alertmanager の API に直接入れました。Discord には 🔴 FIRING · DiscordDeliveryTest · info が、embed のタイトル付きで届いています。

これで分かったのは、title さえあれば、description(message)が空でも Discord は受理するということです。message を空にしたこと自体は問題ではありませんでした。

修正しても残る穴

400 は直りました。ただ、今回の障害そのものを通知で知る手段は、まだありません。

回線が 1 本しかない今の構成では、その回線が止まると、回線越しに知らせる仕組みは動きません。今回の 108 回のタイムアウトがそれです。家の中の監視が正しく鳴っても、その声が外に出ていけません。今回は IPv6 が生きていたので、IPv6 で届く中継を挟めば届いた可能性はあります。ただ、回線が丸ごと止まれば同じことです。

定石は、外部のハートビート(定期的な生存報告)です。healthchecks.io のようなサービスに、監視用の LXC から定期的に生存報告を送り、報告が途絶えたら外部側がメールなどで知らせます。報告が「届かなくなった」ことに外側が気づく仕組みなので、回線が死んでも機能します。これはまだ手を付けていません。

alertmanager_notifications_failed_total が増えたら鳴るアラートを足す案も考えました。ただ、今の設定は通知先が Discord の 1 つだけなので、そのアラートの通知も同じ経路を通り、今回の 400 では同じ空の embed で拒否されます。メールなど別の通知先に分ければ、こうした設定の誤りには効きます。それでも回線が止まれば、家から出る通知はどれも届かないので、外部ハートビートの代わりにはなりません。

なお、Proxmox のバックアップの通知は、毎週の成功通知そのものをハートビートとして扱っています( Home Assistant を Proxmox に入れた記事 )。こちらは「来ないこと」に人が気づく前提で、自動で知らせてはくれません。

まとめ

  • 監視は 5 分で正しく検知していた。届かなかったのは通知の経路のほう
  • 不通のあいだは、discord.com に AAAA レコードが無いため、IPv6 が生きていても届かない
  • Alertmanager の Discord 通知は、title と message を空にしても embed を必ず 1 つ送る(v0.33.1 と v0.34.2 で確認)。中身の空の embed は、Discord に 400 で拒否された
  • title を 1 行入れれば、message は空のままでも受理される

いちばんの教訓は、受け取る側が静かでも、送る側が正常とは限らないことです。届いたかどうかは、送った側(Alertmanager のログと alertmanager_notifications_failed_total)にしか出ていませんでした。通知の設定を変えたら、テストのアラートを 1 本流し、届くところまで見る。次は外部ハートビートを入れて、回線が死んだときにも気づけるようにします。

それでは、またね。

footnote

  1. v0.33.1 の値です。v0.34.0 からは、Discord の 400 は reason="clientError" として数えられます( CHANGELOG の #5332)。 ↩

  2. prometheus/alertmanager notify/discord/discord.go(v0.33.1) ↩

X Facebook B! Hatena