T-CREATOR

Ruby の本番運用ガイド:ログ設計・メトリクス・トレースのベストプラクティス

Ruby の本番運用ガイド:ログ設計・メトリクス・トレースのベストプラクティス

Ruby アプリケーションを本番環境で安定稼働させるには、適切な観測性(Observability)の実装が欠かせません。障害が発生したとき、原因を素早く特定できるかどうかは、ログ・メトリクス・トレースという 3 つの柱をどう設計するかにかかっています。

本記事では、Ruby アプリケーションの本番運用において、効果的なログ設計、メトリクス収集、分散トレーシングのベストプラクティスを詳しく解説します。実際のコード例とともに、運用現場ですぐに活用できる実践的な手法をご紹介しましょう。

背景

Observability の重要性

現代の Web アプリケーションは複雑化の一途を辿っています。マイクロサービスアーキテクチャ、非同期ジョブ処理、外部 API 連携など、システムの構成要素が増えるほど、問題の切り分けが難しくなるでしょう。

観測性とは、システムの内部状態を外部から観察可能にすることです。ログ・メトリクス・トレースという 3 つの観点から情報を収集することで、システムの健全性を継続的に監視できます。

3 つの柱の役割

観測性を支える 3 つの柱には、それぞれ明確な役割があります。

mermaidflowchart TB
    obs["Observability<br/>観測性"] --> logs["Logs<br/>ログ"]
    obs --> metrics["Metrics<br/>メトリクス"]
    obs --> traces["Traces<br/>トレース"]

    logs --> logsDesc["何が起きたかを<br/>詳細に記録"]
    metrics --> metricsDesc["システムの状態を<br/>数値で測定"]
    traces --> tracesDesc["処理の流れを<br/>追跡・可視化"]

    logsDesc --> insight["インサイト獲得"]
    metricsDesc --> insight
    tracesDesc --> insight

ログは「何が起きたか」を時系列で記録します。メトリクスは「どのくらい」という数値データを収集するものです。トレースは「どう処理されたか」というリクエストの経路を追跡します。

これら 3 つを組み合わせることで、障害発生時の原因特定から、パフォーマンス改善、ユーザー体験の最適化まで、幅広い課題に対応できるようになります。

Ruby エコシステムの現状

Ruby には優れたロギングライブラリや APM ツールが揃っています。標準ライブラリの Logger から、構造化ログを実現する SemanticLogger、分散トレーシングの OpenTelemetry まで、選択肢は豊富です。

Rails、Sidekiq、Rake といった主要フレームワークも、それぞれ観測性を高める仕組みを提供しています。これらを適切に組み合わせることが、本番運用の成功につながるでしょう。

課題

ログ設計の課題

本番環境でよく見られるログ設計の問題をまとめました。

#課題影響
1ログレベルの不適切な使用重要な情報が埋もれる
2非構造化ログ検索・集計が困難
3コンテキスト情報の欠如原因特定に時間がかかる
4機密情報の混入セキュリティリスク
5ログの肥大化ストレージコスト増大

ログレベルを適切に設定しないと、DEBUG レベルで大量のログが出力され、本当に必要な情報が見つけられません。また、単純な文字列ログでは、後からの分析が非効率になってしまいます。

リクエスト ID やユーザー ID といったコンテキスト情報が欠けていると、複数のログを関連付けられず、問題の全体像が把握できなくなるでしょう。

メトリクス収集の課題

メトリクス設計でよくある落とし穴を整理します。

mermaidflowchart LR
    start["メトリクス収集開始"] --> problem1["何を測るべきか<br/>不明確"]
    start --> problem2["カーディナリティ<br/>爆発"]
    start --> problem3["メトリクス名が<br/>不統一"]

    problem1 --> impact1["無駄な収集<br/>コスト増"]
    problem2 --> impact2["データベース<br/>負荷増大"]
    problem3 --> impact3["ダッシュボード<br/>作成困難"]

    impact1 --> result["運用品質低下"]
    impact2 --> result
    impact3 --> result

何を測定すべきかが明確でないと、無駄にデータを収集してしまいます。カーディナリティ(一意な値の数)が高すぎるラベルを使うと、メトリクスデータベースに過度な負荷がかかるでしょう。

メトリクス名の命名規則が統一されていないと、複数のチームが異なる形式でメトリクスを登録し、ダッシュボード作成時に混乱が生じます。

トレーシングの課題

分散トレーシングの実装では、以下のような課題に直面します。

実装の複雑さ

すべてのサービスでトレーシングを有効化し、トレース ID を伝播させる必要があります。既存のコードベースに後から導入する場合、変更範囲が広くなるでしょう。

パフォーマンスへの影響

トレーシング情報の収集と送信は、少なからずオーバーヘッドを発生させます。サンプリング率を適切に設定しないと、アプリケーションのレスポンス時間に影響が出てしまいます。

コスト管理

トレースデータは量が多く、APM サービスの利用料金が予想以上に高額になることがあります。必要なデータを見極め、適切なサンプリング戦略を立てる必要があるでしょう。

解決策

ログ設計のベストプラクティス

効果的なログ設計には、いくつかの重要な原則があります。

構造化ログの採用

JSON 形式の構造化ログを採用することで、検索性と集計性が劇的に向上します。SemanticLogger や LogStashLogger といったライブラリを活用しましょう。

構造化ログでは、各ログエントリが明確なフィールドを持ちます。タイムスタンプ、ログレベル、メッセージ、コンテキスト情報などを分離して記録できるため、後からの分析が容易になるでしょう。

ログレベルの使い分け

適切なログレベルの使い分けが、運用効率を左右します。

#レベル用途
1FATAL即座に対応が必要データベース接続不可
2ERRORエラーだが処理継続可能外部 API 呼び出し失敗
3WARN警告、将来的に問題の可能性非推奨メソッド使用
4INFO重要なイベントユーザーログイン
5DEBUGデバッグ情報変数の値

本番環境では INFO 以上、開発環境では DEBUG 以上といった設定が一般的です。必要に応じて動的にログレベルを変更できる仕組みも用意しておくと良いでしょう。

コンテキスト情報の付与

すべてのログにリクエスト ID やユーザー ID を含めることで、分散したログを関連付けられます。

mermaidsequenceDiagram
    participant Client as クライアント
    participant App as Railsアプリ
    participant Worker as Sidekiqワーカー
    participant DB as データベース

    Client->>App: リクエスト送信
    Note over App: request_id生成
    App->>App: ログ出力<br/>(request_id付与)
    App->>Worker: ジョブ投入<br/>(request_id引き継ぎ)
    Worker->>Worker: ログ出力<br/>(request_id付与)
    Worker->>DB: クエリ実行
    Worker->>App: 処理完了
    App->>Client: レスポンス返却

この図は、リクエスト ID がアプリケーション全体でどのように伝播するかを示しています。同じリクエスト ID でログを検索すれば、一連の処理フローを追跡できるでしょう。

メトリクス設計のベストプラクティス

効果的なメトリクス設計では、以下のポイントを押さえます。

RED/USE メソッドの活用

サービスレベルでは RED メソッド、リソースレベルでは USE メソッドを適用します。

RED メソッド(サービス監視)

  • Rate: リクエスト数
  • Errors: エラー率
  • Duration: レスポンス時間

USE メソッド(リソース監視)

  • Utilization: 使用率
  • Saturation: 飽和度
  • Errors: エラー数

これらの指標を継続的に監視することで、システムの健全性を把握できます。

メトリクス命名規則

一貫性のある命名規則により、メトリクスの管理が容易になります。

php-template<namespace>_<name>_<unit>

例:

  • app_http_requests_total(累計リクエスト数)
  • app_http_request_duration_seconds(リクエスト処理時間)
  • sidekiq_jobs_processed_total(処理済みジョブ数)

名前空間でアプリケーションやコンポーネントを区別し、単位を明示することで、メトリクスの意味が明確になるでしょう。

カーディナリティの管理

ラベルの値が無制限に増えないよう注意します。ユーザー ID のような高カーディナリティな値は避け、ユーザー種別のような低カーディナリティな値を使用しましょう。

トレーシングのベストプラクティス

分散トレーシングを効果的に実装するためのポイントです。

サンプリング戦略

すべてのリクエストをトレースするのではなく、適切なサンプリング率を設定します。

mermaidflowchart TB
    request["リクエスト受信"] --> decision{"サンプリング<br/>判定"}
    decision -->|サンプル対象| trace["トレース収集"]
    decision -->|対象外| skip["スキップ"]

    trace --> error{"エラー?"}
    error -->|Yes| always["必ず記録"]
    error -->|No| sample["サンプリング率<br/>に従う"]

    always --> send["APMへ送信"]
    sample --> send
    skip --> end_process["処理継続"]
    send --> end_process

エラーが発生したリクエストは必ずトレースを記録し、正常なリクエストは一定の割合でサンプリングする戦略が効果的です。

スパンの適切な粒度

トレーシングで記録するスパン(処理単位)の粒度を適切に設定します。細かすぎるとオーバーヘッドが増え、粗すぎると詳細が分かりません。

一般的には、以下の単位でスパンを作成すると良いでしょう。

  • HTTP リクエスト/レスポンス
  • データベースクエリ
  • 外部 API 呼び出し
  • 重要なビジネスロジック

具体例

ログ設計の実装例

SemanticLogger を使った構造化ログの実装を見ていきます。

Gem のインストール

まず、必要な Gem をインストールします。

ruby# Gemfile
gem 'semantic_logger'
gem 'rails_semantic_logger'

Bundler でインストールを実行しましょう。

bashbundle install

初期設定

アプリケーション起動時に SemanticLogger を設定します。

ruby# config/initializers/semantic_logger.rb

# ログレベルの設定(環境変数で切り替え可能)
SemanticLogger.default_level = ENV.fetch('LOG_LEVEL', 'info').to_sym

# JSON形式でログを出力
SemanticLogger.add_appender(
  io: $stdout,
  formatter: :json
)

この設定により、すべてのログが JSON 形式で標準出力に出力されます。環境変数でログレベルを制御できるため、本番環境で問題が発生したときに動的に DEBUG レベルに切り替えられるでしょう。

コンテキスト情報の付与

リクエストごとにユニークな ID を付与し、すべてのログに含めます。

ruby# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_logging_context

  private

  def set_logging_context
    # リクエストIDを取得(RailsのデフォルトRequest ID)
    request_id = request.uuid

    # ユーザー情報を取得
    user_id = current_user&.id
    user_email = current_user&.email

    # SemanticLoggerのコンテキストに設定
    logger.with_payload(
      request_id: request_id,
      user_id: user_id,
      user_email: user_email
    )
  end
end

これにより、コントローラー内で出力されるすべてのログに、リクエスト ID とユーザー情報が自動的に付与されます。

ビジネスロジックでのログ出力

適切なログレベルとメッセージで、重要なイベントを記録します。

ruby# app/services/order_service.rb
class OrderService
  include SemanticLogger::Loggable

  def create_order(user, items)
    # 処理開始をINFOレベルで記録
    logger.info(
      'Order creation started',
      user_id: user.id,
      item_count: items.size
    )

    begin
      order = Order.create!(user: user, items: items)

      # 成功をINFOレベルで記録
      logger.info(
        'Order created successfully',
        order_id: order.id,
        total_amount: order.total_amount
      )

      order
    rescue ActiveRecord::RecordInvalid => e
      # バリデーションエラーをWARNレベルで記録
      logger.warn(
        'Order validation failed',
        errors: e.record.errors.full_messages
      )
      raise
    rescue => e
      # 予期しないエラーをERRORレベルで記録
      logger.error(
        'Order creation failed',
        exception: e.class.name,
        message: e.message,
        backtrace: e.backtrace.first(5)
      )
      raise
    end
  end
end

このコードでは、処理の開始、成功、各種エラーをそれぞれ適切なレベルで記録しています。エラー発生時には例外の詳細情報も含めることで、原因特定が容易になるでしょう。

機密情報のフィルタリング

パスワードやトークンなどの機密情報を自動的にマスクします。

ruby# config/initializers/semantic_logger.rb

# フィルタ対象のキーを定義
FILTERED_KEYS = %w[
  password
  password_confirmation
  api_key
  access_token
  secret
  credit_card
].freeze

# カスタムフィルタを設定
SemanticLogger.on_log do |log|
  if log.payload
    log.payload.transform_values! do |value|
      if value.is_a?(Hash)
        filter_sensitive_data(value)
      else
        value
      end
    end
  end
end

def filter_sensitive_data(hash)
  hash.transform_values do |value|
    case value
    when Hash
      filter_sensitive_data(value)
    when String
      FILTERED_KEYS.any? { |key| hash.key?(key) } ? '[FILTERED]' : value
    else
      value
    end
  end
end

この仕組みにより、ログに機密情報が誤って含まれるリスクを軽減できます。

メトリクス収集の実装例

Prometheus クライアントライブラリと Yabeda gem を使ってメトリクスを収集します。

Gem のインストール

ruby# Gemfile
gem 'yabeda-rails'
gem 'yabeda-sidekiq'
gem 'yabeda-prometheus'
gem 'yabeda-puma-plugin'

Yabeda は、Ruby アプリケーション向けの統一されたメトリクスインターフェースを提供するライブラリです。

メトリクスの定義

カスタムメトリクスを定義します。

ruby# config/initializers/yabeda.rb

Yabeda.configure do
  # カウンタ:注文作成の試行回数
  counter :orders_created_total,
          comment: 'Total number of order creation attempts',
          tags: [:status]

  # ヒストグラム:注文処理時間
  histogram :order_processing_duration_seconds,
            comment: 'Time spent processing orders',
            unit: :seconds,
            buckets: [0.1, 0.5, 1, 2, 5, 10],
            tags: [:order_type]

  # ゲージ:現在の在庫数
  gauge :inventory_stock_level,
        comment: 'Current inventory stock level',
        tags: [:product_id]
end

メトリクスタイプにはカウンタ、ヒストグラム、ゲージの 3 種類があります。それぞれ異なる用途で使用するため、適切に選択しましょう。

メトリクスの記録

ビジネスロジック内でメトリクスを記録します。

ruby# app/services/order_service.rb
class OrderService
  include SemanticLogger::Loggable

  def create_order(user, items)
    # 処理時間を測定
    start_time = Time.current

    begin
      order = Order.create!(user: user, items: items)

      # 成功時のメトリクス記録
      Yabeda.orders_created_total.increment(
        status: 'success'
      )

      # 処理時間を記録
      duration = Time.current - start_time
      Yabeda.order_processing_duration_seconds.measure(
        { order_type: order.order_type },
        duration
      )

      order
    rescue => e
      # 失敗時のメトリクス記録
      Yabeda.orders_created_total.increment(
        status: 'failure'
      )

      raise
    end
  end

  def update_inventory(product_id, quantity)
    product = Product.find(product_id)
    product.update!(stock: quantity)

    # 在庫レベルをゲージで記録
    Yabeda.inventory_stock_level.set(
      { product_id: product_id },
      quantity
    )
  end
end

このコードでは、注文処理の成功・失敗をカウントし、処理時間をヒストグラムで記録しています。在庫数はゲージで現在値を記録するでしょう。

Prometheus エンドポイントの設定

メトリクスを Prometheus から収集できるようにエンドポイントを設定します。

ruby# config/routes.rb
Rails.application.routes.draw do
  # Prometheusメトリクスエンドポイント
  mount Yabeda::Prometheus::Exporter => '/metrics'
end

このエンドポイントにアクセスすると、Prometheus 形式でメトリクスが返されます。

Prometheus スクレイプ設定

Prometheus 側で定期的にメトリクスを収集する設定を行います。

yaml# prometheus.yml
scrape_configs:
  - job_name: 'rails_app'
    scrape_interval: 15s
    static_configs:
      - targets: ['app:3000']
    metrics_path: '/metrics'

15 秒ごとに Rails アプリケーションの​/​metricsエンドポイントからメトリクスを取得します。

トレーシングの実装例

OpenTelemetry を使って分散トレーシングを実装します。

Gem のインストール

OpenTelemetry 関連の Gem をインストールしましょう。

ruby# Gemfile
gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-all'

OpenTelemetry は、トレーシングとメトリクスの標準規格です。

OpenTelemetry の初期設定

アプリケーション起動時に OpenTelemetry を設定します。

ruby# config/initializers/opentelemetry.rb
require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

OpenTelemetry::SDK.configure do |c|
  # サービス名の設定
  c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'rails-app')

  # OTLP Exporterの設定(Jaeger、Datadogなどに送信)
  c.use_all(
    'OpenTelemetry::Instrumentation::Rails' => { enabled: true },
    'OpenTelemetry::Instrumentation::ActiveRecord' => { enabled: true },
    'OpenTelemetry::Instrumentation::Sidekiq' => { enabled: true },
    'OpenTelemetry::Instrumentation::Net::HTTP' => { enabled: true }
  )
end

この設定により、Rails、ActiveRecord、Sidekiq、HTTP 通信が自動的にトレースされます。

カスタムスパンの作成

ビジネスロジック内で独自のスパンを作成し、詳細な処理フローを記録します。

ruby# app/services/order_service.rb
class OrderService
  def create_order(user, items)
    tracer = OpenTelemetry.tracer_provider.tracer('order_service')

    # カスタムスパンを作成
    tracer.in_span('create_order', attributes: {
      'user.id' => user.id,
      'items.count' => items.size
    }) do |span|

      # 在庫確認のスパン
      tracer.in_span('check_inventory') do
        check_inventory_availability(items)
      end

      # 注文作成のスパン
      order = tracer.in_span('create_order_record') do
        Order.create!(user: user, items: items)
      end

      # 決済処理のスパン
      tracer.in_span('process_payment', attributes: {
        'order.id' => order.id,
        'amount' => order.total_amount
      }) do
        process_payment(order)
      end

      # スパンに結果情報を追加
      span.set_attribute('order.id', order.id)
      span.set_attribute('order.status', order.status)

      order
    end
  rescue => e
    # エラー情報をスパンに記録
    span = OpenTelemetry::Trace.current_span
    span.record_exception(e)
    span.status = OpenTelemetry::Trace::Status.error(e.message)
    raise
  end
end

このコードでは、注文作成プロセスを複数のスパンに分割しています。各スパンに属性を付与することで、トレース分析時に詳細な情報が得られるでしょう。

サンプリング設定

すべてのリクエストをトレースするとオーバーヘッドが大きいため、サンプリング率を設定します。

ruby# config/initializers/opentelemetry.rb

# カスタムサンプラーの定義
class AdaptiveSampler
  def initialize(default_rate: 0.1, error_rate: 1.0)
    @default_rate = default_rate
    @error_rate = error_rate
  end

  def should_sample?(trace_id, parent_context, links, name, kind, attributes)
    # エラーが発生している場合は必ずサンプル
    if attributes['error'] == true
      OpenTelemetry::SDK::Trace::Samplers::Result.new(
        decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE
      )
    else
      # 通常は設定された確率でサンプル
      if Random.rand < @default_rate
        OpenTelemetry::SDK::Trace::Samplers::Result.new(
          decision: OpenTelemetry::SDK::Trace::Samplers::Decision::RECORD_AND_SAMPLE
        )
      else
        OpenTelemetry::SDK::Trace::Samplers::Result.new(
          decision: OpenTelemetry::SDK::Trace::Samplers::Decision::DROP
        )
      end
    end
  end

  def description
    "AdaptiveSampler{default=#{@default_rate}, error=#{@error_rate}}"
  end
end

OpenTelemetry::SDK.configure do |c|
  c.sampler = AdaptiveSampler.new(default_rate: 0.1)
end

このサンプラーは、通常のリクエストは 10%をサンプリングし、エラーが発生したリクエストは 100%をトレースします。

トレース ID の伝播

非同期ジョブにトレースコンテキストを引き継ぐ設定です。

ruby# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
  around_perform do |job, block|
    # ジョブのメタデータからトレースコンテキストを取得
    trace_context = job.arguments.last if job.arguments.last.is_a?(Hash)

    if trace_context && trace_context[:trace_id]
      # トレースコンテキストを復元
      OpenTelemetry.propagation.extract(trace_context)
    end

    # ジョブ実行をトレース
    tracer = OpenTelemetry.tracer_provider.tracer('sidekiq')
    tracer.in_span(job.class.name) do
      block.call
    end
  end
end

この実装により、Web リクエストから起動された非同期ジョブでも、同じトレースとして追跡できます。

統合ダッシュボードの構築

ログ、メトリクス、トレースを統合的に可視化します。

Grafana ダッシュボード設定

Grafana を使って、メトリクスとログを同一画面で表示します。

json{
  "dashboard": {
    "title": "Rails Application Observability",
    "panels": [
      {
        "title": "Request Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total[5m])"
          }
        ]
      },
      {
        "title": "Error Rate",
        "targets": [
          {
            "expr": "rate(http_requests_total{status=~\"5..\"}[5m])"
          }
        ]
      },
      {
        "title": "Response Time (p95)",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, http_request_duration_seconds_bucket)"
          }
        ]
      },
      {
        "title": "Recent Errors",
        "type": "logs",
        "targets": [
          {
            "expr": "{level=\"error\"}"
          }
        ]
      }
    ]
  }
}

このダッシュボードでは、リクエスト数、エラー率、レスポンス時間、最近のエラーログを一画面で確認できます。

アラート設定

異常を検知したら自動的に通知する設定を行います。

yaml# alertmanager.yml
groups:
  - name: rails_app_alerts
    interval: 30s
    rules:
      # エラー率が5%を超えたらアラート
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
        for: 5m
        annotations:
          summary: 'High error rate detected'
          description: 'Error rate is {{ $value }} requests/sec'

      # レスポンス時間が3秒を超えたらアラート
      - alert: SlowResponse
        expr: histogram_quantile(0.95, http_request_duration_seconds_bucket) > 3
        for: 5m
        annotations:
          summary: 'Slow response time detected'
          description: '95th percentile response time is {{ $value }} seconds'

      # Sidekiqのキュー滞留が100を超えたらアラート
      - alert: SidekiqQueueBacklog
        expr: sidekiq_queue_size > 100
        for: 10m
        annotations:
          summary: 'Sidekiq queue backlog'
          description: 'Queue size is {{ $value }}'

これらのアラートルールにより、問題を早期に検知して対応できるようになります。

まとめ

Ruby アプリケーションの本番運用において、ログ・メトリクス・トレースの 3 つの観点から観測性を実装することが、安定稼働の鍵となります。

構造化ログを採用し、適切なログレベルとコンテキスト情報を付与することで、障害発生時の原因特定が迅速になるでしょう。SemanticLogger を活用すれば、JSON 形式のログを簡単に実装できます。

メトリクス収集では、RED/USE メソッドに基づいて重要な指標を定義し、一貫性のある命名規則を採用することが大切です。Yabeda gem と Prometheus の組み合わせにより、効率的なメトリクス収集が実現できるでしょう。

分散トレーシングは、OpenTelemetry を使うことで標準化された方法で実装できます。適切なサンプリング戦略を立て、カスタムスパンで重要な処理を可視化することで、パフォーマンス問題の特定が容易になります。

これら 3 つの柱を統合的に運用することで、システムの健全性を継続的に監視し、問題を未然に防げるようになるでしょう。本記事で紹介したベストプラクティスを参考に、ぜひ自社の Ruby アプリケーションに観測性を実装してみてください。

関連リンク