このページの目次
フルスタック監視:Prometheus + Grafana + Loki + Tempo + Bark
指標(Prometheus)+ ログ(Loki)+ Trace(Tempo)の3本柱がGrafanaに集約され、アラートはBark経由でiPhoneにプッシュされます。指標はpull(ローカルネットワーク上のnode_exporter)とpush(リモートotel-collector fleet agent、ratholeトンネル+mTLS経由)の2つのモードで収集されます。ログはjournald → otelcol-logs → Lokiへ、Traceはotelcol-homeのtail sampling → Tempoへ流れます。本稿ではローカルホスト li-home-0 を实例として、フルスタックアーキテクチャ、アラートルール、収集手法の比較、タグ付け方針、レベル判定、およびトラブルシューティングの記録を解説します。
一、概要
二、指標収集: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は、パブリックネットワーク経由での到着順序の揺らぎに対応します。
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"}))
三、セキュリティ: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節参照)。
四、アラート:Grafana + Bark による iPhone へのプッシュ
Prometheus は収集を担当し、Grafana は判定と通知を担当します。アラートチェーン:
なぜローカル bark-server を使い、パブリック 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を返します。
アラートルール (計13件)
| ルール | レベル | 条件 | 沈黙 |
|---|---|---|---|
| サドルーターダウン | 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 |
ルールは Grafana provisioning で管理(
/etc/grafana/provisioning/alerting/home-network.yaml)、git でバージョン管理され、Web UI を介さずにルールを変更できます。各アラートはannotations.summaryで現象を記述し、annotations.runbookでトラブルシューティング手順を提供します——アラート通知では summary のみをプッシュし、runbook は Grafana 内で展開して参照します。
メトリクスのトラブルシューティング
- N100 サドルーターの接続: Prometheus が N100 を scrape するにはローカルプロキシ経由が必要ですが、DIRECT ルールを追加して迂回させます。
五、ログ監視 (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、bind
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"
六、ログのトラブルシューティング
- 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に基づいてボリュームを明示的に固定する必要があります。
七、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、bind 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 証明書を再利用します——指標と trace は同じパイプライン、同じセキュリティ体系を共有します。
八、将来の拡張: リモート 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)