フルスタック監視:Prometheus + Grafana + Loki + Tempo + Bark
このページの目次
指標(Prometheus)+ログ(Loki)+トレース(Tempo)の3本線がGrafanaに集約され、アラートはBark経由でiPhoneにプッシュされます。指標はpull(ローカルネットワーク上のnode_exporter)とpush(リモートotel-collector fleet agent、ratholeトンネル+mTLS経由)の2セットに分かれています。ログはjournald → otelcol-logs → Lokiへ、トレースはotelcol-homeのtail sampling → Tempoへ流れます。本インスタンスli-home-0を例に、フルスタックアーキテクチャ、アラートルール、収集手法の比較、タグ付け方針、レベル判定、およびトラブルシューティングの記録をカバーします。
1. 概要
2. 指標収集:2つのモデル
| 従来の pull (node_exporter) | リモート push (metrics-fleet) | |
|---|---|---|
| 適用対象 | ローカルネットワークノード、ローカルホスト | パブリックIPの遠隔サーバー |
| 収集エージェント | node_exporter / LHM exporter | otel-collector-contrib (host_metrics) |
| 指標命名 | node_*, windows_*, lhm_* | system_cpu_*, system_memory_* |
| ネットワーク | Prometheusが能動的に取得、ポート到達可能必要 | リモートが能動的に送信、インバウンドポート不要 |
| 転送のセキュリティ | なし (LAN内では平文) | mTLS + パス制限 (OTLP書き込みのみ許可) |
| 接続方法 | prometheus.yml の static_configs を手動編集 | add-server.sh <sshエイリアス> |
| ダッシュボード | ノードごとに独立したJSON | 単一テンプレート、$host ドロップダウンで切り替え |
| 中継依存 | なし | ratholeトンネル → 中継VPS → 自宅 |
Pull モデル:scrape_configs
Prometheus は15秒ごとにこれらのターゲットを取得します:ローカルホストの node_exporter(:9100)、llama-server(:18080)、Home Assistant(:8123)、Windows マシン(9182/9183)、N100 サイドルーター(:9100)、Xiaomi ルーター(:9184)、セルフモニタリング(:9090)。
Push モデル:OTLP 受信
遠隔サーバーは otel-collector を介して host_metrics を収集し、mTLS 認証と rathole 暗号化トンネルを経て、自宅の Prometheus の remote write エンドポイントに送信します:
Prometheus 設定
out_of_order_time_window: 30mは、パブリックIP経由での到着順序の揺らぎに対応します。
Grafana ダッシュボード
Provisioning ディレクトリ:7つのホストパネル(CPU / GPU / メモリ / ディスク / 冷却、ノードごとにJSON分割)+ 1つのサーバー状態テンプレート($host ドロップダウンで OTLP 送信された host_name を自動検出)+ ネットワーク監視パネル。
パネル式例:
# CPU: 100*(1-avg by(host_name)(system_cpu_utilization_ratio{host_name="$host",state="idle"}))
# メモリ: 100*system_memory_utilization_ratio{host_name="$host",state="used"}
# 稼働検知(push モデルには up 指標がないため、最新送信時刻で代替):
time()-max(timestamp(system_memory_usage_bytes{host_name="$host"}))
3. セキュリティ:CA と mTLS
リモート push リンクのセキュリティは、プライベートCAによって発行されたクライアント証明書によって保証されます:
~/infra/metrics-fleet/ca/
├── make-ca.sh # CA の一度きりの作成 (EC P-256, 10年)
├── issue-server-cert.sh # サーバー証明書 issuance → /etc/caddy/metrics-fleet/
├── issue-client-cert.sh # クライアント証明書 issuance → out/clients/<name>/
├── revoke-client-cert.sh # 失効 + Caddy revoked.caddy の更新 + リロード
└── out/clients/<name>/ # 発行済クライアント証明書 (遠隔機ごとに1セット)
Caddy は 127.0.0.1:9443 で mTLS ゲートウェイとして動作します:クライアント証明書の検証(require_and_verify, CA信頼)、失効リストの参照 → 403、/api/v1/otlp/* のみ許可し strip_prefix → otelcol-home :4318。
各遠隔サーバーのパブリックIPは、ローカルホストとN100の両方のmihomoのDIRECTルールに追加する必要があります。これにより、ratholeが接続とSSHをプロキシ経由ではなく、ネイティブルーティングで処理することを確保します(接続はネイティブルーティングでなければなりません – rathole-tunnel.md 4.3節参照)。
4. アラート:Grafana + Bark による iPhone へのプッシュ
Prometheus が収集を担当し、Grafana が判定と通知を担当します。アラートチェーン:
なぜローカル bark-server を使い、パブリックIP webhook を使わないのか:もしアラート通知が rathole トンネルやパブリックIP経由であれば、トンネルが切断された際にアラートを送信できません。ローカルの bark-server は iPhone と直接接続し、外部依存がゼロで、インフラストラクチャに依存しません。
連絡先と通知ポリシー
# /etc/grafana/provisioning/alerting/contact-points.yaml
contactPoints:
- name: bark-home
receivers:
- type: webhook
settings:
url: "http://127.0.0.1:8085/<token>" # ローカル bark-server、トンネルを迂回
maxAlerts: 2 # APNs 4KB、2件超えると PayloadTooLarge
title: "[{{ .Status }}] {{ .CommonLabels.grafana_folder }}{{ if .CommonLabels.alertname }} · {{ .CommonLabels.alertname }}{{ end }}"
message: "{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}"
policies:
- receiver: bark-home
group_by: # 同種はマージ、異種は個別送信
group_wait: 30s # 初回遅延、同種を待機
group_interval: 5m # グループ内の後続アラート間隔
repeat_interval: 4h # 未解決の場合、4時間ごとに再通知
maxAlerts: 2が鍵 – bark は webhook JSON を APNs に透過的に渡すため、アラート1件あたり約1.1–1.4KB、2件を超えると APNs の4KB上限を超え、bark は直接 500 を返します。
アラートルール (計14件)
| ルール | レベル | 条件 | 沈黙 |
|---|---|---|---|
| サイドルーターダウン | critical | up{job="li-home-router"}==0 | 3m |
| ルーター不通 | critical | ping_loss{target="gateway"}>30% | 2m |
| 外網高パケットロス | warning | ping_loss{target="223.5.5.5"}>20% | 3m |
| TCP再送率高い | warning | Tcp_RetransSegs / Tcp_OutSegs > 10% | 5m |
| DNS 全遅延 | warning | 3ドメインの解決がすべて >100ms | 5m |
| mihomo TUN 無効 | critical | nikki ネットワークインターフェース指標消失 | 2m |
| CN ノード ディスク不足 | warning | ルートパーティション残り <5GB | 5m |
| 給水ポンプ停止 | critical | fan7 RPM <1500 | 5m |
| VRM 過熱 | warning | temp5 >90°C | 2m |
| GPU ホットスポット過熱 | warning | 7900XTX ジョンクション温度 >100°C | 2m |
| N100 CPU 過負荷 | warning | CPU >90% | 5m |
| ローカル exporter 接続切断 | critical | up{job="local-info"}==0 | 2m |
| rathole サービス到達不能 | critical | rathole_service_up < 1 (service タグ別) | 3m |
| 日次レポートメール配信失敗 | warning | Loki:llm-job@daily-report 失敗 または notify-email: 送信失敗 | 1m |
ルールは Grafana provisioning で管理されます (
/etc/grafana/provisioning/alerting/home-network.yaml)、git でバージョン管理され、ルール変更時に Web UI にログインする必要はありません。各アラートはannotations.summaryで現象を記述し、annotations.runbookでトラブルシューティング手順を提供します – アラート通知では summary のみをプッシュし、runbook は Grafana 内で展開して参照します。日次レポート自体はローカルメールで送信されます。notify-emailの失敗時には systemd/Loki がこの Bark アラートをトリガーし、アラートチャネルはメールに依存しません。
指標のトラブルシューティング
- N100 サイドルーターの接続:Prometheus が N100 を scrape するにはローカルプロキシを経由する必要があります。DIRECT ルールを追加して迂回します。
5. ログ監視 (Loki + OpenTelemetry)
「指標はあるがログがない」というギャップを埋めます。ローカルホストのすべてのシステムログ(カーネル + systemd サービス + Docker コンテナ)は、journald → otelcol-logs → Loki → Grafana Explore 検索へと統一されます。
デザイン上の取舍
- OpenTelemetry を選択、Promtail/Vector を使用しない:ローカルホストではすでに otelcol-home が稼働しており、Vector を追加すると収集エージェントの技術スタックが重複します。OTel は遠隔ホストのログ接続をほぼ無償にします(同じ mTLS/rathole/Caddy トンネルを再利用するため)。
- Loki はネイティブ OTLP エンドポイント
/otlp/v1/logsを使用 (旧lokiexporterは廃止)。 - Docker の journald ログドライバへの変更:コンテナログに
CONTAINER_NAMEが付与され journald に格納され、同じjournaldreceiverによって収集される → 実際のコンテナ名があり、単一のデータソースとなる。 - native systemd、
127.0.0.1にバインド、90日保持、ログダッシュボードは作成しない (直接 Explore を使用)。
データフロー
コンポーネント
| コンポーネント | 場所 | 備考 |
|---|---|---|
| Loki 3.7 | /usr/local/bin/loki,conf /etc/loki | :3100,OTLP /otlp/v1/logs,ユーザー loki |
| otelcol-logs | /usr/local/bin/otelcol-contrib(0.154, fleet バイナリを再利用) | unit /etc/systemd/system/otelcol-logs.service, root 不要 (group systemd-journal) |
| Grafana データソース | /etc/grafana/provisioning/datasources/loki.yaml | uid loki-local |
タグ付け方針
低基数はインデックスタグに、高基数は構造化メタデータ(検索可能だがインデックス化しない)に含める:
| インデックスタグ | 意味 |
|---|---|
host_name | resource host.name (全体名、metrics と整合) |
service_name | = host.name、Loki ネイティブのグループ化フィールド |
unit | systemd サービス名; Docker コンテナ = コンテナ名 (OTTL によって CONTAINER_NAME から上書き) |
container | Docker コンテナ名 (コンテナログのみ) |
level | 以下「レベル判定」参照 |
transport | kernel / stdout / syslog / journal / audit |
レベル判定 (混合戦略)
journald の PRIORITY は、ネイティブソース(systemd-journal/syslog/kernel)でのみ真のレベルを表します。コンテナログや生の stdout/stderr の PRIORITY は、単にストリーム(stdout=6/stderr=3)を表すだけで、意味がありません。
- ベースライン:コンテナと stdout 以外のソースは PRIORITY に基づいてレベルを割り当てます (0–2 critical / 3 error / 4 warn / 5–6 info / 7 debug);コンテナログのデフォルトは
info。 - 本文による上書き:OTTL
IsMatchでメッセージ本文から[error]/level=info/"level":"warn"および大文字の独立語WARNING/ERRORを識別します。 - Loki 側で
discover_log_levels: true– Loki がlevel属性を認識し、detected_level(Grafana のレベル可視化用) を設定します。
検索例 (Grafana Explore)
{unit="sshd.service"}
{level="error"}
{container="adguardhome"}
{transport="kernel"}
{service_name="li-home-0"} |= "timeout"
6. ログのトラブルシューティング
- Loki 単一バイナリ ring 自己接続失敗:デフォルトではネットワークインターフェースIPをブロードキャストしますが、gRPC は 127.0.0.1 のみをリスンしているため、接続が拒否されます。
common.instance_addr: 127.0.0.1を必須にします。 - journaldreceiver はログ全体を body(Map) に配置:フィールドは
body["PRIORITY"]であり attributes ではありません。OTTL はまず body から属性を抽出し、最後にset(body, body["MESSAGE"])を実行します。 - service_name のセマンティクス:欲しいのは全体識別子であり、resource レベルです。Loki の
discover_service_nameは resource 属性のみを読み取るため、OTTL resource コンテキストでservice.name = host.nameを設定します。 - Docker コンテナの unit:journald ドライバ下では
_SYSTEMD_UNIT=docker.serviceは機能しないため、OTTL でCONTAINER_NAMEを上書きします。 - PRIORITY を信用できない:adguard/HA/owntracks は info+warning をすべて stderr に書き込むため、PRIORITY はすべて 3 になります。これを信用するとすべて error になってしまいます。
discover_log_levelsを false に設定しないでください – そうしないとdetected_levelが消え、Grafana のヒストグラムが単一の棒グラフになります。 - カラーログ = バイト配列:ANSIを含むMESSAGEはjournaldによってint-sliceとして保存され、OTTLではデコードできません。根本解決 = ソースで色を無効化:コンテナに
NO_COLOR=1+PY_COLORS=0を追加;RustサービスにEnvironment=NO_COLOR=1+RUST_LOG_STYLE=neverを追加。 - node_exporter sgcc 大量出力:
sgcc_ts.promはタイムスタンプ付き(root 権限で所有される孤児ファイル)、textfile collector はサポートしていません。textfile_collector/から移動すれば根治します。 - runlike によるコンテナ再構築でデータ消失:
uvx runlikeは匿名/命名ボリュームに対してエラーを出力するため、docker inspect .Mountsに従ってボリュームを明示的に固定する必要があります。
7. Trace:Tempo + tail sampling
Trace と指標は同じ OTLP エントリポイント (otelcol-home :4318) を共有し、内部でパイプラインごとに分流します:
サンプリング戦略
tail sampling (head sampling ではなく) を使用し、span ツリーが完全に切り捨てられないようにします:
| 戦略 | 条件 | サンプリングレート |
|---|---|---|
| errors-only | status_code == ERROR | 100% |
| latency-sampling | 応答時間 >500ms | 100% |
| probabilistic | その他の正常リクエスト | 10% |
tail sampling は head sampling よりも10秒の意思決定遅延がかかります (span ツリーが揃うまで待ってから決定するため) が、「親 span はサンプリングされたが子 span はサンプリングされていない」という分断が発生しません – これは分散呼び出しチェーンのトラブルシューティングにとって極めて重要です。
Tempo バックエンド
# /etc/tempo/config.yaml
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 127.0.0.1:4317
ingester:
max_block_duration: 5m
lifecycler:
ring:
kvstore:
replication_factor: 1
単一インスタンス inmemory ring (クラスター不要)、720h (30日) 保持。Loki と同じパターン:native systemd、127.0.0.1 にバインド、Grafana Explore で Trace ID に基づく直接検索が可能。
接続方法
アプリケーション側で OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 (ローカル) または https://otlp.liz6.com:9443 (リモート、fleet トンネル経由) を設定します。SDK が自動的に trace を生成します。遠隔アプリケーションは既存の fleet collector の mTLS 証明書を再利用します – 指標とトレースは同じパイプライン、同じセキュリティ体系を共有します。
8. 将来の拡張:リモート fleet ログ
既存の metrics-fleet 転送を再利用します (Caddy の変更は不要、/api/v1/otlp/* は v1/logs をすでにカバーしています):
- リモート otelcol-fleet agent に journald receiver + logs パイプラインを追加 →
otlphttpでotlp.liz6.com:9443へ - 自宅の otelcol-home に logs パイプラインを追加 →
otlphttpで Loki/otlp/v1/logsへ add-server.sh/CA/命名規則をすべて継承し、host_name/service_nameは各ノード名とする
関連ドキュメント
- network-architecture.md — ネットワークトポロジとデーモン
- rathole-tunnel.md — トンネルアーキテクチャ (mihomo を迂回する接続)
- server-lifecycle.md — リモートサーバー接続スクリプト (add-server)