はじめに
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 を乗り換えた記事 に書きました。
図のとおり、外への疎通確認も Discord への通知も、同じ 1 本の回線を通ります。この構成が、後で書く「通知が外に出ていけない」問題につながります。
検知は 正しかった
Prometheus の ALERTS を見ると、WanDown は 4 時半ごろに発火し、正午の少し前に解消していました。ゲートウェイの自動更新が走ってから、僕が手で直すまで、という障害の区間とほぼ一致します。
障害中は、外への ICMP と HTTPS が全滅し、ゲートウェイだけが応答していました。ルールが想定していたとおりの形です。
監視は、正しい時刻に、正しい理由で鳴っていました。問題はその先にあります。
不通の あいだに 届かないのは 当然
Alertmanager のログには、発火の 5 分後から、5 分おきに次の行が並んでいました。
Notify for alerts failed ... WanDown ... context deadline exceededDiscord への送信がタイムアウトしています。IPv4 が止まっているのだから、これは当然です。
ただ、IPv6 は生きていました。それなら IPv6 で Discord に届かないのか、と考えて DNS を引いてみると、discord.com には AAAA レコードがありません。
$ dig +short discord.com AAAA$ dig +short discord.com A162.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_total | 117 |
alertmanager_notifications_failed_total | 117 |
うち 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 だけを変えたら受理されたので、中身が空なことが拒否の理由だと考えています。
設定を変えたときに、テストのアラートを 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
-
v0.33.1 の値です。v0.34.0 からは、Discord の 400 は
reason="clientError"として数えられます( CHANGELOG の #5332)。 ↩ -
prometheus/alertmanager notify/discord/discord.go(v0.33.1) ↩