petitviolet_blog

@petitviolet blog

HTTPリクエスト/レスポンスを記録してリプレイするためのruby gem作った

RubyGem作ってみよう、Rackミドルウェア書いてみよう、の習作。

みたいな使い方。

実用上は、Goで書かれたgoreplayなど、もっと便利なツール群がある。 名前はこれをオマージュしてつけた。

どんなものか

使い方は大体こんな雰囲気でconfig.ruとかに書く。

use Rack::Rreplay.Middleware(directory: './tmp', format: :json),
    { sample: 5, extra_header_keys: %w[X-ACCESS-TOKEN], debug: true }

HTTPリクエスト/レスポンスのdumpを吐き出すdirectoryとformat(:msgpack or :json)を与えつつ、オプションをいくつか渡せるようになっている。 この設定であればHTTPリクエスト5回につき1回dumpするようになっている。

出力結果はJSONなら例えばこんな感じ。

{
  "request": {
    "body": null,
    "headers": {
      "X-ACCESS-TOKEN": "eyJhY2Nlc3MtdG9rZW4iOiJGdkNyczdnT0pLYUN3SU5lTWNmMjVnIiwiY2xp%0AZW50IjoiZnlsc01qMHlfYTl3cHVpWmJiWUJodyIsInVpZCI6ImFsaWNlQGV4%0AYW1wbGUuY29tIn0%3D%0A",
      "content-type": "application/json",
      "cookie": null,
      "user-agent": "petitviolet curl"
    },
    "method": "GET",
    "path": "/api/whoami",
    "query_strings": ""
  },
  "response": {
    "body": "{\"id\":\"f7fb4b22-1792-477f-ab28-cd36491fd09f\",\"name\":\"alice\"}",
    "headers": {
      "Cache-Control": "max-age=0, private, must-revalidate",
      "Content-Type": "application/json; charset=utf-8",
      "X-Access-Token": "...",
      ...
    },
    "status": 200
  },
  "response_time": "0.395582",
  "time": "2019-12-30T23:43:41+09:00",
  "uuid": "c80468a9-5424-4a17-a1bc-6ab325252d36"
}

このログを使ってHTTPリクエストをリプレイするとこんな感じになる

$ bundle exec rreplay 'http://localhost:3000' ./tmp/rreplay.log.json -f json --verbose | jq -S .
{
  "response": {
    "actual": {
      "response": {
        "body": "{\"id\":\"f7fb4b22-1792-477f-ab28-cd36491fd09f\",\"name\":\"alice\"}",
        "headers": {
          "Cache-Control": "max-age=0, private, must-revalidate",
          "Content-Type": "application/json; charset=utf-8",
          "X-Access-Token": "...",
          ...
        },
        "status": "200"
      },
      "response_time": "0.029326"
    },
    "recorded": {
      "record": {
        "body": "{\"id\":\"f7fb4b22-1792-477f-ab28-cd36491fd09f\",\"name\":\"alice\"}",
        "headers": {
          "Cache-Control": "max-age=0, private, must-revalidate",
          "Content-Type": "application/json; charset=utf-8",
          "X-Access-Token": "...",
          ...
        },
        "status": 200
      },
      "response_time": "0.395582"
    }
  },
  "time": "2020-01-02T22:35:47+09:00",
  "uuid": "c80468a9-5424-4a17-a1bc-6ab325252d36"
}

このように、あるリクエストをもう一度投げた時にデグレしてないかとかレスポンスタイムに大きな差がないか、というのをいい感じに頑張ればテストや性能検証とかに使えないかな〜とは思っているが何も実装はしていない。

実装について

Rackミドルウェアという都合上、initialize(app, ...)call(env)が定義されたクラスであることが求められる。 HTTPのリクエストとレスポンスを吐き出していくためファイルのローテーションをやりたいというモチベーションが湧いてきて、initializeするたびにLoggerをnewするのは流石に避けたかったのでClass.newミドルウェアを作ることにした。

use Rack::Rreplay.Middleware(directory: './tmp', format: :json),
    { sample: 1, extra_header_keys: %w[X-ACCESS-TOKEN], debug: true }

このようにconfig.ru等に書くとミドルウェアが有効になるが、Rack::Rreplay.MiddlewareはClassオブジェクトを返す関数となっていて実装はこのあたりrreplay/rreplay.rb

関数の引数に与えたディレクトリ名を使ってログローテーションするLogger(正確にはLogger::LogDevice)のインスタンスをnewして使い回すためにClass.newで作ったクラスのクラス変数にloggerを埋め込むというやり方をしている。 クラス変数、というだけでなるべく使いたくない気持ちにはなるので別の方法があればよかったが思いつかなかった。

また、JSONとMessagePackの2種類のフォーマットをサポートしてみている。JSONをMessagePackにして少し圧縮しているだけだが。 MessagePackはUTF-8なStringではないのでbinaryとしてファイルに書き込む必要があり、内部的に使用しているLogger::LogDeviceのコンストラクタにbinmode: trueを渡すことで動くようになる。

Loggerを使い回すという観点だとClass.newではなくLoggerをユーザ側から差し込んでもらう方がシンプルにはなるが、こういったフラグ管理などを考えるとこれで良かったのかもしれない。(一応差し込めるようにはなっている)