16
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Chef::Handler::Elasticsearchを作った - Chefのレポートを時系列で可視化

Last updated at Posted at 2014-05-22

この記事は最終更新から1年以上経過しています。 気をつけてね。

前回 OhaiのデータをElasticsearchに入れてKibanaで見る構成管理 - Qiita に続いて、Chefの活動レポートをlogstashよろしく時系列で可視化してみます。

ログでなくレポート?

Chefもログファイルを出すんだから、そもそもfluentdやlogstashにtailで拾わせればいいんじゃないかと思うかも。
しかし、Chefのログはだら流し系で、apache httpd アクセスログなどのように一行ごとに完結しないため、そのまま投げても役立てるのは難しいです。
その点レポートはイベント単位でひとまとまりなので、収集する意味は十分にあるでしょう。

Chef::Handler::Elasticsearch

Cookbookはこちら。

higanworks-cookbooks/chef_handler_elasticsearch

以下デフォルトの挙動。

  • index template chef_handler_templateが無かったら作成する、対象はchef_handler-*
  • logstash風にするため@timestampを付与
  • Chefの成功レポートは/chef_handler-YYYY.MM.DD/success/ID にPUT
  • Chefの例外レポートは/chef_handler-YYYY.MM.DD/failure/ID にPUT
  • PUTのタイムアウトは3秒

IDはEnterprise Chefのため用意されているっぽい、Chef実行毎に生成されるユニークIDを使っています(DEBUGログで確認可能)。
このハンドラでは通常のログにも出力しているので、イベントと紐付けて参照できます。

ハンドラ部分のソース(v1.0.0時点)はこんな感じです。

libraries/default.rb
# Cookbook Name:: chef_handler_elasticsearch
# Library:: default
#
# Copyright 2014, HiganWorks LLC.
#

class Chef::Handler::Elasticsearch < ::Chef::Handler
  require 'timeout'
  attr_reader :opts, :config

  def initialize(opts = {})
    @config = {}
    @default = {
      url: 'http://localhost:9200',
      timeout: 3,
      prefix: 'chef_handler',
      prepare_template: true,
      template_order: 10,
      index_use_utc: true,
      index_date_format: "%Y.%m.%d",
      mappings: default_mapping
    }
    @opts = opts
    @opts
  end

  def default_mapping
'{
"_default_" : {
"numeric_detection" : true,
"dynamic_date_formats" : ["yyyy-MM-dd HH:mm:ss Z", "date_optional_time"]
}
}'
  end

  def report
    @default.merge!(node[:chef_handler_elasticsearch].symbolize_keys) if node[:chef_handler_elasticsearch]
    @config= @default.merge(@opts)
    Chef::Log.debug @config.to_s

    client = Chef::HTTP.new(@config[:url])
    index = "/#{@config[:prefix]}-#{build_logstash_date(data)}"

    if exception
      type = 'failure'
    else
      type = 'success'
    end

    prepare_template(client) if @config[:prepare_template]

    body = data.merge({'@timestamp' => Time.at(data[:end_time]).to_datetime.to_s})

    Chef::Log.debug "===== Puts to es following..."
    Chef::Log.debug body.to_s

    begin
      res = timeout(@config[:timeout]) {
        client.put([index, type, Chef::RequestID.instance.request_id].join('/'), body.to_json)
      }
      Chef::Log.debug "===== Response from es following..."
      Chef::Log.debug res.to_s
      Chef::Log.info "== Chef::Handler::Elasticsearch request_id: #{JSON.parse(res)['_id']}"
    rescue => e
      Chef::Log.error "== #{e.class}: Status report could not put to Elasticsearch."
    end
  end

  def build_logstash_date(data)
    if config[:index_use_utc]
      Time.at(data[:end_time]).getutc.strftime(@config[:index_date_format])
    else
      Time.at(data[:end_time]).strftime(@config[:index_date_format])
    end
  end

  def prepare_template(client)
    begin
      res = timeout(@config[:timeout]) {
        client.get("/_template/#{@config[:prefix]}_template")
      }
    rescue Net::HTTPServerException
      put_template(client)
      return
    rescue => e
      Chef::Log.error "== #{e.class}: Status report could not put to Elasticsearch."
      raise e.class, e.message
    end

    unless JSON.parse(@config[:mappings]) == JSON.parse(res)["#{@config[:prefix]}_template"]["mappings"]
      put_template(client)
    end
  end

  def put_template(client)
    begin
      Chef::Log.info "== create mapping template to Elasticsearch."
      res = timeout(@config[:timeout]) {
        client.put("/_template/#{@config[:prefix]}_template", build_template_body)
      }
    rescue => e
      Chef::Log.warn "== #{e.class}: mapping template could not put to Elasticsearch. Exiting..."
      raise e.class, e.message
    end
  end

  def build_template_body
    body = Hash.new
    body["template"] = "#{@config[:prefix]}-*"
    body["order"] = @config[:template_order]
    body["mappings"] = JSON.parse(@config[:mappings])
    Chef::Log.debug "===== Template for index following..."
    Chef::Log.debug body.to_json
    body.to_json
  end
end

使い方

次のいずれかで使えます。設定変更はattributeまたは初期化時にどうぞ。

run_listにrecipe[chef_handler_elasticsearch::default]を入れる

この場合、attributeで設定をoverrideします。

Chef::Config[:report_handlers],Chef::Config[:exception_handlers]に自前で追加する

自前のCookbookで、recipeかLibrariyで次のようにします。
metadataにdepends 'chef_handler_elasticsearch' を入れる必要があります。

Chef::Config[:report_handlers] << Chef::Handler::Elasticsearch.new

デフォルトの設定を上書きするには、初期化時にオプションで渡すか、

libraries/default.rb
Chef::Config[:report_handlers] << Chef::Handler::Elasticsearch.new(
  url: 'http://test.example.com:9200',
  timeout: 10,
)
```

自前のCookbookやRoleなどで優先度の高いattributeを使いましょう。

```ruby:attributes/default.rb
override[:chef_handler_elasticsearch][:url] = 'http://test.example.com:9200'
override[:chef_handler_elasticsearch][:timeout] = 10
```

#### solo.rbやclient.rbで指定する。

Cookbookごと環境に入れたり、余分なAttributeが増えるのを嫌う場合はハンドラのソースを適当な場所にコピーしてrequireします。


```ruby:
require 'path_to_lib/chef_handler_elasticsearch.rb'

Chef::Config[:report_handlers] << Chef::Handler::Elasticsearch.new(
  url: 'http://test.example.com:9200',
  timeout: 10,
)
```

### ハンドラ実行

ハンドラの追加が上手く言っていれば、Elasticsearchにレポートを投げるようになっています。
初回のみテンプレートを作成します。

```
[2014-05-22T03:30:15+00:00] INFO: Chef Run complete in 0.060295361 seconds  

Running handlers:       
[2014-05-22T03:30:15+00:00] INFO: Running report handlers       
[2014-05-22T03:30:15+00:00] INFO: HTTP Request Returned 404 Not Found:        
[2014-05-22T03:30:15+00:00] INFO: == create mapping template to Elasticsearch.       
[2014-05-22T03:30:19+00:00] INFO: == Chef::Handler::Elasticsearch request_id: 9368d16f-1f09-487e-9701-8acb001737fc       
  - Chef::Handler::Elasticsearch       
Running handlers complete       

```


## Kibanaでの表示調整

とりあえず標準のLogstash Dashboardで表示してみましょう。

Index Settingsから、`logstash`の文字列を`chef_handler`に変更します。`_all`でもOK。
インデックス名はハンドラの初期化時に指定できるので任意のものがつけられます。


![スクリーンショット_5_22_14__11_48_AM.png](https://qiita-image-store.s3.amazonaws.com/0/7454/807d79eb-c07a-2d2f-ec8e-6dd8ddb3a5ec.png "スクリーンショット_5_22_14__11_48_AM.png")

さて、よくみる時系列データになりましたね。
ただ、デフォルトではsuccessもfailureも一緒くたなので、ひとつ調製例を。

![スクリーンショット_5_22_14__12_34_PM.png](https://qiita-image-store.s3.amazonaws.com/0/7454/5f7e7443-92d5-555d-d66f-61a729f3b1ca.png "スクリーンショット_5_22_14__12_34_PM.png")


試しにクエリを次の2つにわけてみました。

- `_type:success`
- `_type:failure`


![スクリーンショット_5_22_14__12_36_PM.png](https://qiita-image-store.s3.amazonaws.com/0/7454/d9d6b5f4-418c-e4ef-028d-c59a898b6e90.png "スクリーンショット_5_22_14__12_36_PM.png")

色分けでスッキリ。


## 課題

index templateには改良の余地が結構ありそう。
16
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?