Skip to content
Published on

Grafana + Loki + Promtail ログパイプライン構築ガイド

Authors
  • Name
    Twitter
Grafana + Loki + Promtail ログパイプライン

概要

本番環境においてログは障害対応とデバッグの要です。ELK(Elasticsearch + Logstash + Kibana)スタックが長らく標準でしたが、Elasticsearchの高いリソース使用量と複雑な運用負担が課題でした。Grafana Lokiはこれらの問題を解決するために登場した軽量ログ収集システムで、ログ本文をインデックスせずラベルベースのインデックスのみを行うことで、ストレージコストと運用の複雑さを大幅に削減します。

この記事では、Promtail → Loki → Grafana パイプラインをDocker Composeで構築し、LogQLクエリとアラートルールまで設定する全プロセスを解説します。

アーキテクチャ概要

ログパイプライン全体のフローは以下の通りです:

graph LR
    A[Application Logs] -->|tail| B[Promtail]
    B -->|HTTP Push| C[Loki]
    C -->|Store| D[Object Storage / Filesystem]
    C -->|Query| E[Grafana]
    E -->|Alert| F[Slack / Email]

各コンポーネントの役割:

コンポーネント役割
PromtailログファイルをtailしてLokiに送信するエージェント
Lokiログストレージ。ラベルインデックス+チャンク保存
Grafanaログの可視化、ダッシュボード、アラート設定

環境準備

プロジェクトディレクトリ構成

mkdir -p loki-stack/{config,data}
cd loki-stack

# ディレクトリ構成
# loki-stack/
# ├── docker-compose.yml
# ├── config/
# │   ├── loki-config.yml
# │   └── promtail-config.yml
# └── data/

Docker Compose設定

docker-compose.yml

version: '3.8'

services:
  loki:
    image: grafana/loki:3.3.2
    container_name: loki
    ports:
      - '3100:3100'
    volumes:
      - ./config/loki-config.yml:/etc/loki/local-config.yaml
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped
    networks:
      - loki-net

  promtail:
    image: grafana/promtail:3.3.2
    container_name: promtail
    volumes:
      - ./config/promtail-config.yml:/etc/promtail/config.yml
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
    command: -config.file=/etc/promtail/config.yml
    depends_on:
      - loki
    restart: unless-stopped
    networks:
      - loki-net

  grafana:
    image: grafana/grafana:11.4.0
    container_name: grafana
    ports:
      - '3000:3000'
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
      - GF_AUTH_ANONYMOUS_ENABLED=true
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      - loki
    restart: unless-stopped
    networks:
      - loki-net

volumes:
  loki-data:
  grafana-data:

networks:
  loki-net:
    driver: bridge

Loki設定

config/loki-config.yml

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: info

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 720h # 30日間保持
  max_query_length: 721h
  max_query_parallelism: 4
  ingestion_rate_mb: 10
  ingestion_burst_size_mb: 20

compactor:
  working_directory: /loki/compactor
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h
  delete_request_store: filesystem

主要な設定ポイント:

  • schema: v13 — 最新のTSDBスキーマでクエリ性能を向上
  • retention_period: 720h — 30日後に自動削除
  • auth_enabled: false — シングルテナントモード(開発・小規模運用向け)

Promtail設定

config/promtail-config.yml

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push
    batchwait: 1s
    batchsize: 1048576 # 1MB

scrape_configs:
  # システムログ収集
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: syslog
          host: myserver
          __path__: /var/log/syslog

  # Dockerコンテナログ収集
  - job_name: docker
    static_configs:
      - targets:
          - localhost
        labels:
          job: docker
          __path__: /var/lib/docker/containers/**/*.log
    pipeline_stages:
      - docker: {}
      - json:
          expressions:
            stream: stream
            time: time
            log: log
      - labels:
          stream:
      - output:
          source: log

  # Nginxアクセスログ
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          type: access
          __path__: /var/log/nginx/access.log
    pipeline_stages:
      - regex:
          expression: '^(?P<remote_addr>[\w.]+) - (?P<remote_user>\S+) \[(?P<time_local>[^\]]+)\] "(?P<method>\w+) (?P<request_uri>\S+) \S+" (?P<status>\d+) (?P<body_bytes_sent>\d+)'
      - labels:
          method:
          status:
      - metrics:
          http_requests_total:
            type: Counter
            description: 'Total HTTP requests'
            match_all: true
            action: inc

Pipeline Stagesの詳細フロー

Promtailのパイプライン処理フローを可視化すると:

graph TD
    A[Raw Log Line] --> B[docker stage]
    B --> C[json stage - フィールド抽出]
    C --> D[labels stage - ラベル付与]
    D --> E[output stage - 最終ログ]
    E --> F[Loki Push]

    G[Nginx Log Line] --> H[regex stage - パターンマッチング]
    H --> I[labels stage - method, status]
    I --> J[metrics stage - カウンター増加]
    J --> F

実行と確認

# スタック起動
docker compose up -d

# ステータス確認
docker compose ps

# Lokiステータス確認
curl -s http://localhost:3100/ready
# ready

# Promtailターゲット確認
curl -s http://localhost:9080/targets | jq '.[] | .labels'

# Lokiに保存されたラベル確認
curl -s http://localhost:3100/loki/api/v1/labels | jq

GrafanaでLokiを接続

1. データソース追加

Grafana(http://localhost:3000)にアクセス後:

  1. Connections → Data Sources → Add data source
  2. Lokiを選択
  3. URL: http://loki:3100
  4. Save & Testをクリック

2. LogQL基本クエリ

Exploreメニューで様々なLogQLクエリを実行してみましょう:

# 全syslog参照
{job="syslog"}

# ERRORキーワードフィルタリング
{job="syslog"} |= "error"

# Nginx 5xxエラーのみ参照
{job="nginx", type="access"} | json | status >= 500

# 正規表現フィルター
{job="docker"} |~ "(?i)exception|panic|fatal"

# 直近1時間のエラーカウント(1分間隔)
count_over_time({job="syslog"} |= "error" [1m])

# 上位10件のエラーパターン
{job="syslog"} |= "error"
  | pattern `<_> error: <message>`
  | topk(10, count_over_time({job="syslog"} |= "error" [1h]))

LogQL演算子まとめ

演算子説明
|=文字列を含む{job="app"} |= "error"
!=文字列を含まない{job="app"} != "debug"
|~正規表現マッチ{job="app"} |~ "err|warn"
!~正規表現非マッチ{job="app"} !~ "health"
| jsonJSONパース{job="app"} | json
| logfmtlogfmtパース{job="app"} | logfmt

ダッシュボード構成

JSONモデルでダッシュボードプロビジョニング

config/dashboards/logs-overview.jsonファイルを作成して自動プロビジョニングできます:

{
  "dashboard": {
    "title": "Logs Overview",
    "panels": [
      {
        "title": "Error Rate (1m)",
        "type": "timeseries",
        "targets": [
          {
            "expr": "sum(count_over_time({job=~\".+\"} |= \"error\" [1m]))",
            "legendFormat": "errors/min"
          }
        ],
        "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 }
      },
      {
        "title": "Log Volume by Job",
        "type": "barchart",
        "targets": [
          {
            "expr": "sum by (job) (count_over_time({job=~\".+\"} [5m]))",
            "legendFormat": "{{job}}"
          }
        ],
        "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 }
      },
      {
        "title": "Recent Errors",
        "type": "logs",
        "targets": [
          {
            "expr": "{job=~\".+\"} |= \"error\""
          }
        ],
        "gridPos": { "x": 0, "y": 8, "w": 24, "h": 10 }
      }
    ]
  }
}

アラート設定

GrafanaのUnified Alertingを活用してエラー急増時に通知を送りましょう:

Contact Point設定(Slack)

# Grafana provisioning: config/alerting/contact-points.yml
apiVersion: 1
contactPoints:
  - orgId: 1
    name: slack-alerts
    receivers:
      - uid: slack-1
        type: slack
        settings:
          url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
          title: '🚨 {{ .CommonLabels.alertname }}'
          text: |
            **Status:** {{ .Status }}
            **Summary:** {{ .CommonAnnotations.summary }}

アラートルール作成

Grafana UIで:

  1. Alerting → Alert Rules → New Alert Rule
  2. クエリ: count_over_time({job="syslog"} |= "error" [5m]) > 50
  3. 評価間隔: 1分
  4. 待機時間: 5分(一時的なスパイクを無視)
  5. Contact Point: slack-alerts

プロダクション運用Tips

1. マルチテナント設定

# loki-config.yml
auth_enabled: true

# PromtailからテナントIDを送信
clients:
  - url: http://loki:3100/loki/api/v1/push
    tenant_id: team-backend

2. S3互換オブジェクトストレージの使用

# loki-config.yml(プロダクション)
common:
  storage:
    s3:
      endpoint: minio:9000
      bucketnames: loki-chunks
      access_key_id: ${MINIO_ACCESS_KEY}
      secret_access_key: ${MINIO_SECRET_KEY}
      insecure: true
      s3forcepathstyle: true

3. Kubernetes環境でのHelm デプロイ

# Loki Stack Helmチャート
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

helm install loki grafana/loki-stack \
  --namespace observability \
  --create-namespace \
  --set grafana.enabled=true \
  --set promtail.enabled=true \
  --set loki.persistence.enabled=true \
  --set loki.persistence.size=50Gi

4. ログボリューム制御

# promtail-config.yml - 不要なログをドロップ
pipeline_stages:
  - match:
      selector: '{job="nginx"}'
      stages:
        - regex:
            expression: '"(?P<method>\w+) (?P<uri>\S+)'
        - drop:
            expression: '^/health$'
            source: uri
        - drop:
            expression: '^/metrics$'
            source: uri

全体データフローまとめ

sequenceDiagram
    participant App as Application
    participant PT as Promtail
    participant LK as Loki
    participant S3 as Storage
    participant GF as Grafana
    participant User as Operator

    App->>App: Write logs to /var/log/app.log
    PT->>App: Tail log file
    PT->>PT: Pipeline processing (parse, label, filter)
    PT->>LK: HTTP POST /loki/api/v1/push
    LK->>LK: Index labels + compress chunks
    LK->>S3: Store chunks & index
    User->>GF: Open Dashboard
    GF->>LK: LogQL query
    LK->>S3: Read chunks
    LK->>GF: Return results
    GF->>User: Render logs & charts
    GF->>GF: Evaluate alert rules
    GF-->>User: 🚨 Slack notification (if threshold exceeded)

まとめ

Grafana + Loki + PromtailスタックはELKと比べてはるかに少ないリソースで効果的なログパイプラインを構築できます。主な利点をまとめると:

  • 低リソース使用量: ログ本文をインデックスしないためストレージとメモリを節約
  • Grafanaネイティブ統合: メトリクス(Prometheus)とログ(Loki)を1つのダッシュボードで参照
  • LogQL: PromQLに似た構文で学習コストが緩やか
  • 水平スケーリング: マイクロサービスアーキテクチャで読み取り/書き込みパスを独立してスケーリング

本番環境ではS3互換ストレージ、マルチテナント、適切なretentionポリシーを必ず検討しましょう。

クイズ

Q1: LokiがELKと比べてストレージコストが低い核心的な理由は? Lokiはログ本文をインデックスせず、ラベル(label)のみをインデックスするためです。Elasticsearchは全文(full-text)インデックスを行い、はるかに多くのストレージとメモリを消費します。

Q2: Promtailのpositionsファイルの役割は? 各ログファイルで最後に読み取った位置(オフセット)を記録します。Promtail再起動時に重複送信や欠落なく、前回の位置から読み取りを再開できます。

Q3: LogQLの|=|~の違いは? |=は正確な文字列の包含を検査し、|~は正規表現(regex)パターンマッチングを実行します。例:|= "error"は"error"文字列の包含、|~ "err|warn"は"err"または"warn"にマッチします。

Q4: Lokiのschema v13で使用されるインデックスストアは? TSDB(Time Series Database)ストアを使用します。以前のバージョンのBoltDBよりクエリ性能と圧縮効率が大幅に向上しています。

Q5: Pipeline stagesのdropステージの用途は? 特定の条件にマッチするログ行をLokiに送信せず破棄する役割です。ヘルスチェックやメトリクスエンドポイントなど不要なログをフィルタリングしてストレージコストを削減します。

Q6: マルチテナントモードでPromtailがテナントを区別する方法は? Promtailのclients設定でtenant_idを指定すると、LokiにHTTPヘッダーX-Scope-OrgIDとして送信され、テナントごとにログが分離されます。

Q7: count_over_time関数の役割は? 指定された時間範囲内のログ行の数をカウントするLogQLメトリクスクエリです。例:count_over_time( {(job = 'app')} |= "error" [5m])は直近5分間のエラーログ数を返します。

Q8: Lokiのretention_periodとcompactorの関係は? retention_periodはログの保持期間を定義し、compactorが実際に期限切れのチャンクを見つけて削除する役割を担います。compactorのretention_enabled: trueが必ず設定されている必要があります。