Solr の Prometheus Exporter を作ってみた

# お知らせ

私が開発した solr-exporter (https://github.com/mosuka/solr-exporter) は Apache Lucene/Solr プロジェクト (http://lucene.apache.org/solr/) へコントリビュートしました。
今後は Apache Solr に同梱される solr-exporter をご利用ください。
詳細は下記 URL を参照してください。
http://lucene.apache.org/solr/guide/monitoring-solr-with-prometheus-and-grafana.html

---

[Solr のモニタリングに使えるソフトウェア](https://qiita.com/mosuka/private/dde60a50707b19b537f3) で、[Apache Solr](http://lucene.apache.org/solr/) (以下「Solr」) のモニタリングに使えるソフトウェアを紹介しました。
それらは、基本的に JMX の機能を利用して Solr のメトリクスをモニタリングできるようにしているものです。
Solr でインデックスしているデータをモニタリングすることはできません。

例えば、

- アプリケーションログを保存していたら、エラーがどれくらい、どのシステムで発生しているか
- EC であれば、商品カテゴリー毎の商品数の推移
- ユーザー投稿型のサービスであれば、問題のあるキーワードを含むコンテンツがどれだけあるか

Solr もインデックスされたデータの可視化であれば、[Lucidworks](https://lucidworks.com) が OSS で公開している [Banana](https://github.com/lucidworks/banana) (Kibana3 相当) で行うことができます。これは [Elastic](https://www.elastic.co) が提供する [Kibana](https://www.elastic.co/jp/products/kibana) にあたるソフトウェアです。
しかし、アラーティングまで行うとなると、[X-Pack](https://www.elastic.co/jp/products/x-pack) や、[Yelp](https://www.yelp.com) が OSS で公開している [ElastAlert](https://github.com/Yelp/elastalert) のようなソフトウェアが必要ですが、良さそうなのがなかなかありません。それに、監視のためにいろいろツールが増えてしまっても運用が大変になってきます。

そこで、モニタリングソフトウェアを　[Prometheus](https://prometheus.io) と [Grafana](https://grafana.com) に一本化し、Solr のメトリクスとインデックスをモニタリングできるようにする [solr-exporter](https://github.com/mosuka/solr-exporter) を作ってみました。

# solr-exporter

[solr-exporter](https://github.com/mosuka/solr-exporter) を使うことで、Solr メトリクスと Solr がインデックスしているデータのモニタリングとアラーティングが可能となります。
下の図は、スタンドアローンの Solr へ接続する際のダイアグラムです。もちろん、[SolrCloud](https://lucene.apache.org/solr/guide/7_1/solrcloud.html) へ接続することも可能です。

![sample](https://github.com/mosuka/solr-exporter/raw/master/solr-exporter-diagram.png "sample")

[solr-exporter](https://github.com/mosuka/solr-exporter) は Java で開発されています。
Java を採用した理由は、Solr プロジェクトが提供する [SolrJ](https://lucene.apache.org/solr/guide/7_1/using-solrj.html) (Solr クライアントライブラリ) を使用するためです。
Solr は REST ライクなインターフェースを備えていて、HTTP でアクセスできるのですが、フェールオーバーなどを考慮すると実装が大変です。
[SolrJ](https://lucene.apache.org/solr/guide/7_1/using-solrj.html) は スタンドアローンの Solr への接続だけでなく、[SolrCloud](https://lucene.apache.org/solr/guide/7_1/solrcloud.html) を使用したクラスターの場合、[ZooKeeper](https://zookeeper.apache.org) から Node Discovery を行なって、稼働中の Solr へ接続してくれます。
また、Solr に同梱されるクライアントライブラリなので、メンテンスも十分にされていて安心して使えます。

Solr に接続した [solr-exporter](https://github.com/mosuka/solr-exporter) は [Solr のモニタリングに使える API](https://qiita.com/mosuka/private/c2da5302ab002f63935b) で紹介した [Metrics API](https://lucene.apache.org/solr/guide/7_1/metrics-reporting.html#metrics-api) や、Solr に対して全文検索を行い、それらのレスポンスを [Prometheus](https://prometheus.io) のメトリクスとして公開 (expose) します。

設定は YAML フォーマットのファイルで管理されます。
返却される API の JSON レスポンスのスクレイピングには [jq](https://stedolan.github.io/jq/) コマンドのクエリーを採用しています。

## 開発した経緯

[solr-exporter](https://github.com/mosuka/solr-exporter) を開発するに至った経緯は次のようになります。

- **検索エンジンとして Solr の運用ノウハウを持っている・監視システムとして [Prometheus](https://prometheus.io) に統一・集約したい**
    - 想定する実行環境が [Kubernetes](https://kubernetes.io) で、[Prometheus](https://prometheus.io) は [Kubernetes](https://kubernetes.io) との相性もよさそう
    - [Prometheus](https://prometheus.io) は、元 Google のエンジニアが Google 社内監視システムの Borgmon にインスパイアされて開発したもので筋がよさそう
    - 監視システムとして、Solr と同じ [Lucene](https://lucene.apache.org) をベースにした検索エンジンである [Elasticsearch](https://www.elastic.co/jp/products/elasticsearch) や、可視化・モニタリングのために [Kibana](https://www.elastic.co/jp/products/kibana)、[X-Pack](https://www.elastic.co/jp/products/x-pack) を導入して運用コストを上げたくない
- **Solr でインデックスしたドキュメント (データ) のモニタリングをしたい**
    - 「このキーワードにマッチするドキュメントがどれくらいある」などをモニタリングしたい
    - Solr の全文検索クエリーを実行したい
    - [Banana](https://github.com/lucidworks/banana) や [Zeppelin](https://zeppelin.apache.org) では可視化はできても、アラーティングまでできない
    - [ElastAlert](https://github.com/Yelp/elastalert) のような、Solr 向けのアラーティングのソフトウェアが欲しい
- メトリクスのような時系列データを [Lucene](https://lucene.apache.org) の転置インデックスで扱うのに抵抗がある
    - 時系列データには時系列データベースを使用した方がいいのではないか
- **Solr の Exporter 作ったら、上記の課題を解決できる**

というところから、「無いなら作ったらいいじゃない」ということで個人的に作ってみました。

## インストール

[solr-exporter](https://github.com/mosuka/solr-exporter) の[リポジトリのリリースページ](https://github.com/mosuka/solr-exporter/releases) から コンパイル済みのアーカイブをダウンロードします。
適当なディレクトリで解答してインストールは完了です。

```
$ cd ~/
$ unzip solr-exporter-0.3.8-bin.zip
$ cd solr-exporter-0.3.8
```

## 実行

インストールしたディレクトリ以下の `./bin/solr-exporter` を実行してください。

```
$ ./bin/solr-exporter -p 9983 -b http://localhost:8983/solr -f ./conf/config.yml
```

Windows 環境の人は、同じディレクトリの `.\bin\solr-exporter.bat` を使用します。

```
> .\bin\solr-exporter.bat -p 9983 -b http://localhost:8983/solr -f .\conf\config.yml
```

SolrCloud モードでクラスタを組んでいる場合は、次のように ZooKeeper の接続文字列を指定します。

```
$ ./bin/solr-exporter -p 9983 -z localhost:2181/solr -f ./conf/config.yml
```

他のコマンドラインオプションについてはヘルプを確認してください。

```
$ ./bin/solr-exporter -h
usage: SolrCollector [-h] [-v] [-p PORT] [-b BASE_URL] [-z ZK_HOST] [-f CONFIG]
                     [-n NUM_THREADS]

Prometheus exporter for Apache Solr.

optional arguments:
  -h, --help             show this help message and exit
  -v, --version          show version
  -p PORT, --port PORT   solr-exporter listen port
  -b BASE_URL, --baseurl BASE_URL
                         specify Solr base URL when connecting  to Solr in standalone mode (for
                         example 'http://localhost:8983/solr')
  -z ZK_HOST, --zkhost ZK_HOST
                         specify  ZooKeeper  connection  string  when  connecting  to  Solr  in
                         SolrCloud mode (for example 'localhost:2181/solr')
  -f CONFIG, --config-file CONFIG
                         specify configuration file
  -n NUM_THREADS, --num-thread NUM_THREADS
                         specify number of threads
```

## 設定ファイル例

solr-exporter の設定ファイルは次のような YAML ファイルになっています。

`query` で Solr へのリクエストを設定し、`jsonQueries` でそのレスポンスをパースする jq クエリーを設定します。
これは、Solr の `solrconfig.xml` でリクエストハンドラのパスをユーザーが自由に変更できることに対応するのと、ユーザーが自由にメトリクス名、ラベル名を変更できるようにするためです。
メトリクス名などのルールについては Prometheus の [Metric and label naming](https://prometheus.io/docs/practices/naming/) にガイドラインがありますので参考にしてみてください。

```yaml
ping:
  query:
    path: /admin/ping
  jsonQueries:
    - |-
      . as $object | $object |
      (if $object.status == "OK" then 1.0 else 0.0 end) as $value |
      {
        name         : "solr_ping",
        type         : "GAUGE",
        help         : "See following URL: http://lucene.apache.org/solr/guide/7_1/ping.html",
        label_names  : [],
        label_values : [],
        value        : $value
      }

metrics:
  query:
    path: /admin/metrics
    params:
      - group: 'all'
      - type: 'all'
      - prefix: ''
      - property: ''
  jsonQueries:
    ##############################
    # jetty
    ##############################
    # solr_metrics_jetty_response_total
    - |-
      .metrics["solr.jetty"] | to_entries | .[] | select(.key | startswith("org.eclipse.jetty.server.handler.DefaultHandler")) | select(.key | endswith("xx-responses")) as $object |
      $object.key | split(".") | last | split("-") | first as $status |
      $object.value.count as $value |
      {
        name         : "solr_metrics_jetty_response_total",
        type         : "counter",
        help         : "See following URL: https://lucene.apache.org/solr/guide/7_1/metrics-reporting.html",
        label_names  : ["status"],
        label_values : [$status],
        value        : $value
      }

... [中略] ...

collections:
  query:
    path: /admin/collections
    params:
      - action: 'CLUSTERSTATUS'
  jsonQueries:
    # solr_collections_live_nodes
    - |-
      .cluster.live_nodes | length as $value|
      {
        name         : "solr_collections_live_nodes",
        type         : "gauge",
        help         : "See following URL: https://lucene.apache.org/solr/guide/7_1/collections-api.html#clusterstatus",
        label_names  : [],
        label_values : [],
        value        : $value
      }

... [中略] ...

queries:
  - query:
      collection: collection1
      path: /select
      params:
        - q: "*:*"
        - start: 0
        - rows: 0
        - json.facet: |-
            {
              category: {
                type: terms,
                field: cat
              }
            }
    jsonQueries:
      # solr_facets_category
      - |-
        .facets.category.buckets[] as $object |
        $object.val as $term |
        $object.count as $value |
        {
          name         : "solr_facets_category",
          type         : "gauge",
          help         : "Category facets",
          label_names  : ["collection", "term"],
          label_values : ["collection1", $term],
          value        : $value
        }
```

solr-exporter では、`ping`、`metrics`、`collections`、`queries` の設定を持っています。

- ping - Solr の [Ping](https://lucene.apache.org/solr/guide/7_1/ping.html) を利用し、コア、コレクションが利用可能かのメトリクスを取得
- metrics - Solr の [Metrics API](https://lucene.apache.org/solr/guide/7_1/metrics-reporting.html#metrics-api) を利用して、メトリクスを取得
- collections - Solr の [Collections API](https://lucene.apache.org/solr/guide/7_1/collections-api.html) を利用して、クラスターの情報をメトリクスとして取得
- queries - Solr の[検索](http://lucene.apache.org/solr/guide/7_1/searching.html)を利用して、検索結果をメトリクスとして取得

Solr の返す JSON レスポンスを、次のフォーマットでメトリクスとして JSON を組み立てる [jq](https://stedolan.github.io/jq/) のクエリー設定すると、Prometheus 形式のデータに変換して公開 (expose) します。

```json
{
  "name": "some_metric_name",
  "type": "gauge", 
  "help": "describe metric.",
  "label_names": ["label_name1", "label_name2"],
  "label_values": ["label_value1", "label_value2"],
  "value": 1.0
}
```

例えば、上記 JSON は、下記のような Prometheus フォーマットに変換されます。

```text
# HELP some_metric_name describe metric.
# TYPE some_metric_name gauge
some_metric_name{label_name1 ="label_value1", label_name2 ="label_value2",} 1.0
```

solr-exporter を実行したら、Solr のメトリクスが expose できることを、次の URL へアクセスして確認してください。

```text
$ curl -s 'http://localhost:9983/metrics'
# HELP solr_metrics_jetty_response_total See following URL: https://lucene.apache.org/solr/guide/7_1/metrics-reporting.html
# TYPE solr_metrics_jetty_response_total counter
solr_metrics_jetty_response_total{base_url="http://localhost:8983/solr",status="1xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8983/solr",status="2xx",} 516.0
solr_metrics_jetty_response_total{base_url="http://localhost:8983/solr",status="3xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8983/solr",status="4xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8983/solr",status="5xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8984/solr",status="1xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8984/solr",status="2xx",} 517.0
solr_metrics_jetty_response_total{base_url="http://localhost:8984/solr",status="3xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8984/solr",status="4xx",} 0.0
solr_metrics_jetty_response_total{base_url="http://localhost:8984/solr",status="5xx",} 0.0

... [中略] ...

# TYPE solr_collections_shard_leader gauge
solr_collections_shard_leader{zk_host="localhost:2181/solr",collection="banana-int",shard="shard1",replica="core_node3",core="banana-int_shard1_replica_n1",base_url="http://localhost:8983/solr",node_name="localhost:8983_solr",type="NRT",} 0.0
solr_collections_shard_leader{zk_host="localhost:2181/solr",collection="banana-int",shard="shard1",replica="core_node4",core="banana-int_shard1_replica_n2",base_url="http://localhost:8984/solr",node_name="localhost:8984_solr",type="NRT",} 1.0
solr_collections_shard_leader{zk_host="localhost:2181/solr",collection="apache-log",shard="shard1",replica="core_node3",core="apache-log_shard1_replica_n1",base_url="http://localhost:8983/solr",node_name="localhost:8983_solr",type="NRT",} 0.0
solr_collections_shard_leader{zk_host="localhost:2181/solr",collection="apache-log",shard="shard1",replica="core_node4",core="apache-log_shard1_replica_n2",base_url="http://localhost:8984/solr",node_name="localhost:8984_solr",type="NRT",} 1.0
solr_collections_shard_leader{zk_host="localhost:2181/solr",collection="solr-log",shard="shard1",replica="core_node3",core="solr-log_shard1_replica_n1",base_url="http://localhost:8983/solr",node_name="localhost:8983_solr",type="NRT",} 0.0
solr_collections_shard_leader{zk_host="localhost:2181/solr",collection="solr-log",shard="shard1",replica="core_node4",core="solr-log_shard1_replica_n2",base_url="http://localhost:8984/solr",node_name="localhost:8984_solr",type="NRT",} 1.0
# HELP solr_scrape_duration_seconds Time this Solr scrape took, in seconds.
# TYPE solr_scrape_duration_seconds gauge
solr_scrape_duration_seconds 1.584429789
```

上記のようにメトリクスを出力できていれば solr-exporter の準備は完了です。

デフォルトで、主要なメトリクスや、Solr の検索クエリーのサンプルが設定されています。
不要なメトリクスや、別のメトリクス、検索結果を取得したい場合は適宜修正をしてください。

# Prometheus

ここから solr-exporter の公開するメトリクスを Pull してくるため、Prometheus の準備を行います。

## Prometheus のインストールと設定

Prometheus のインストールについては、[Installation | Prometheus](https://prometheus.io/docs/prometheus/latest/installation/ を参考にして、環境に合わせて行なってください。

インストール後、`prometheus.yaml` の `scrape_configs` に solr-exporter の情報を追加します。

```
$ vi ./prometheus.yaml
```

```yaml
# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).

... [中略] ...

# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'solr'
    static_configs:
      - targets: ['localhost:9983']
```

以上で、solr-exporter からメトリクスを Pull することができます。

## Promethues の実行

次のコマンドで Prometheus を起動します。

```
$ ./prometheus --config.file ./prometheus.yml
```

Prometheus を起動すると、次の URL で Prometheus の UI にアクセスできます。

```
http://localhost:9090/graph
```

Status メニューから Targets をクリックすると、Prometheus が収集しているターゲットの一覧を確認できます。
次のように、先ほど登録した solr-exporter がターゲットに存在することを確認してください。一覧にない場合、または、エラーが発生する場合は、設定ファイルの内容が間違っていないか確認してください。

![Screen Shot 2017-12-01 at 17.52.23.png](https://qiita-image-store.s3.amazonaws.com/0/58291/82f67c85-5fb1-ed34-c312-e415f93ca82f.png)

Prometheus が収集したメトリクスは、次のようにグラフで可視化できます。

![Screen Shot 2017-12-01 at 17.54.08.png](https://qiita-image-store.s3.amazonaws.com/0/58291/33f71c93-3292-96ac-6639-8f1903d8c8d8.png)

が、やっぱりちょっと寂しいです。Grafana のサンプルダッシュボードを用意しているのでそちらを表示してみます。

# Grafana

ここから Prometheus に保存されたメトリクスをより高度に可視化するため、Grafana の準備を行います。

## Grafana のインストール

Grafana のインストールについては、[Installing Grafana](http://docs.grafana.org/installation/) を参考にして、環境に合わせて行なってください。

## Grafana ダッシュボード

Grafana を起動すると、次の URL で UI にアクセスできます。

```
http://localhost:3000
```

<img width="1310" alt="Screen Shot 2017-12-03 at 23.11.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/58291/615c1837-8eb6-5e76-e90f-0e9aebf7f1b0.png">

初期の管理者アカウントは次のようになります。

User : admin
Password : admin


今回、サンプルのダッシュボードを用意しました。次の URL で公開しています。
https://grafana.com/dashboards/3888

サンプルのダッシュボードは、次のようにしてインポートできます。
`Dashboards` → `Import` で `Import Dashboard` ダイアログを表示します。

<img width="383" alt="Screen Shot 2017-12-03 at 23.14.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/58291/709134bc-ce7c-2b5f-5ad0-4914d951d9d8.png">

`Grafana.com Dashboard` に `3888` (サンプルのダシュボードの ID) を入力してください。

<img width="801" alt="Screen Shot 2017-12-03 at 23.17.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/58291/8c92acb6-b2ad-14c5-b87d-ebcc7d169022.png">

インポートに成功すると、次のようなダッシュボードが表示できます。

![solr-dashboard.png](https://qiita-image-store.s3.amazonaws.com/0/58291/3bc3a00b-5912-1bbb-acd8-7f37bfbdc9e8.png)


サンプルのダッシュボードなので、自由に編集して、好みのダッシュボードを作成してください。

# まとめ

Solr の [Metrics API](https://lucene.apache.org/solr/guide/7_1/metrics-reporting.html#metrics-api) の API のレスポンスに一貫性がなく、機械的に一発で Prometheus フォーマットへ変換することが困難でしたが、jq のクエリを使うことで、不揃いなフォーマットを統一したフォーマットに直すことができるようになりました。

Solr は、全文検索エンジンとして豊富な機能を提供し、稼働実績もあるソフトウェアですが、モニタリングやアラーティングについてはエコシステムが整っていないため、弱い部分です。
solr-exporter は、そんな Solr の弱点を、モニタリングやアラーティングで強力な機能を提供する Prometheus を繋ぐソフトウェアです。

実験的な試みなので、不具合もあるかもしれませんが、興味あれば使ってみてください。
また、もっとこうした方がいいなどあれば、ぜひ Pull Request ください。

Solr コミュニティでも Metrics API のレスポンスを Prometheus フォーマットで公開 (expose) する [[SOLR-10654] Expose Metrics in Prometheus format](https://issues.apache.org/jira/browse/SOLR-10654) という Issue も上がっています。
また、[OpenMetrics](https://github.com/RichiH/OpenMetrics) という Prometheus のフォーマットをベースに、メトリクスのフォーマットの標準化をしようという試みもされています。
[Prometheus Advent Calendar 2017](https://qiita.com/advent-calendar/2017/prometheus) もあります。Prometheus にも興味がでてきた方は、こちらもご覧ください。

![TH160_9784774189307.jpg](https://qiita-image-store.s3.amazonaws.com/0/58291/b6d09ffb-aea6-d4cb-988b-f9d7994d3bfe.jpeg)

Solr のインデックスしたデータを抽出するには Facet や Stats API、検索クエリーを利用することになると思います。[［改訂第3版］Apache Solr入門](http://gihyo.jp/book/2017/978-4-7741-8930-7) に検索クエリーの書き方や各種 API が解説されているので、興味ある方は是非読んでみてください。