---
title: フルスタック監視:Prometheus + Grafana + Loki + Tempo + Bark
url: https://doc.liz6.com/ja/homelab/monitoring
locale: ja
area: homelab
tags:
- homelab
date: 2026-07-01
modified: 2026-07-16
description: 指標(Prometheus)+ ログ(Loki)+ Trace(Tempo)の3本柱がGrafanaに集約され、アラートはBark経由でiPhoneにプッシュされます。指標はpull(ローカルネットワーク上のnode_exporter)とpush(リモートotel-collector fleet agent、ratholeトンネル+mTLS経由)の2つのモードで収集されます。…
---

# フルスタック監視: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 を实例として、フルスタックアーキテクチャ、アラートルール、収集手法の比較、タグ付け方針、レベル判定、およびトラブルシューティングの記録を解説します。

## 一、概要

<svg viewBox="0 0 780 360" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="監視アーキテクチャの概要">
  <defs><marker id="mah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#64748b"/></marker></defs>
  <rect width="780" height="360" fill="#ffffff"/>
  <text x="390" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">監視アーキテクチャ:指標+ログ+Trace → Grafana、アラート → Bark → iPhone</text>
  <!-- Remote fleet -->
  <rect x="20" y="50" width="130" height="90" rx="8" fill="#fef3c7" stroke="#f59e0b"/>
  <text x="85" y="72" text-anchor="middle" font-size="10" font-weight="700" fill="#b45309">リモート fleet</text>
  <text x="85" y="88" text-anchor="middle" font-size="9" fill="#d97706">otel-collector</text>
  <text x="85" y="102" text-anchor="middle" font-size="9" fill="#d97706">host_metrics 30s</text>
  <text x="85" y="116" text-anchor="middle" font-size="9" fill="#d97706">file_storage キュー</text>
  <text x="85" y="130" text-anchor="middle" font-size="8" fill="#b45309">mTLS クライアント証明書</text>
  <line x1="150" y1="98" x2="200" y2="72" stroke="#f59e0b" stroke-width="1.3" marker-end="url(#mah)"/>
  <!-- rathole + Caddy -->
  <rect x="204" y="44" width="110" height="102" rx="6" fill="#e2e8f0" stroke="#94a3b8"/>
  <text x="259" y="68" text-anchor="middle" font-size="10" font-weight="700" fill="#475569">rathole</text>
  <text x="259" y="84" text-anchor="middle" font-size="9" fill="#64748b">TCP トンネル</text>
  <text x="259" y="100" text-anchor="middle" font-size="9" fill="#64748b">↓ Caddy</text>
  <text x="259" y="116" text-anchor="middle" font-size="9" fill="#64748b">mTLS 終端</text>
  <text x="259" y="132" text-anchor="middle" font-size="9" fill="#64748b">パス制限 + 失効</text>
  <line x1="314" y1="95" x2="356" y2="95" stroke="#475569" stroke-width="1.3" marker-end="url(#mah)"/>
  <!-- otelcol-home -->
  <rect x="360" y="50" width="110" height="90" rx="8" fill="#e0e7ff" stroke="#818cf8"/>
  <text x="415" y="72" text-anchor="middle" font-size="10" font-weight="700" fill="#3730a3">otelcol-home</text>
  <text x="415" y="88" text-anchor="middle" font-size="9" fill="#4f46e5">OTLP 受信</text>
  <text x="415" y="102" text-anchor="middle" font-size="9" fill="#4f46e5">metrics パイプライン</text>
  <text x="415" y="116" text-anchor="middle" font-size="9" fill="#4f46e5">traces パイプライン</text>
  <text x="415" y="130" text-anchor="middle" font-size="9" fill="#4f46e5">tail sampling</text>
  <!-- metrics→Prom -->
  <line x1="470" y1="80" x2="532" y2="80" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#mah)"/>
  <!-- traces→Tempo -->
  <line x1="470" y1="118" x2="532" y2="118" stroke="#8b5cf6" stroke-width="1.5" marker-end="url(#mah)"/>
  <rect x="536" y="56" width="90" height="24" rx="6" fill="#dbeafe" stroke="#3b82f6"/>
  <text x="581" y="72" text-anchor="middle" font-size="10" font-weight="700" fill="#1e40af">Prometheus</text>
  <rect x="536" y="106" width="90" height="24" rx="6" fill="#ede9fe" stroke="#a78bfa"/>
  <text x="581" y="122" text-anchor="middle" font-size="10" font-weight="700" fill="#6d28d9">Tempo :4317</text>
  <!-- Local pull -->
  <rect x="646" y="56" width="116" height="22" rx="4" fill="#f1f5f9"/>
  <text x="704" y="71" text-anchor="middle" font-size="9" fill="#64748b">node_exporter (pull)</text>
  <line x1="646" y1="67" x2="630" y2="67" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2 4"/>
  <!-- Logs row -->
  <rect x="20" y="176" width="82" height="24" rx="5" fill="#fce7f3"/><text x="61" y="192" text-anchor="middle" font-size="9" font-weight="700" fill="#9d174d">journald</text>
  <line x1="102" y1="188" x2="144" y2="188" stroke="#ec4899" stroke-width="1.3" marker-end="url(#mah)"/>
  <rect x="148" y="176" width="82" height="24" rx="5" fill="#fce7f3"/><text x="189" y="192" text-anchor="middle" font-size="9" font-weight="700" fill="#9d174d">otelcol-logs</text>
  <line x1="230" y1="188" x2="278" y2="188" stroke="#ec4899" stroke-width="1.3" marker-end="url(#mah)"/>
  <rect x="282" y="176" width="70" height="24" rx="5" fill="#fce7f3"/><text x="317" y="192" text-anchor="middle" font-size="9" font-weight="700" fill="#9d174d">Loki :3100</text>
  <!-- Grafana -->
  <rect x="440" y="166" width="110" height="44" rx="8" fill="#dcfce7" stroke="#22c55e"/>
  <text x="495" y="186" text-anchor="middle" font-size="11" font-weight="700" fill="#166534">Grafana</text>
  <text x="495" y="201" text-anchor="middle" font-size="9" fill="#15803d">指標 · ログ · Trace</text>
  <!-- Bark -->
  <rect x="580" y="166" width="80" height="44" rx="8" fill="#fff7ed" stroke="#f97316"/>
  <text x="620" y="186" text-anchor="middle" font-size="11" font-weight="700" fill="#c2410c">Bark</text>
  <text x="620" y="201" text-anchor="middle" font-size="9" fill="#ea580c">APNs → iPhone</text>
  <!-- arrows to Grafana -->
  <line x1="626" y1="69" x2="545" y2="180" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2 4"/>
  <line x1="626" y1="118" x2="545" y2="183" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2 4"/>
  <line x1="352" y1="188" x2="436" y2="188" stroke="#94a3b8" stroke-width="1" stroke-dasharray="2 4"/>
  <!-- bottom notes -->
  <rect x="20" y="228" width="740" height="70" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
  <text x="40" y="250" font-size="10" fill="#475569">metrics-fleet: リモート otel-collector(fleet agent) → OTLP+mTLS → ratholeトンネル → Caddyゲートウェイ → otelcol-home(metrics+traceパイプライン)</text>
  <text x="40" y="268" font-size="10" fill="#475569">ローカル: node_exporter → Prometheus(pull) · journald → otelcol-logs → Loki · otelcol-home traces → Tempo(gRPC, 720h保持)</text>
  <text x="40" y="286" font-size="10" fill="#475569">Grafanaで指標(Prometheus)+ログ(Loki)+Trace(Tempo)を統一クエリ · アラートはBark webhook → APNs → iPhone(ローカル bark-server、トンネル/パブリックIPに依存しない)</text>
  <rect x="20" y="310" width="740" height="22" rx="5" fill="#f1f5f9"/>
  <text x="390" y="326" text-anchor="middle" font-size="9" fill="#94a3b8">全コンポーネントは native systemd · bind 127.0.0.1 · Dockerはアプリケーションのみを実行</text>
</svg>

## 二、指標収集: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 エンドポイントに送信します:

<svg viewBox="0 0 720 330" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Push モデル: リモート指標は mTLS と rathole トンネルを経て自宅にプッシュされる">
  <defs><marker id="oah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="330" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Push モデル: リモート指標は mTLS + rathole トンネルを経て自宅にプッシュされる</text>
  <!-- Row 1 -->
  <rect x="20" y="60" width="200" height="70" rx="8" fill="#fef3c7" stroke="#f59e0b"/>
  <text x="120" y="88" text-anchor="middle" font-size="11" font-weight="700" fill="#b45309">リモート otel-collector</text>
  <text x="120" y="106" text-anchor="middle" font-size="9" fill="#d97706">host_metrics 収集</text>
  <line x1="220" y1="95" x2="260" y2="95" stroke="#475569" stroke-width="1.6" marker-end="url(#oah)"/>
  <text x="240" y="85" text-anchor="middle" font-size="8" fill="#475569">OTLP+mTLS 証明書</text>
  <rect x="260" y="60" width="200" height="70" rx="8" fill="#e2e8f0" stroke="#94a3b8"/>
  <text x="360" y="86" text-anchor="middle" font-size="11" font-weight="700" fill="#475569">パブリック入口</text>
  <text x="360" y="103" text-anchor="middle" font-size="9" fill="#64748b">otlp.liz6.com:9443</text>
  <text x="360" y="117" text-anchor="middle" font-size="9" fill="#64748b">DNS → 中継 VPS</text>
  <line x1="460" y1="95" x2="500" y2="95" stroke="#475569" stroke-width="1.6" marker-end="url(#oah)"/>
  <rect x="500" y="60" width="200" height="70" rx="8" fill="#e2e8f0" stroke="#94a3b8"/>
  <text x="600" y="88" text-anchor="middle" font-size="11" font-weight="700" fill="#475569">rathole トンネル</text>
  <text x="600" y="106" text-anchor="middle" font-size="9" fill="#64748b">TCP トランスパレンシー · 暗号文のみ表示</text>
  <!-- connector row1 -> row2 -->
  <path d="M600,130 L600,152 L130,152 L130,170" fill="none" stroke="#475569" stroke-width="1.6" marker-end="url(#oah)"/>
  <!-- Row 2 -->
  <rect x="20" y="170" width="220" height="70" rx="8" fill="#e2e8f0" stroke="#94a3b8"/>
  <text x="130" y="196" text-anchor="middle" font-size="11" font-weight="700" fill="#475569">自宅 Caddy mTLS ゲートウェイ</text>
  <text x="130" y="213" text-anchor="middle" font-size="9" fill="#64748b">証明書検証 + 失効チェック</text>
  <text x="130" y="227" text-anchor="middle" font-size="9" fill="#64748b">/api/v1/otlp/* のみ許可</text>
  <line x1="240" y1="205" x2="280" y2="205" stroke="#475569" stroke-width="1.6" marker-end="url(#oah)"/>
  <rect x="280" y="170" width="210" height="70" rx="8" fill="#e0e7ff" stroke="#818cf8"/>
  <text x="385" y="196" text-anchor="middle" font-size="11" font-weight="700" fill="#3730a3">otelcol-home</text>
  <text x="385" y="213" text-anchor="middle" font-size="9" fill="#4f46e5">:4318</text>
  <text x="385" y="227" text-anchor="middle" font-size="9" fill="#4f46e5">batch 処理</text>
  <line x1="490" y1="205" x2="530" y2="205" stroke="#475569" stroke-width="1.6" marker-end="url(#oah)"/>
  <rect x="530" y="170" width="170" height="70" rx="8" fill="#dbeafe" stroke="#3b82f6"/>
  <text x="615" y="196" text-anchor="middle" font-size="11" font-weight="700" fill="#1e40af">Prometheus</text>
  <text x="615" y="213" text-anchor="middle" font-size="9" fill="#1e40af">remote write</text>
  <text x="615" y="227" text-anchor="middle" font-size="9" fill="#1e40af">:9090</text>
  <!-- bottom note -->
  <rect x="20" y="258" width="680" height="50" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
  <text x="40" y="278" font-size="10" fill="#475569">mTLS は Caddy でのみ終端され、rathole は終始暗号文のみをトランスパシーするため、平文は見えません。</text>
  <text x="40" y="294" font-size="10" fill="#475569">Prometheus 設定 `out_of_order_time_window=30m` は、パブリックネットワーク経由での到着順序の揺らぎに対応します。</text>
</svg>

> Prometheus 設定 `out_of_order_time_window: 30m` は、パブリックネットワーク経由での到着順序の揺らぎに対応します。

### Grafana ダッシュボード

Provisioning ディレクトリ: 7つのホストパネル(CPU / GPU / メモリ / ディスク / 冷却、ノードごとに JSON 分割) + 1つのサーバー状態テンプレート(`$host` ドロップダウンで OTLP 送信元の host_name を自動検出) + ネットワーク監視パネル。

パネル式例:
```promql
# 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](rathole-tunnel.md) 4.3節参照)。

## 四、アラート:Grafana + Bark による iPhone へのプッシュ

Prometheus は収集を担当し、Grafana は判定と通知を担当します。アラートチェーン:

<svg viewBox="0 0 720 200" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="アラートチェーン: Grafana ルール評価はローカル bark-server を経由して iPhone にプッシュされる">
  <defs><marker id="bah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="200" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">アラートチェーン: Grafana ルール評価 → bark-server(ローカル) → APNs → iPhone</text>
  <rect x="20" y="60" width="190" height="70" rx="8" fill="#dcfce7" stroke="#22c55e"/>
  <text x="115" y="92" text-anchor="middle" font-size="12" font-weight="700" fill="#166534">Grafana ルール評価</text>
  <text x="115" y="112" text-anchor="middle" font-size="10" fill="#15803d">30秒ごとに評価</text>
  <line x1="210" y1="95" x2="250" y2="95" stroke="#475569" stroke-width="1.6" marker-end="url(#bah)"/>
  <text x="230" y="85" text-anchor="middle" font-size="9" fill="#475569">webhook</text>
  <rect x="250" y="60" width="230" height="70" rx="8" fill="#fff7ed" stroke="#f97316"/>
  <text x="365" y="88" text-anchor="middle" font-size="12" font-weight="700" fill="#c2410c">bark-server</text>
  <text x="365" y="104" text-anchor="middle" font-size="10" fill="#ea580c">127.0.0.1:8085</text>
  <text x="365" y="119" text-anchor="middle" font-size="9" fill="#ea580c">ローカル直結、トンネルを経ない</text>
  <line x1="480" y1="95" x2="520" y2="95" stroke="#475569" stroke-width="1.6" marker-end="url(#bah)"/>
  <text x="500" y="85" text-anchor="middle" font-size="9" fill="#475569">APNs</text>
  <rect x="520" y="60" width="180" height="70" rx="8" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="610" y="92" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">iPhone</text>
  <text x="610" y="112" text-anchor="middle" font-size="10" fill="#0f766e">ロック画面プッシュ</text>
  <rect x="20" y="150" width="680" height="40" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="174" text-anchor="middle" font-size="12" fill="#115e59">なぜローカル bark-server を使うのか: アラート通知はトンネル/パブリックIPに依存してはならない——リンクがダウンした際にアラートも送信できなくなるため、ローカル直結は外部依存がゼロである。</text>
</svg>

**なぜローカル bark-server を使い、パブリック webhook を使わないのか**: アラート通知が rathole トンネルやパブリックIP経由だと、トンネルがダウンした際にアラートも送信できなくなります。ローカルの bark-server は iPhone と直結しており、外部依存がゼロで、インフラストラクチャに依存しません。

### 連絡先と通知ポリシー

```yaml
# /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を返します。

### アラートルール (計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 を使用)。

### データフロー

<svg viewBox="0 0 760 240" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="ログデータフロー">
  <defs><marker id="lah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#64748b"/></marker></defs>
  <rect width="760" height="240" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">ログデータフロー: journald → otelcol-logs → Loki → Grafana</text>
  <!-- Sources -->
  <rect x="20" y="54" width="88" height="28" rx="5" fill="#e2e8f0"/><text x="64" y="73" text-anchor="middle" font-size="11" fill="#334155">カーネル dmesg</text>
  <rect x="20" y="86" width="88" height="28" rx="5" fill="#e2e8f0"/><text x="64" y="105" text-anchor="middle" font-size="11" fill="#334155">systemd サービス</text>
  <rect x="20" y="118" width="88" height="28" rx="5" fill="#e2e8f0"/><text x="64" y="137" text-anchor="middle" font-size="11" fill="#334155">Docker コンテナ</text>
  <!-- Merge lines -->
  <line x1="108" y1="68" x2="152" y2="60" stroke="#94a3b8" stroke-width="1"/>
  <line x1="108" y1="100" x2="148" y2="92" stroke="#94a3b8" stroke-width="1"/>
  <line x1="108" y1="132" x2="152" y2="124" stroke="#94a3b8" stroke-width="1"/>
  <!-- journald -->
  <rect x="156" y="58" width="140" height="76" rx="8" fill="#e0e7ff" stroke="#818cf8"/>
  <text x="226" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">journald</text>
  <text x="226" y="97" text-anchor="middle" font-size="9" fill="#4f46e5">永続化 /var/log/journal</text>
  <text x="226" y="113" text-anchor="middle" font-size="9" fill="#4f46e5">SystemMaxUse=8G</text>
  <line x1="296" y1="96" x2="352" y2="96" stroke="#475569" stroke-width="1.8" marker-end="url(#lah)"/>
  <!-- otelcol-logs -->
  <rect x="356" y="58" width="156" height="76" rx="8" fill="#fef3c7" stroke="#f59e0b"/>
  <text x="434" y="78" text-anchor="middle" font-size="11" font-weight="700" fill="#b45309">otelcol-logs</text>
  <text x="434" y="95" text-anchor="middle" font-size="9" fill="#d97706">journaldreceiver + OTTL</text>
  <text x="434" y="109" text-anchor="middle" font-size="9" fill="#d97706">resource_detection+batch</text>
  <text x="434" y="123" text-anchor="middle" font-size="9" fill="#d97706">User=otelcol-logs</text>
  <line x1="512" y1="96" x2="570" y2="96" stroke="#475569" stroke-width="1.8" marker-end="url(#lah)"/>
  <!-- Loki -->
  <rect x="574" y="58" width="52" height="76" rx="6" fill="#fce7f3" stroke="#ec4899"/>
  <text x="600" y="86" text-anchor="middle" font-size="11" font-weight="700" fill="#be185d">Loki</text>
  <text x="600" y="103" text-anchor="middle" font-size="9" fill="#9d174d">90日</text>
  <line x1="626" y1="96" x2="678" y2="96" stroke="#475569" stroke-width="1.8" marker-end="url(#lah)"/>
  <!-- Grafana -->
  <rect x="682" y="58" width="52" height="76" rx="6" fill="#dcfce7" stroke="#22c55e"/>
  <text x="708" y="86" text-anchor="middle" font-size="11" font-weight="700" fill="#166534">Grafana</text>
  <text x="708" y="103" text-anchor="middle" font-size="9" fill="#15803d">Explore</text>
  <!-- bottom notes -->
  <rect x="60" y="148" width="640" height="60" rx="8" fill="#f8fafc" stroke="#e2e8f0"/>
  <text x="100" y="170" font-size="10" fill="#475569">重要: journaldreceiver はフィールドを body(Map) に配置し、OTTL は body から属性を抽出し、最後に `set(body, body["MESSAGE"])` で復元する。</text>
  <text x="100" y="186" font-size="10" fill="#475569">Docker 側 /etc/docker/daemon.json: log-driver=journald + tag={{.Name}} + live-restore=true</text>
  <text x="100" y="202" font-size="10" fill="#475569">Loki 側: common.instance_addr=127.0.0.1 (否则 ring 广播 IP 自连失败),discover_log_levels=true</text>
</svg>

### コンポーネント

| コンポーネント | 場所 | 備考 |
|------|------|------|
| 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)を表すだけで、意味を持ちません。

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)

```logql
{unit="sshd.service"}
{level="error"}
{container="adguardhome"}
{transport="kernel"}
{service_name="li-home-0"} |= "timeout"
```

## 六、ログのトラブルシューティング

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` に基づいてボリュームを明示的に固定する必要があります。

## 七、Trace: Tempo + tail sampling

Trace と指標は同じ OTLP 入口(otelcol-home :4318)を共有し、内部でパイプラインによって分流されます:

<svg viewBox="0 0 720 220" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Trace チェーン: アプリは otelcol-home の tail sampling を経由して Tempo に流入し、Grafana に到達する">
  <defs><marker id="tah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="220" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Trace チェーン: アプリ → otelcol-home → tail sampling → Tempo → Grafana</text>
  <rect x="20" y="60" width="130" height="90" rx="8" fill="#e2e8f0" stroke="#94a3b8"/>
  <text x="85" y="100" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">アプリ/サービス</text>
  <text x="85" y="118" text-anchor="middle" font-size="9" fill="#475569">OTLP trace</text>
  <text x="85" y="131" text-anchor="middle" font-size="9" fill="#475569">exporter</text>
  <line x1="150" y1="105" x2="185" y2="105" stroke="#475569" stroke-width="1.6" marker-end="url(#tah)"/>
  <rect x="185" y="60" width="230" height="90" rx="8" fill="#e0e7ff" stroke="#818cf8"/>
  <text x="300" y="82" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">otelcol-home</text>
  <text x="300" y="98" text-anchor="middle" font-size="9" fill="#4f46e5">:4318(指標入口と同じ)</text>
  <text x="300" y="112" text-anchor="middle" font-size="9" fill="#4f46e5">パイプライン: memory_limiter</text>
  <text x="300" y="126" text-anchor="middle" font-size="9" fill="#4f46e5">→ tail_sampling → batch</text>
  <line x1="415" y1="105" x2="450" y2="105" stroke="#475569" stroke-width="1.6" marker-end="url(#tah)"/>
  <rect x="450" y="60" width="120" height="90" rx="8" fill="#ede9fe" stroke="#a78bfa"/>
  <text x="510" y="100" text-anchor="middle" font-size="12" font-weight="700" fill="#6d28d9">Tempo</text>
  <text x="510" y="118" text-anchor="middle" font-size="9" fill="#7c3aed">:4317 · gRPC</text>
  <line x1="570" y1="105" x2="605" y2="105" stroke="#475569" stroke-width="1.6" marker-end="url(#tah)"/>
  <rect x="605" y="60" width="95" height="90" rx="8" fill="#dcfce7" stroke="#22c55e"/>
  <text x="652" y="95" text-anchor="middle" font-size="11" font-weight="700" fill="#166534">Grafana</text>
  <text x="652" y="111" text-anchor="middle" font-size="9" fill="#15803d">Explore</text>
  <text x="652" y="125" text-anchor="middle" font-size="8" fill="#15803d">tempo-local</text>
  <rect x="20" y="160" width="680" height="45" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="180" text-anchor="middle" font-size="12" fill="#3730a3">Trace と指標は同じ OTLP 入口(otelcol-home :4318)を共有し、内部でパイプラインによって分流され、互いに干渉しません。</text>
  <text x="360" y="196" text-anchor="middle" font-size="12" fill="#3730a3">tail_sampling は span ツリーを完全な状態に保ち、親 span と子 span のサンプリング不一致による断線を防ぎます。</text>
</svg>

### サンプリングポリシー

**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 バックエンド

```yaml
# /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、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` をすでにカバーしています):
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` が各ノード名となります。

## 関連ドキュメント

- [network-architecture.md](network-architecture.md) — ネットワークトポロジーとデーモン
- [rathole-tunnel.md](rathole-tunnel.md) — トンネルアーキテクチャ(mihomo を迂回する接続)
- [server-lifecycle.md](server-lifecycle.md) — リモートサーバー接続スクリプト(add-server)
