Rubyで作るfinagle-thriftクライント

[FOLIOのアドベントカレンダー](https://adventar.org/calendars/3424)9日目です。
前日は[@ken5scal](https://twitter.com/ken5scal)による[AzureADでYubiKeyを管理しつつ認証(TOTP)する](https://ken5scal.hatenablog.com/entry/2018/12/08/AureAD%E3%81%A7YubiKey%E3%82%92%E7%AE%A1%E7%90%86%E3%81%97%E3%81%A4%E3%81%A4%E8%AA%8D%E8%A8%BC%28TOTP%29%E3%81%99%E3%82%8B_%23azureAD_%23yubikey)記事でした。
手元の下書きに「前日分の感想を書く」って書いてありましたが、感想が書きづらいタイプのhow-to記事でした。

本日は **Rubyで作るfinagle-thriftクライント** と銘打ち、小ネタを書きます。感想が書きづらいタイプのhow-to記事です。

# TL;DR
- FOLIOではPORTフロントエンドと呼ばれる管理画面的なものを作ってる
- バックエンドは大体 `finagle-thrift` を喋る
- PORTフロントエンドも `finagle-thrift` 喋れるようにして分散トレーシングできるようにした
- Rubyで `finagle-thrift` やってるドキュメントどこにもなくて苦労したので本稿で供養

# FOLIOの構成
![PORTフロントエンド含む構成図](https://qiita-image-store.s3.amazonaws.com/0/61546/ecf73d0c-67c5-da05-b772-54f2398126c0.png)

FOLIO ( https://folio-sec.com ) は、モバイルアプリ, Webサービスで利用可能なオンライン証券サービスを作っています。
バックエンドはほぼScalaによるマイクロサービスでできていて、RPCには `finagle-thrift` を利用しています（詳細後述）。

BtoCサービスによくあることですが、裏側にはいわゆる「管理画面」が存在しています。
FOLIOでは、証券サービスを支える裏方部分を **PORT** と呼んでいます。
（参考: [株式会社FOLIOの次世代証券システムをひもとく](http://itohiro73.hatenablog.com/entry/2017/12/25/081835) )

PORTのフロントエンドをRailsで作成しています。
サービスリリース当初からPORTフロントエンドはバックエンドシステムに対しRead/WriteのThrift通信を行っており、

- 商品として配信するテーマの入稿
- 口座開設の審査

などの機能を提供していました。

しかしPORTフロントエンドは分散トレーシングのための仕組みを導入していなかったので、PORTフロントエンドの利用者がトラブルを報告してきても、どのバックエンドマイクロサービスによる事象かを調査する際、日時によるログの絞り込みを行う必要がありました（低精度）。

流石につらくなってきたので、BtoCのサービス側と同じく、分散トレーシングに対応するためにRailsでも `finagle-thrift` ライブラリを使うようにしました。
その際、自分の調べた範囲ではRubyで `finagle-thrift` を導入する方法が見つからなかったので、色々と苦労しました。
もう二度とこんな苦労をする人が現れないように、記録を残しておくのが本稿の趣旨です。


# finagle-thrift の解説
`finagle-thrift` の前に[Finagle](https://twitter.github.io/finagle/guide/index.html)の解説をします。

FinagleはTwitter社が中心に作成しているOSSのRPCフレームワークです。
特にサーバ側のパフォーマンスを追求しており、多くのリクエストを並行に捌くことができるように設計されています。

Finagleでは通信プロトコルとして、HTTP, Thrift, MySQLが使えます。
本稿ではこのうちThriftを通信プロトコルとして使うためのライブラリ、 `finagle-thrift` に関して記載します。
Thriftは、始めはFacebook社によって開発された、RPCライブラリです。
今は[Apache Thrift](http://thrift.apache.org/)としてOSSで公開されています。
Twitter社はThriftを拡張し、自社のService-Oriented Architechtureで利用するために、Zipkinによる分散トレーシングのための情報をパケットに載せられるようにしました。
この拡張プロトコルをTTwitterと呼びます。

以下、Rubyのクライアントから、 `finagle-thrift` ライブラリを使ってTTwitterプロトコルで通信するための方法を記載します。

# Rubyで `finagle-thrift` を使って通信する最小コード
このセクションで解説するコードは https://github.com/laysakura/ruby-finagle-thrift-client-example に置いてあります。
実行方法も書いているので、そちらも参考にしてください。

まず、サーバサイドと共有するThriftファイルはこちらです。

```thrift:hello_service.thrift
namespace java com.laysakura
namespace rb Laysakura.Idl

service HelloService {
  string sayHelloTo(1: string name)
}
```

`sayHelloTo()` APIがひとつ定義されただけのシンプルなものです。

Thriftファイルを以下のコマンドでコンパイルすると、 `gen-rb` ディレクトリ以下にRubyのファイルが生成されます。

```bash:Thriftファイルのコンパイル
$ brew install thrift

$ thrift --version
Thrift version 0.11.0

$ thrift --gen rb hello_service.thrift

$ ls gen-rb
hello_service.rb  hello_service_constants.rb  hello_service_types.rb
```

これでThriftのAPI定義に合わせてRPCする準備はOKです。
あとは下記のGemfileを:

```ruby:Gemfile
source 'https://rubygems.org'

gem 'finagle-thrift'
gem 'thrift_client'
```

使ってライブラリをインストールし、下記の `run.sh` を実行すれば `localhost:9999` に立っているTTwitterを喋るサーバへのRPCコールができます。

```bash
$ bundle install --path=vendor/bundle
$ bundle exec ruby run.sh  # localhost:9999 に sayHelloTo() APIを生やしたサーバが立っていること前提
```

```ruby:run.rb
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'gen-rb'))
require 'thrift_client'
require 'finagle-thrift'
require 'hello_service'

SERVER = 'localhost:9999'
CLIENT_NAME = 'ruby-finagle-thrift-client-example'

# HelloServiceのクライアント作成
client = ::ThriftClient.new(::Laysakura::Idl::HelloService::Client, SERVER)

# Tracingの有効化
#
## https://zipkin.io/pages/instrumenting.html の "Endpoint" を設定。
##
## このように設定しないと、 https://github.com/twitter/finagle/blob/version-6.40.0/finagle-thrift/src/main/ruby/lib/finagle-thrift/trace.rb#L145 のように
## gethostname(3) の結果を名前解決しようとするが、名前解決できない名前が取得されるとThrift API callが例外で失敗する。
::Trace.default_endpoint = ::Trace::Endpoint.new(::Trace::Endpoint.host_to_i32('127.0.0.1'), 0, CLIENT_NAME)
::FinagleThrift.enable_tracing!(client, ::FinagleThrift::ClientId.new(name: CLIENT_NAME))

# HelloServiceのサーバ側の sayHelloTo() APIを叩く
begin
  resp = client.sayHelloTo('John')
ensure
  client.disconnect!
end

puts "Response: '#{resp}'"
puts "TraceId: #{::Trace.id.trace_id.to_s}"
```

`::Trace.default_endpoint = ...` の行に注意してください。
自分の試している限りでは、この行がなければ `::FinagleThrift.enable_tracing!(...)` が失敗し、Zipkin用のトレース情報を出すことができませんでした。

# RailsでTraceIdをロギングする
おまけ的な扱いです。

先に上げたPORTのフロントエンド（管理画面）はRailsで作っています。
PORTフロントエンドのRailsで生成したTraceIdをバックエンドのマイクロサービスに伝達しつつ、PORTフロントエンドのログにもバックエンドのログにもTraceIdを出力するようにしています。
ログはKibanaに転送されるので、Kibana上で `traceId:9549be5ecbfacc56` のように検索すると、PORTフロントエンドへのリクエストを完遂させるために叩いたバックエンドマイクロサービスのログも一気通貫して見ることができます。

`Rails.logger.info()` などのロギングメソッドを叩くときに、「このRailsへのリクエストで生成したTraceId」を取得することが必要でしたが、調べても分からなかった、試行錯誤の結果[request_store](https://github.com/steveklabnik/request_store) Gemを使い、rack_middleware層で下記のように実現しています。

```ruby:lib/folio/rack_middleware/store_request_local_vars.rb
module Folio
  module RackMiddleware
    # loggerなどで使うため、Rackのrequestごとに固有の値を ::RequestStore に格納する
    class StoreRequestLocalVars
      def initialize(app)
        @app = app
      end

      def call(env)
        # finagle-thrift の traceId
        #
        # https://github.com/twitter/finagle/blob/version-6.40.0/finagle-thrift/src/main/ruby/lib/finagle-thrift/trace.rb#L151-L159
        # にあるように、Thread local variableを使っているので、
        # ここで取得した値が別のリクエストのtraceIdではないことが保証される。
        # (1つの thread local variable に複数のリクエストが同時に書き込むことはないという想定)
        ::RequestStore.store[:trace_id] = ::Trace.id.trace_id.to_s

        @app.call(env)
      end
    end
  end
end
```

```ruby:config/initializers/logger.rb
class JsonLogFormatter
  include ActiveSupport::TaggedLogging::Formatter

  def initialize
    @fixed_fields = {
      appname: Settings.app_name,
      appVersion: Settings.app_version,
      hostname: Socket.gethostname
    }
  end

  def call(severity, time, progname, msg)
    fields = {
      '@timestamp': time,
      level: severity,
      message: msg
    }

    # 以下、リクエストごとに変わる値。
    # loggerを呼び出しているのとは異なるリクエストの値を取らないよう、 ::RequestStore から取得すべき。
    trace_id_fields = { traceId: ::RequestStore.store[:trace_id] }

    fields.merge(
      trace_id_fields.merge(
        @fixed_fields
      )
    ).to_json.concat("\n")
  end
end
```

# おわりに
PORTフロントエンドでも分散トレーシングできるようにするため、 `finagle-thrift` をRubyから使う方法を開拓しました。
苦労を供養するため、このドキュメントを残しました。
誰も書いていなかったということは誰もRubyで `finagle-thrift` なんてやってないような気はするのですが... 誰かのお役に立てれば嬉しいです。

明日はFOLIOアドベントカレンダー10日目、[@takanoripe](https://twitter.com/takanoripe)による記事です。