コンテンツにスキップ

BraveのMySQL Tracingを使用する際の注意点

Javaにおいて、Tracingを実装する際にはBrave(※)が使用されることが多いです。

Brave側で公式実装としてspring-web, mysql ,kafka, okhttpといったものを提供しています。
https://github.com/openzipkin/brave/tree/master/instrumentation

また、一方で、lettuceのようにライブラリ側でBrave対応をしているケースもあります。
https://github.com/lettuce-io/lettuce-core/wiki/Tracing

※ OpenTelemetryに至るまで色々な歴史があったため、このあたりは複雑です。いずれ、Micrometer Tracing が主流になっていくんだろうと思います。

問題

Braveが公式実装として提供してくれているMySQLのTracingですが、1点注意点があります。

SpanのTagとして、実行したRaw SQLが送られてしまいます。

利用環境にもよるでしょうが、例えば全社共通でZipkinなりJaeger を動かしていて、各チームのサービスが自由に利用している場合を想定しましょう。

チームを跨いで各サービスのRPCがTracingされるため一見すると便利ですが、実行したRaw SQLがSpan Tagとして保存されているため、他のチームにも見られてしまいます。

Raw SQLには個人情報が含まれる可能性が大いにあるため、この状況は望ましくありません。 そのため、Span Tagにこういった情報は含めるべきではありません。

Brave側にはIssueを立ててるため、そのうち何かしらのオプションが導入されるかもしれません。

回避策 - カスタマイズしたTracingQueryInterceptor.javaを使用する

現時点ではどうすることもできないので、TracingQueryInterceptor.javaをカスタマイズして、それを利用するアプローチです。

Issue commentを参考にしてください。
https://github.com/openzipkin/brave/issues/1329#issuecomment-1127178480

回避策 - MyBatisの場合

例えば自分はよくMyBatisを使用しているのですが、MyBatisの場合はMybatisインターセプタのレイヤで以下のような実装をし、これを用いると良いです。MyBatis上のsqlIdが使える点が良いです。

@Intercepts(
    Signature(
        type = Executor::class,
        method = "update",
        args = [MappedStatement::class, Any::class]
    ),
    Signature(
        type = Executor::class,
        method = "query",
        args = [
            MappedStatement::class, Any::class, RowBounds::class, ResultHandler::class, CacheKey::class,
            BoundSql::class
        ]
    ),
    Signature(
        type = Executor::class,
        method = "query",
        args = [MappedStatement::class, Any::class, RowBounds::class, ResultHandler::class]
    )
)
class TracingInterceptor(
    private val tracing: Tracing,
    private val remoteServiceName: String,
) : Interceptor {
    override fun intercept(invocation: Invocation): Any {
        val statement = invocation.args[0] as MappedStatement
        val sqlCommandType = statement.sqlCommandType
        val sqlId = statement.id
        val query = statement.getBoundSql(invocation.args[1]).sql

        if (tracing.currentTraceContext().get() == null) {
            // trace context(parent scope)がないときは、tracingはしない。
            log.debug("Parent scope is not exist, execute without tracing. sqlId={}", sqlId)
            return invocation.proceed()
        }

        val span = tracing.tracer().nextSpan().apply {
            kind(Span.Kind.CLIENT)
            name(sqlCommandType.name)
            tag("sql.id", sqlId)
            tag("sql.query", query) // placeholder SQLなので露出してもOK。不安なら削除してもOK
            remoteServiceName(remoteServiceName)
            start()
        }
        try {
            tracing.tracer().withSpanInScope(span).use {
                return invocation.proceed()
            }
        } catch (rethrown: Throwable) {
            span.error(rethrown)
            throw rethrown
        } finally {
            span.finish()
        }
    }

    companion object {
        private val log = KotlinLogging.logger {}
    }
}

MyBatisインターセプタの利用の仕方については割愛します。

今回はMyBatisでの例でしたが、同じような方法で他のORMライブラリでも回避できると思います。ご参考までに。


最終更新日: 2022/05/16 12:56