Sidekiqをモニタリングする [翻訳]

# Sidekiqをモニタリングする

最終更新 2019/10/31 編集者 RKushnir

運用中のSidekiqプロセスを監視するツールを使用して、常に稼働しており、メモリやCPUを過度に使用していないことを確認することをお勧めします。
[Inspeqtor](https://github.com/mperham/inspeqtor)を作ったのは、利用可能な既存のツール（たとえば、monit、god、bluepill）が気に入らなかったためです。
私のおすすめは、

1. [Upstart](https://github.com/mperham/sidekiq/tree/master/examples/upstart)または[Systemd](https://github.com/mperham/sidekiq/tree/master/examples/systemd)を使用してSidekiqを起動/停止する。 こうすることで、Ruby VMがクラッシュした場合、プロセスはすぐに再起動します。

2. Inspeqtorを使用してCPUとメモリの使用量を監視し、必要に応じてSidekiqを再起動する。

## Web UI

Sidekiqには、Sidekiqの現在の状態を表示できるWebアプリケーションが付属しています。

### Rails

次のコードを `config/routes.rb` に追加してください。

```ruby
require 'sidekiq/web'
mount Sidekiq::Web => '/sidekiq'
```

#### Forbidden エラー

フォームを送信しようとしたときに `Forbidden` エラーを受け取った場合、有効なセッションが構成されていないことが原因です。
CSRF攻撃を防ぐために、このような設定がなされています。
Railsと同じセッションを共有するようにWebアプリケーションを構成する必要があります。
require文の後にこのコードを `routes.rb` に入れてみてください。

```ruby
# Rails < 4:
Sidekiq::Web.set :session_secret, Rails.configuration.secret_token

# 5.2 > Rails >= 4:
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]

# Rails >= 5.2:
Sidekiq::Web.set :session_secret, Rails.application.credentials[:secret_key_base]
```

上記のコードがうまくいかない場合は、`rack-protection` のどの部分が問題を引き起こしているのかをデバッグするために下記のコードが役に立ちます。
セッションを設定するコードの直後に、次のコードを追加してください。

```ruby
Sidekiq::Web.use(::Rack::Protection, { use: :authenticity_token, logger: Rails.logger, message: "Didn't work!" })
```

`rack-protection` がアクションを妨げている場合、「Forbidden」の代わりに「Didn't Work」と表示されるはずです。
そうでなかった場合は、`Sidekiq::Web.use` の呼び出しが起動プロセスのできるだけ早い段階でWebアプリケーションに追加されていることを確認してください。
デフォルトでは、ログはアプリサーバーのstderrに記録されます。ログをより細かく制御するために `:logger` オプションを追加できます。
詳細については、[rack-protection](https：//github.com/sinatra/rack-protection/blob/pre-merge/lib/rack/protection/base.rb)を参照してください。

ヒント：以前は動作していたのに、アップグレード後に突然動作しなくなったという場合は、**Cookieをクリア** してみてください。
互換性のないCSRFトークンが、古いバージョンの rack-protection によって使用されている可能性があります。
セッションをクリアすると、新しい有効なトークンが生成されます。

ヒント：ロードバランサー -> リバースプロキシ -> Webアプリケーションサーバーという構成の場合は、`X-Forwarded-Proto` ヘッダーが適切に設定されていることを確認してください。
ロードバランサーがSSLターミネーションポイントである場合、内部的な値ではなく、ロードバランサーから転送された値に設定されていることを確認する必要があります。（そうでない場合、`rack-protection` は、httpsではなくhttp経由でのリクエストを使用します）

#### セッションが失われる問題

Web UIを使用しているときに[セッションが上書きされている](https://github.com/redis-store/redis-rails/issues/34)場合、初期化のタイミングで[Sidekiqのセッションを完全に無効](https://github.com/mperham/sidekiq/pull/3183)にできます。

```ruby
require 'sidekiq/web'
Sidekiq::Web.set :sessions, false
```

#### 認証

本番アプリケーションでは、認証情報へのアクセスを保護する必要があります。これを実現するには、ルーティングの制約機能（config/routes.rbファイル内）を使用できます。

##### Devise

認証された `User` を許可する。

```ruby
# config/routes.rb
authenticate :user do
  mount Sidekiq::Web => '/sidekiq'
end
```

上記に加えて、`User#admin?` が true を返すことを確認する。

```ruby
# config/routes.rb
authenticate :user, lambda { |u| u.admin? } do
  mount Sidekiq::Web => '/sidekiq'
end
```

##### Clearance

Clearanceは特定のルーティングへのアクセスを制限するために[ルーティング制約](https://github.com/thoughtbot/clearance#access-control)機能を提供しています。

```ruby
Blog::Application.routes.draw do
  # すべての認証されたユーザーのアクセスを制限する
  constraints Clearance::Constraints::SignedIn.new do
    mount Sidekiq::Web, at: '/sidekiq'
  end

  # すべての認証されたアドミンのアクセスを制限する
  constraints Clearance::Constraints::SignedIn.new { |user| user.admin? } do
    mount Sidekiq::Web, at: '/sidekiq'
  end
end
```

##### Authlogic

```ruby
# lib/admin_constraint.rb
class AdminConstraint
  def matches?(request)
    return false unless request.cookie_jar['user_credentials'].present?
    user = User.find_by_persistence_token(request.cookie_jar['user_credentials'].split(':')[0])
    user && user.admin?
  end
end

# config/routes.rb
require "admin_constraint"
mount Sidekiq::Web => '/sidekiq', :constraints => AdminConstraint.new
```

###### Authlogic 4+

Authlogic 4.0.0からセキュアCookieが有効になったため、AdminConstraintのCookieからのuser_credentialsの読み取りはデフォルトのセットアップでは失敗します。
代わりに、セッションから直接読み取ることができます。

```ruby
# lib/admin_constraint.rb
class AdminConstraint
  def matches?(request)
    return false unless request.session.has_key?(:user_credentials)

    user = User.find_by_persistence_token(request.session.fetch(:user_credentials))
    user && user.admin?
  end
end
```

##### Restful Authentication or Sorcery

`User` モデルのインスタンスが `admin?` に応答するか確認するコードは下記の通りです。

```ruby
# lib/admin_constraint.rb
class AdminConstraint
  def matches?(request)
    return false unless request.session[:user_id]
    user = User.find request.session[:user_id]
    user && user.admin?
  end
end

# config/routes.rb
require 'sidekiq/web'
require 'admin_constraint'
mount Sidekiq::Web => '/sidekiq', :constraints => AdminConstraint.new
```

##### 独自にカスタマイズされた認証

```ruby
class AuthConstraint
  def self.admin?(request)
    return false unless (cookie = request.cookie_jar['auth'])

    Rails.cache.fetch(cookie['user'], :expires_in => 1.minute) do
      auth_data = JSON.parse(Base64.decode64(cookie['data']))
      response = HTTParty.post(Auth.validate_url, :query => auth_data)

      response.code == 200 && JSON.parse(response.body)['roles'].to_a.include?('Admin')
    end
  end
end

# config/routes.rb
constraints lambda {|request| AuthConstraint.admin?(request) } do
  mount Sidekiq::Web => '/admin/sidekiq'
end
```

##### Rails with Google authentication

@jonhyman が[Google認証をSidekiqに適用する方法](https://github.com/mperham/sidekiq/issues/2460)について書いてくれています。

##### `warden/github` を使用したGithub認証

Rackベースの `Sidekiq::Web UI` (since Sidekiq 4.2.0) の開発者による [Gist](https://gist.github.com/badosu/5a6bb42cc0c0a7ae6e9b01313c61a5b9) を参照してください。

##### routes.rbにベーシック認証のコードを直接書く

```ruby
# config/routes.rb
require "sidekiq/web"
Sidekiq::Web.use Rack::Auth::Basic do |username, password|
  # Protect against timing attacks:
  # - See https://codahale.com/a-lesson-in-timing-attacks/
  # - See https://thisdata.com/blog/timing-attacks-against-string-comparison/
  # - Use & (do not use &&) so that it doesn't short circuit.
  # - Use digests to stop length information leaking (see also ActiveSupport::SecurityUtils.variable_size_secure_compare)
  ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USERNAME"])) &
    ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"]))
end if Rails.env.production?
mount Sidekiq::Web, at: "/sidekiq"
```

`ActionDispatch::Request::Session` のエラーが起きたなら、その原因はRailsとRackの互換性がないことです。
回避策としては[このコメント](https://github.com/rails/rails/issues/15843#issuecomment-125784043)を見てください。

Wardenを使っており `failure_app` をグローバルにセットしている（Railsの `application.rb` などで）なら、ブラウザのベーシック認証のプロンプトが表示されません。その理由は、Wardenがステータスコード 401に対して `failed_app` を呼び出すためです。
この挙動をバイパスするためには、一例として下記のようなコードを使ってください。

```ruby
class AllowBasicAuthPrompt
  def initialize(app); @app = app; end
  def call(env)
    # Tell Warden to not intercept the 401
    env['warden'].custom_failure!; @app.call(env)
  end
end
Sidekiq::Web.use AllowBasicAuthPrompt
Sidekiq::Web.use Rack::Auth::Basic do |username, password|
  ...
end
```

### 単体で（Railsなしで）動作させる

選択したRackサーバーで `Sidekiq::Web` を起動するための `config.ru` の例を次に示します。

```ruby
require 'sidekiq'

Sidekiq.configure_client do |config|
  config.redis = { :size => 1 }
end

require 'sidekiq/web'
run Sidekiq::Web
```

Herokuで[ワンクリックデプロイをする方法](https://heroku.com/deploy?template=https://github.com/honeypotio/sidekiq_dashboard)もあります。

Sidekiqを既存のRackアプリケーションにもマウントできます。

```ruby
require 'your_app'

require 'sidekiq/web'
run Rack::URLMap.new('/' => Sinatra::Application, '/sidekiq' => Sidekiq::Web)
```

#### RackセッションとWeb攻撃に対する保護

`Sidekiq::Web` が機能するには、有効なRackセッションが必要であることに注意してください。Web UIでボタンをクリックしたときに `Forbidden` エラーが表示される場合は、Rackセッションが正しく構成されていないことが原因です。
Sidekiqはセッションを設定できません。システムで有効なセッションを設定する方法がわからない場合、最善のオプションは、StackOverflowを検索するか、Web UIの実行に使用しているコードで質問を投稿することです。

`Sidekiq::Web` は `Rack::Protection` を使用して、一般的なWeb攻撃（[CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery)、[XSS](https://en.wikipedia.org/wiki/Cross-site_scripting)など）からアプリケーションを保護します。
`Rack::Protection` は、リクエストがセキュリティ要件を満たさないと判断した場合、セッションを無効にし、`Forbidden` エラーを発生させます。
考えられる状況の1つは、アプリケーションがリバースプロキシの背後で動作し、重要なヘッダーを渡さないことです（`X-Forwarded-For`、`X-Forwarded-Proto`）。
このような状況と解決策は、[この記事](https://www.sourcediver.org/blog/2015/07/01/rack-protection-and-nginx/)と[イシュー #2560](https://github.com/mperham/sidekiq/issues/2560)に記載されています。

Railsアプリにワイルドカードドメインがあり、それらのすべてからWeb UIにアクセスさせたい場合は、[イシュー #2730](https://github.com/mperham/sidekiq/issues/2730) を参照してください。

----

![web-ui.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33011/859c3b91-567c-c159-71d0-b93ceb2fef3d.png)

設定がうまくいっているなら、ブラウザには次のように表示されるはずです。

### 単体で動かしつつベーシック認証を使用する

```ruby
# this code goes in your config.ru
require 'sidekiq'

Sidekiq.configure_client do |config|
  config.redis = { :size => 1 }
end

require 'sidekiq/web'
map '/sidekiq' do
  use Rack::Auth::Basic, "Protected Area" do |username, password|
    # Protect against timing attacks:
    # - See https://codahale.com/a-lesson-in-timing-attacks/
    # - See https://thisdata.com/blog/timing-attacks-against-string-comparison/
    # - Use & (do not use &&) so that it doesn't short circuit.
    # - Use digests to stop length information leaking
    Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_USERNAME"])) &
      Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SIDEKIQ_PASSWORD"]))
  end

  run Sidekiq::Web
end
```

### Nagios

以下のURLは、特定のキューの深さが特定の範囲内にあることを検証する [check_sidekiq_queue](https://github.com/wanelo/nagios-checks/blob/master/check_sidekiq_queue) スクリプトを含むNagiosチェックのコレクションです。
redis-cliコマンドラインツールを使用する単純なシェルスクリプトであり、rubyに依存しません。

https://github.com/wanelo/nagios-checks

### Scout

[Scount](https://scoutapm.com/)はRailsアプリケーションのモニタリングサービスです。
このサービスは下記の機能を提供します。

1. 各Sidekiqワーカーの主要なメトリック（平均および95パーセンタイルの実行時間、遅延、エラー率など）。

1. 個々のジョブのタイミングとメモリ割り当ての両方のGitHub拡張トランザクショントレース。


### Pingdom Server Monitoring

[Pingdom Server Monitoring](http://server-monitor.pingdom.com/)には、Sidekiqワーカーを監視するための[プラグイン](https://server-monitor.pingdom.com/plugin_urls/7111-sidekiq-monitor)があります。

このプラグインは、通常のジョブに加えて時間指定されたジョブと再試行について、エンキュー、失敗、および処理されたジョブ数を監視します。利用可能なメトリックのアラートを設定することもできます。

### Queue Backlog をモニタリングする

Pingdomで単純なHTTPエンドポイントを使用して、Sidekiqのdefaultキューのバックログのサイズを確認できます。
下記のコードを `config/routes.rb` に入れてください。

```ruby
require 'sidekiq/api'
match "queue-status" => proc { [200, {"Content-Type" => "text/plain"}, [Sidekiq::Queue.new.size < 100 ? "OK" : "UHOH" ]] }, via: :get
```

こうすることで、http://example.com/queue-status にアクセスしたときのレスポンスbodyは「OK」または「UHOH」になります。
Pingdomチェックを1分ごとに実行し、response == 'UHOH'の場合にメールを送信するといったことができます。

### Queue Latency をモニタリングする

#### カスタムエンドポイントを使用する

キューにたくさんのジョブを投入すると、キューのバックログを監視するときに誤検知が発生する可能性があります。（例：各ジョブが高速に処理されているなら問題ないのに、バックログの数が多いことを問題視してしまう）

代わりに、キューの待ち時間を監視してください。キューの待ち時間は、最も古いジョブがキューにプッシュされた時間と現在の時間との差です。下記のコードは、ジョブがキューに入れられてから30秒以上処理されていないかどうかをチェックします。`config/routes.rb` に入れてください。

```ruby
require 'sidekiq/api'
match "queue-latency" => proc { [200, {"Content-Type" => "text/plain"}, [Sidekiq::Queue.new.latency < 30 ? "OK" : "UHOH" ]] }, via: :get
```

こうすることで、http://example.com/queue-latency にアクセスしたときのレスポンスbodyは「OK」または「UHOH」のいずれかになります。

#### 組み込みのダッシュボードAPIを使用する

Sidekiqは、`/sidekiq/stats` にJSON形式のダッシュボードAPIを提供しています。このエンドポイントのレスポンスは下記の通りです。

```json
{
  "sidekiq": {
    "processed": 12345,
    "failed": 56,
    "busy": 25,
    "enqueued": 178,
    "scheduled": 0,
    "retries": 0,
    "default_latency": 12
  },
  "redis": {
    "connected_clients": "120",
    "uptime_in_days": "35",
    "used_memory_human": "602.31M",
    "used_memory_peak_human": "1.01G"
  }
}
```

#### sidekiqmon

Sidekiq 6.0には、コマンドラインに基本的な統計情報を出力する新しい `sidekiqmon` バイナリが付属しています。
REDIS_URLを使用して、`sidekiqmon` にRedisインスタンスの場所を指定してください。

```shell
> sidekiqmon # uses localhost:6379
...
> REDIS_URL=redis://redis.example.com:6380/5 sidekiqmon
...
```