dip Engineer Blog

Engineer Blog
ディップ株式会社のエンジニアによる技術ブログです。
弊社はバイトル・はたらこねっとなど様々なサービスを運営しています。

sqlxでNew Relicのドライバをラップするときの注意点

バイトルPRO開発課の岡田です。

ディップではNew Relicの導入を進めています。
バイトルPROでもNew Relicの導入を進めていますが、
バイトルPROはGo言語を使用しており、DB操作にはsqlxというライブラリを使っているのですが、 sqlxでAPM情報を取得する際に難しかった部分がありましたので書かせていただきます。

APM情報の取得方法

Goを使っている場合、New RelicでデータベースのAPM情報を取得する方法としては下記2つの方法があります。

  • New Relicが用意しているドライバを使用する
  • 独自ドライバにNew Relicが用意しているドライバをラップする

MySQLPostgreSQLなどの一部のデータベースはAPM情報を取得するためのドライバがNew Relic側で用意されています。
一方、用意されていないデータベースドライバを使用したい場合にはそのデータベースのドライバをNew Relicが用意しているドライバでラップする必要があります。今回の内容はこちらをもとに行っています。
バイトルPROではOracleに接続するためにgo-oci8というライブラリを使用しています。
ラップする方法としてはNew Relicが「Goエージェントの互換性と要件」というドキュメントのdatabase/sqlの箇所でMySQLやPostgreSQLでの実装例が書かれているので、それを参考に実装すればいいはずでした。

問題

ドキュメントを参考に実装を進めると、
下記のようなエラーが出てテストコードが通りませんでした。

sql: expected 0 arguments, got 1

該当のコードは下記のようなことをしています。
*下記は参考のための例として記載していますが、実際には存在しないコードです。

package db

import (
    "context"
    "database/sql"
    "fmt"

    "github.com/jmoiron/sqlx"
    "github.com/newrelic/go-agent/v3/newrelic"
    "github.com/newrelic/go-agent/v3/newrelic/sqlparse"

    "github.com/mattn/go-oci8"
)

var (
    baseBuilder = newrelic.SQLDriverSegmentBuilder{
        BaseSegment: newrelic.DatastoreSegment{
            Product: newrelic.DatastoreOracle,
        },
        ParseQuery: sqlparse.ParseQuery,
    }
)

type Client struct {
    DB *sqlx.DB
}

// New ...
func New() (*Client, error) {
    src := "" // 接続情報
    sql.Register("nroracle", newrelic.InstrumentSQLDriver(oci8.Driver, baseBuilder))
    db, err := sqlx.Open("nroracle", src)
    if err != nil {
        return nil, err
    }
    if err := db.Ping(); err != nil {
        return nil, err
    }
}

type User struct {
    Name string `db:"name"`
}

func (c *Client) GetName(ctx context.Context, id string) (*User, error) {
    // サンプルのクエリ
    query := `SELECT name FROM user Where id = :id`
    namedStmt, err := c.DB.PrepareNamedContext(ctx, query)
    // ここでエラーになる
    if err != nil {
        return nil, err
    }
    defer namedStmt.Close()

    params := map[string]interface{}{
        "id": id,
    }

    var row User
    if err = namedStmt.GetContext(ctx, &row, params); err != nil {
        return nil, err
    }

    return &row, nil
}

調査

この問題を解決するために調査をすすめると SQL内でplaceholderを使用していると実行できないが、placeholderを含めていないとQueryを実行することができるということがわかりました。

-- これだと実行できる
SELECT name FROM user;

-- こちらは実行できない
SELECT name FROM user WHERE id = :id

sqlx内でのバインド方法が関係しているのではないかと思い、 sqlxのバインド方法を確認すると下記のようなソース(Driver名の宣言バインドタイプの判定)が見つかりました。
どうやらドライバの名前がdefaultBindsで定義されている名前によってplaceholderがどのタイプなのかを識別しているようです。
例えば、sqlx.Open("nrpostgres", src)としていればplaceholderは$であり、sqlx.Open("mysql", src)としていればplaceholderは?になります。

Driver名の宣言

var defaultBinds = map[int][]string{
    DOLLAR:   []string{"postgres", "pgx", "pq-timeouts", "cloudsqlpostgres", "ql", "nrpostgres", "cockroach"},
    QUESTION: []string{"mysql", "sqlite3", "nrmysql", "nrsqlite3"},
    NAMED:    []string{"oci8", "ora", "goracle", "godror"},
    AT:       []string{"sqlserver"},
}

バインドタイプの判定

switch bindType {
// oracle only supports named type bind vars even for positional
case NAMED:
    rebound = append(rebound, ':')
    rebound = append(rebound, name...)
case QUESTION, UNKNOWN:
    rebound = append(rebound, '?')
case DOLLAR:
    rebound = append(rebound, '$')
    for _, b := range strconv.Itoa(currentVar) {
        rebound = append(rebound, byte(b))
    }
    currentVar++
case AT:
    rebound = append(rebound, '@', 'p')
    for _, b := range strconv.Itoa(currentVar) {
        rebound = append(rebound, byte(b))
    }
    currentVar++
}

Oracleのplaceholderの記載方法は:なのでドライバをラップする際にはdefaultBindsNAMEDに記載されている名前でdirverをOpenする必要がありましたが、私はnroracleという任意の名前にしていました。
ちなみに、New Relicが用意しているMySQLやPostgreSQLのドライバ名についてはsqlx内で定義されているので、こちらは気にしなくて問題ありません。

さらに、バイトルPROではgo-oci8というライブラリを使用していて、このドライバはinit関数内でoci8として名前でドライバ名を登録されています。
Goの標準パッケージのsqlライブラリでは同じ名前のドライバ名を登録することができないので、New Relicのドライバをラップする際にoci8という名前のドライバ名は使用できなくなります。
ですので、oragoraclegodrorという名前でドライバを登録する必要がありました。

結論

ドライバ名をnroracleからoraに変更することで処理を実行させることができました。

// New ...
func New() (*Client, error) {
    src := "" // 接続情報
    sql.Register("ora", newrelic.InstrumentSQLDriver(oci8.Driver, baseBuilder))
    db, err := sqlx.Open("ora", src)
    if err != nil {
        return nil, err
    }
    if err := db.Ping(); err != nil {
        return nil, err
    }
}

sqlx以外のライブラリを使用していれば、任意の名前でドライバ名を設定できますが、sqlxではドライバ名でplaceholderの種類を判定していましたので、ドライバ名をsqlx内で決められた名前にする必要がありました。
今回はgo-oci8というドライバを使用していますが、他にも独自のドライバを使用した場合でもこの点を注意する必要があるのでみなさんのお役に立てればと思います。