使用Ktor服务器监视性能



我想使用Ktor服务器插件来跟踪API调用的性能指标向Sentry发送性能事务。如何将Sentry java和kotlin sdk与Ktor集成?

有两种方法可以将Ktor服务器与Sentry性能监控集成在一起。

通过OpenTelemetry

Sentry SDK支持导出OpenTelemetry跟踪和跨度作为Sentry事务和跨度。

通过OpenTelemetry的Ktor 1.0和Ktor 2.0客户端和服务器集成(OpenTelemetry Registry for Ktor)添加OpenTelemetry跟踪到Ktor。

然后配置Sentry SDK和OpenTelemetry,将OpenTelemetry跟踪和跨度传播到Sentry。

通过Sentry SDK

创建一个自定义Ktor插件,将事务和跨度导出到Sentry。它可能看起来像这样:

import io.ktor.server.application.*
import io.ktor.server.application.hooks.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import io.sentry.Breadcrumb
import io.sentry.ITransaction
import io.sentry.Sentry
import io.sentry.SpanStatus
import io.sentry.kotlin.SentryContext
import io.sentry.protocol.Request
import io.sentry.protocol.Response
import kotlinx.coroutines.withContext
val sentryTransactionKey = AttributeKey<ITransaction>("SentryTransaction")
val SentryPlugin = createApplicationPlugin("SentryPlugin") {
on(MonitoringEvent(Routing.RoutingCallStarted)) { call ->
val transaction = Sentry.startTransaction(
/* name = */ "${call.request.httpMethod.value} ${call.request.path()}",
/* operation = */ "call",
/* customSamplingContext = */ CustomSamplingContext().apply {
this["path"] = call.request.path().lowercase()
},
/* bindToScope = */ true
)
// customize as necessary
Sentry.configureScope { scope ->
scope.addBreadcrumb(Breadcrumb.http(call.request.uri, call.request.httpMethod.value))
scope.request = Request().apply {
method = call.request.httpMethod.value
url = call.request.path()
queryString = call.request.queryString()
headers = call.request.headers.toMap()
.mapValues { (_, v) -> v.firstOrNull() }
}
scope.setTag("url", call.request.uri)
scope.setTag("host", call.request.host())
scope.user = User().apply {
// call.authentication
}
}
call.attributes.put(sentryTransactionKey, transaction)
transaction.startChild("setup", "Call setup")
}
on(MonitoringEvent(Routing.RoutingCallStarted)) { call ->
call.sentryTransactionOrNull()?.let { t ->
t.latestActiveSpan?.finish(SpanStatus.OK)
t.setTag("route", call.route.parent.toString())
t.startChild("processing", "Request processing")
}
}
on(ResponseBodyReadyForSend) { call, content ->
call.sentryTransactionOrNull()?.let { t ->
t.latestActiveSpan?.finish(SpanStatus.OK)
t.startChild("sending", "Sending response")
}
}
on(CallFailed) { call, cause ->
call.sentryTransactionOrNull()?.apply {
throwable = cause
}
}
on(ResponseSent) { call ->
call.sentryTransactionOrNull()?.let { t ->
t.latestActiveSpan?.finish(SpanStatus.OK)
Sentry.addBreadcrumb(
Breadcrumb.http(call.request.uri, call.request.httpMethod.value, call.response.status()?.value)
)
t.contexts.setResponse(
Response().apply {
headers = call.response.headers.allValues().toMap().mapValues { (_, v) -> v.firstOrNull() }
statusCode = call.response.status()?.value
},
)
t.finish(SpanStatus.fromHttpStatusCode(call.response.status()?.value, SpanStatus.OK))
}
}
on(SentryContextHook()) { block ->
block()
}
}
fun ApplicationCall.sentryTransactionOrNull() =
if (attributes.contains(sentryTransactionKey)) attributes[sentryTransactionKey] else null
class SentryContextHook : Hook<suspend (suspend () -> Unit) -> Unit> {
private val phase = PipelinePhase("SentryContext")
override fun install(
pipeline: ApplicationCallPipeline,
handler: suspend (suspend () -> Unit) -> Unit
) {
pipeline.insertPhaseBefore(ApplicationCallPipeline.Setup, phase)
pipeline.intercept(phase) {
withContext(SentryContext()) {
handler(::proceed)
}
}
}
}

插件应该安装在StatusPages之后(如果使用),这样on(CallFailed)才能正常工作。

插件为请求配置SentryContext(需要Sentry kotlin coroutines扩展),以便Sentry调用创建子跨度或面包屑或其他此类操作将附加到请求范围。此作用域也被传播到子协同程序中。

它还创建了一个采样上下文,该上下文可用于控制性能监视的采样。例子:

Sentry.init { options ->
...
options.tracesSampleRate = 1.0 // default if sampler below returns null
options.setTracesSampler { context ->
val samplingContext = context.customSamplingContext
if (samplingContext != null) {
// if this is the continuation of a trace, just use that decision
// (rate controlled by the caller)
if (context.transactionContext.parentSampled == true) return@setTracesSampler 1.0
@Suppress("MagicNumber")
when (samplingContext["path"] as? String) {
"/healthz" -> 0.001
// customize as necessary
else -> null
}
} else {
null
}
}
}

最新更新