フルスタック監視:Prometheus + Grafana + Loki + Tempo + Bark

27 分で読了
このページの目次

指標(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. 概要

フルスタック監視データフロー ローカルとリモートのメトリクス、ログ、Trace は Prometheus、Loki、Tempo に入り、Grafana に集約され、Bark 経由で iPhone へ通知されます。 フルスタック監視データフローMetrics · Logs · Traces · Alerts 01 / 信号源02 / 安全な入口03 / 収集・処理04 / 保存05 / 照会・通知 ローカル exportersnode / Windows / LHMリモート fleetotel-collectorjournaldローカルシステム・サービスログ rathole + CaddyTCP トンネル · mTLS 終端 otelcol-homemetrics + tracesotelcol-logsjournald receiver Prometheusメトリクス保存TempoTrace 保存Lokiログ保存 Grafanaメトリクス · ログ · Trace Bark → iPhonewebhook · APNs 15s scrape OTLP + mTLS限定書き込み remote writeサンプリング後 Trace journal entriesOTLP logs PromQLTraceQLLogQL webhook 凡例主要データ安全な入口Prometheus pull

2. 指標収集:2つのモデル

従来の pull (node_exporter)リモート push (metrics-fleet)
適用対象ローカルネットワークノード、ローカルホストパブリックIPの遠隔サーバー
収集エージェントnode_exporter / LHM exporterotel-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 エンドポイントに送信します:

Push モデル:遠隔指標は mTLS + rathole トンネルを経て自宅にプッシュされる リモート otel-collector host_metrics 収集 OTLP+mTLS証明書 パブリックIP入口 otlp.liz6.com:9443 DNS → 中継VPS rathole トンネル TCP トランスパレンシー ・ 暗号文のみ表示 自宅 Caddy mTLS ゲートウェイ 証明書検証 + 失効チェック /api/v1/otlp/* のみ許可 otelcol-home :4318 batch 処理 Prometheus remote write :9090 mTLS は Caddy でのみ終端され、rathole は常に暗号文のみを透過するため、平文トラフィックは見えません。 Prometheus 設定では out_of_order_time_window=30m を設定し、パブリックIP経由での到着順序の揺らぎに対応します。

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 が判定と通知を担当します。アラートチェーン:

アラートチェーン:Grafana ルール評価 → bark-server(ローカル) → APNs → iPhone Grafana ルール評価 30秒ごとに評価 webhook bark-server 127.0.0.1:8085 ローカル直結、トンネル経由ではない APNs iPhone ロック画面プッシュ なぜローカル bark-server を通すのか:アラート通知はトンネル/パブリックIPに依存できない – リンクが切断された際にもアラートを送信できないため、ローカル直結で外部依存をゼロにする。

なぜローカル 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: ["grafana_folder", "alertname"]     # 同種はマージ、異種は個別送信
    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件)

ルールレベル条件沈黙
サイドルーターダウンcriticalup{job="li-home-router"}==03m
ルーター不通criticalping_loss{target="gateway"}>30%2m
外網高パケットロスwarningping_loss{target="223.5.5.5"}>20%3m
TCP再送率高いwarningTcp_RetransSegs / Tcp_OutSegs > 10%5m
DNS 全遅延warning3ドメインの解決がすべて >100ms5m
mihomo TUN 無効criticalnikki ネットワークインターフェース指標消失2m
CN ノード ディスク不足warningルートパーティション残り <5GB5m
給水ポンプ停止criticalfan7 RPM <15005m
VRM 過熱warningtemp5 >90°C2m
GPU ホットスポット過熱warning7900XTX ジョンクション温度 >100°C2m
N100 CPU 過負荷warningCPU >90%5m
ローカル exporter 接続切断criticalup{job="local-info"}==02m
rathole サービス到達不能criticalrathole_service_up < 1 (service タグ別)3m
日次レポートメール配信失敗warningLoki: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 を使用)。

データフロー

ログデータフロー:journald → otelcol-logs → Loki → Grafana カーネル dmesg systemd サービス Docker コンテナ journald 永続化 /var/log/journal SystemMaxUse=8G otelcol-logs journaldreceiver + OTTL resource_detection+batch User=otelcol-logs Loki 90日 Grafana Explore 重要:journaldreceiver はフィールドを body(Map) に配置し、OTTL は body から属性を抽出し、最後に `set(body, body["MESSAGE"])` で復元する。 Docker 側 /etc/docker/daemon.json: log-driver=journald + tag={{.Name}} + live-restore=true Loki: common.instance_addr=127.0.0.1(自己接続を修正)、discover_log_levels=true

コンポーネント

コンポーネント場所備考
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.yamluid loki-local

タグ付け方針

低基数はインデックスタグに、高基数は構造化メタデータ⁠(検索可能だがインデックス化しない)に含める:

インデックスタグ意味
host_nameresource host.name (全体名、metrics と整合)
service_name= host.name、Loki ネイティブのグループ化フィールド
unitsystemd サービス名; Docker コンテナ = コンテナ名 (OTTL によって CONTAINER_NAME から上書き)
containerDocker コンテナ名 (コンテナログのみ)
level以下「レベル判定」参照
transportkernel / stdout / syslog / journal / audit

レベル判定 (混合戦略)

journald の PRIORITY は、ネイティブソース(systemd-journal/syslog/kernel)でのみ真のレベルを表します。コンテナログや生の stdout/stderr の PRIORITY は、単にストリーム(stdout=6/stderr=3)を表すだけで、意味がありません。

  1. ベースライン⁠:コンテナと stdout 以外のソースは PRIORITY に基づいてレベルを割り当てます (0–2 critical / 3 error / 4 warn / 5–6 info / 7 debug);コンテナログのデフォルトは info。
  2. 本文による上書き⁠:OTTL IsMatch でメッセージ本文から [error]/level=info/"level":"warn" および大文字の独立語 WARNING/ERROR を識別します。
  3. 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. ログのトラブルシューティング

  1. Loki 単一バイナリ ring 自己接続失敗⁠:デフォルトではネットワークインターフェースIPをブロードキャストしますが、gRPC は 127.0.0.1 のみをリスンしているため、接続が拒否されます。common.instance_addr: 127.0.0.1 を必須にします。
  2. journaldreceiver はログ全体を body(Map) に配置⁠:フィールドは body["PRIORITY"] であり attributes ではありません。OTTL はまず body から属性を抽出し、最後に set(body, body["MESSAGE"]) を実行します。
  3. service_name のセマンティクス⁠:欲しいのは全体識別子であり、resource レベルです。Loki の discover_service_name は resource 属性のみを読み取るため、OTTL resource コンテキストで service.name = host.name を設定します。
  4. Docker コンテナの unit:journald ドライバ下では _SYSTEMD_UNIT=docker.service は機能しないため、OTTL で CONTAINER_NAME を上書きします。
  5. PRIORITY を信用できない⁠:adguard/HA/owntracks は info+warning をすべて stderr に書き込むため、PRIORITY はすべて 3 になります。これを信用するとすべて error になってしまいます。discover_log_levels を false に設定しないでください – そうしないと detected_level が消え、Grafana のヒストグラムが単一の棒グラフになります。
  6. カラーログ = バイト配列⁠:ANSIを含むMESSAGEはjournaldによってint-sliceとして保存され、OTTLではデコードできません。⁠根本解決 = ソースで色を無効化⁠:コンテナに NO_COLOR=1+PY_COLORS=0 を追加;Rustサービスに Environment=NO_COLOR=1+RUST_LOG_STYLE=never を追加。
  7. node_exporter sgcc 大量出力⁠:sgcc_ts.prom はタイムスタンプ付き(root 権限で所有される孤児ファイル)、textfile collector はサポートしていません。textfile_collector/ から移動すれば根治します。
  8. runlike によるコンテナ再構築でデータ消失⁠:uvx runlike は匿名/命名ボリュームに対してエラーを出力するため、docker inspect .Mounts に従ってボリュームを明示的に固定する必要があります。

7. Trace:Tempo + tail sampling

Trace と指標は同じ OTLP エントリポイント (otelcol-home :4318) を共有し、内部でパイプラインごとに分流します:

Trace チェーン:アプリケーション → otelcol-home → tail sampling → Tempo → Grafana アプリケーション/サービス OTLP trace exporter otelcol-home :4318(指標エントリポイントと同じ) パイプライン: memory_limiter → tail_sampling → batch Tempo :4317 ・ gRPC Grafana Explore tempo-local Trace と指標は同じ OTLP エントリポイント (otelcol-home :4318) を共有し、内部でパイプラインごとに分流し、互いに干渉しません。 tail_sampling は span ツリーを完全な状態に保ち、親 span と子 span のサンプリング不一致による分断を防ぎます。

サンプリング戦略

tail sampling (head sampling ではなく) を使用し、span ツリーが完全に切り捨てられないようにします:

戦略条件サンプリングレート
errors-onlystatus_code == ERROR100%
latency-sampling応答時間 >500ms100%
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: { store: inmemory }
      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 をすでにカバーしています):

  1. リモート otelcol-fleet agent に journald receiver + logs パイプラインを追加 → otlphttp で otlp.liz6.com:9443 へ
  2. 自宅の otelcol-home に logs パイプラインを追加 → otlphttp で Loki /otlp/v1/logs へ
  3. add-server.sh/CA/命名規則をすべて継承し、host_name/service_name は各ノード名とする

関連ドキュメント