【Rails】sqlcommenterのデモアプリを試してみた

先日2021/1/29にGoogle Cloudのブログでリリースが発表されたSqlcommenterのRuby on Railsデモアプリを触ってみたので、試した手順と感想を書いてみたいと思います。

cloud.google.com

sqlcommenterとは

sqlcommenter の文章を引用すると

sqlcommenter is a suite of middlewares/plugins that enable your ORMs to augment SQL statements before execution, with comments containing information about the code that caused its execution. This helps in easily correlating slow performance with source code and giving insights into backend database performance.

sqlcommenterはミドルウェア/プラグインのスイートで、ORMがSQL文を実行する前に、実行の原因となったコードに関する情報を含むコメントを追加することを可能にします。これにより、遅いパフォーマンスをソースコードと容易に関連付けることができ、バックエンドのデータベースのパフォーマンスについての洞察を得ることができます。

というプロジェクトのようです。

Railsアプリケーションの場合、ActiveRecord::Modelを使ってSQLクエリを実行された際、アプリケーションログからSQLクエリ(SELECT文等)を確認することはできますが、それがどこのコントローラのどこのアクションのコードから実行されたか等の情報は分かりませんでした。 sqlcommenter を使用することでSQLクエリにコメントとして情報を付加することができます。

Rails以外にも、djangoやFlask、Spring等様々な言語のフレームワークに対応しています。(参考)

sqlcommenterがサポートするDB

FAQページによると、2021年2月4日時点でサポート対象のデータベースは

の5つのようです。

デモアプリの概要

デモアプリは以下に公開されています。 github.com

これはRuby on RailsAPIアプリケーションで、以下のAPIが2つあるだけのシンプルなアプリケーションです。

  • GET /posts
    • postsテーブルの一覧を返す
  • POST /posts
    • postsテーブルにレコードを追加する

データベースは sqlite3 を使用していて、1つのテーブルのみ( posts テーブル)使用します。

コントローラとroutesは以下のようになっています。

class PostsController < ApplicationController
  def index
    render json: Post.all
  end

  def create
    title = params[:title].to_s.strip
    head :bad_request if title.empty?
    render json: Post.create!(title: title)
  end
end
Rails.application.routes.draw do
 resources :posts, only: %i[index create]
end

セットアップ

以下のデモプロジェクトのREADMEに記載されている手順を実行しています。

まずは、sqlcommenter をgit cloneし、デモアプリに移動します。

$ git clone git@github.com:google/sqlcommenter.git
$ cd sqlcommenter/
$ cd ruby/sqlcommenter-ruby/sqlcommenter_rails_demo/

次にmarginalia をgit cloneし、ローカルでformatting ブランチをチェックアウトします。

$ git clone git@github.com:glebm/marginalia.git ../marginalia
$ git -C ../marginalia checkout formatting

READMEを読むと、marginalia は以下のようにSQLクエリにコメントを追加できるGemのようです。また marginaliabasecamp のプロジェクトですが、未マージのPRがマージされるまでは上記の fork したコードを使用する必要があるようです。

Account Load (0.3ms)  SELECT `accounts`.* FROM `accounts` 
WHERE `accounts`.`queenbee_id` = 1234567890 
LIMIT 1 
/*application:BCX,controller:project_imports,action:show*/

デモアプリでは使用するgemはGemfileに記載されています。どうやら現時点(2021/2/4時点)では rubygemsにリリースされていない ようなので、以下のようにソースコードのパスを指定するようです。

gem 'sqlcommenter_rails', path: '../sqlcommenter_rails'
gem 'marginalia', path: '../marginalia'
gem 'marginalia-opencensus', path: '../marginalia-opencensus'

bin/setupを実行し、Gemのインストールとテーブル作成をします。

$ bin/setup
== Installing dependencies ==
〜〜〜
The Gemfiles dependencies are satisfied

== Preparing database ==
== 20190608153219 CreatePosts: migrating ===============
-- create_table(:posts)
   -> 0.0021s
== 20190608153219 CreatePosts: migrated (0.0022s) =======


== Removing old logs and tempfiles ==

== Restarting application server ==

アプリケーションの起動

rails serverを起動してみます。

$ bin/rails s

デモアプリには以下の2つのエンドポイントがあるようなので、それぞれcurlでリクエストを送ってみます。

  • GET /posts
  • POST /posts

GET /posts

curl/posts にGETリクエストを送ってみます。

ログに出力されるSQLクエリに、コメントで情報が付加されどのコントローラー、アクションで実行されているか把握できます。

$ curl localhost:3000/posts

Post Load (0.3ms)  SELECT "posts".* FROM "posts"
/*action='index' application='SqlcommenterRailsDemo', controller='posts', db_driver='ActiveRecord::ConnectionAdapters::SQLite3Adapter', framework='rails_v6.0.3.4', route='/posts', traceparent='00-5fc1dcbb8e80456572df7bfc5ef35ad5-a0b5c78c11c98239-01'*/

POST /posts

$ curl -X POST localhost:3000/posts -d 'title=my-post'

INSERT INTO "posts" ("title", "created_at", "updated_at") VALUES (?, ?, ?)
/*action='create', application='SqlcommenterRailsDemo', controller='posts', db_driver='ActiveRecord::ConnectionAdapters::SQLite3Adapter', framework='rails_v6.0.3.4', route='/posts' traceparent='00-e89035e34eb8c73d809ef10b89f25eff-7d8ae48f42e75014-01'*/

コメントに追加する情報

sqlcommenterのページによると、デフォルトでコメントに追加される情報は

  • action (アクション名)
  • application (アプリケーション名)
  • controller (コントローラー名)
  • db_driver (DBドライバ)
  • framework (フレームワーク)
  • routes (ルーツ)

の6つですが、イニシャライザーを追加することで情報を変更できます。

# アクション、アプリケーション名、ネームスペース付きコントローラ名、ホスト名、ジョブ名、ファイル及びライン行数を出力
Marginalia::Comment.components = [ :action, :application, :controller_with_namespace, :hostname, :job, :line]

デモアプリのPOST /posts で試した場合、出力結果が以下のように変わります。

INSERT INTO "posts" ("title", "created_at", "updated_at") VALUES (?, ?, ?) 
/*action='create',application='SqlcommenterRailsDemo',controller_with_namespace='PostsController',hostname='takashi',line='/app/controllers/posts_controller.rb:25:in `create\''*/

Opencensusの対応状況

今回試してはいませんが、READMEによるとデモアプリで使われている sqlcommenter_rails Gemは、デフォルトの設定で OpenCensus traceをMarginaliaコメントの最後につけてくれるので、任意のExporterを使って分析バックエンドに連携できます。

具体的に上記のSQLクエリの実行結果では traceparent='00-5fc1dcbb8e80456572df7bfc5ef35ad5-a0b5c78c11c98239-01' の部分が OpenCensus trace に該当しそうです。

RubyにおけるOpencensusの対応状況(2021年2月現在)に関しては、Pre-Alpha ということなので、これからの発展に期待したいです。

使ってみた感想

クエリを実行しているコードの場所を特定するには良さそうと思いました。規模が大きめのプロジェクトに関しては Datadog などが既に導入されていてスロークエリを追える状況になっているかと思いますので、そういったプロジェクトには追加では入れる必要はなさそうな気がします。

また、実際にスロークエリを修正する際には、例えばMySQLの場合、キャッシュがない状態でのパフォーマンスをNO_SQL_CACHEつけたクエリをSQLクライアントから実行してみたり、EXPLAINで実行計画を見てみて、スロークエリの対応を決めるという流れになりそうです。

Railsプロジェクトであれば、bulletを使うことでN+1を回避することができるので、このような他のスロークエリ対策と併せて使うと良さそうです。

参考資料