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ライブラリでも回避できると思います。ご参考までに。