Skip to content

Commit fbee73e

Browse files
committed
chore: trace-id and interceptors for ktor server
1 parent d64bfbb commit fbee73e

File tree

7 files changed

+78
-18
lines changed

7 files changed

+78
-18
lines changed

backend/jvm/src/main/kotlin/dev/suresh/App.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.suresh
33
import BuildConfig
44
import dev.suresh.config.SysConfig
55
import dev.suresh.plugins.configureHTTP
6+
import dev.suresh.plugins.configureInterceptors
67
import dev.suresh.plugins.configureOTel
78
import dev.suresh.plugins.configureSecurity
89
import dev.suresh.plugins.custom.customPlugins
@@ -24,6 +25,7 @@ fun main(args: Array<String>) =
2425
}
2526

2627
fun Application.module() {
28+
configureInterceptors()
2729
configureHTTP()
2830
configureSecurity()
2931
configureOTel()
@@ -35,5 +37,6 @@ fun Application.module() {
3537
services()
3638
mgmtRoutes()
3739
}
40+
3841
// CoroutineScope(coroutineContext).launch {}
3942
}
Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,53 @@
11
package dev.suresh.plugins
22

3-
import io.ktor.http.HttpStatusCode.Companion.BadRequest
4-
import io.ktor.http.HttpStatusCode.Companion.InternalServerError
5-
import io.ktor.http.HttpStatusCode.Companion.NotFound
6-
import io.ktor.http.HttpStatusCode.Companion.Unauthorized
3+
import dev.suresh.http.*
4+
import io.ktor.http.HttpStatusCode
75
import io.ktor.server.application.*
86
import io.ktor.server.plugins.*
97
import io.ktor.server.plugins.statuspages.*
108
import io.ktor.server.request.*
119
import io.ktor.server.response.*
1210

1311
fun Application.errorRoutes() {
12+
1413
install(StatusPages) {
15-
status(Unauthorized) { call, _ -> call.respond(Unauthorized, "Unauthorized") }
14+
status(HttpStatusCode.Unauthorized) { call, status ->
15+
when {
16+
call.isApi ->
17+
call.respondError(
18+
HttpStatusCode.Unauthorized, "Authorization is required to access this resource")
19+
}
20+
}
1621

1722
exception<Throwable> { call, cause ->
1823
val status =
1924
when (cause) {
20-
is BadRequestException -> BadRequest
21-
else -> InternalServerError
25+
is BadRequestException -> HttpStatusCode.BadRequest
26+
else -> HttpStatusCode.InternalServerError
2227
}
2328

2429
call.application.log.error(status.description, cause)
25-
call.respond(status, "${cause.message}")
30+
call.respondError(status, cause.message ?: "Unknown error", cause)
2631
}
2732

28-
unhandled { it.respond(NotFound, "The requested URL ${it.request.path()} was not found") }
33+
unhandled {
34+
it.respondError(
35+
HttpStatusCode.NotFound, "The requested URL ${it.request.path()} was not found")
36+
}
2937
}
3038
}
3139

3240
fun userError(message: Any): Nothing = throw BadRequestException(message.toString())
41+
42+
suspend fun ApplicationCall.respondError(
43+
status: HttpStatusCode,
44+
message: String,
45+
cause: Throwable? = null
46+
) =
47+
respond(
48+
status = status,
49+
message =
50+
ErrorStatus(
51+
code = status.value,
52+
message = message,
53+
details = if (debug) cause?.stackTraceToString() else cause?.message))

backend/jvm/src/main/kotlin/dev/suresh/plugins/Http.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import io.ktor.serialization.kotlinx.json.json
66
import io.ktor.server.application.*
77
import io.ktor.server.plugins.*
88
import io.ktor.server.plugins.autohead.*
9+
import io.ktor.server.plugins.callid.CallId
10+
import io.ktor.server.plugins.callid.callIdMdc
911
import io.ktor.server.plugins.calllogging.*
1012
import io.ktor.server.plugins.compression.*
1113
import io.ktor.server.plugins.contentnegotiation.*
@@ -16,12 +18,18 @@ import io.ktor.server.plugins.hsts.*
1618
import io.ktor.server.plugins.partialcontent.*
1719
import io.ktor.server.request.*
1820
import io.ktor.server.resources.Resources
21+
import io.ktor.server.response.respond
1922
import io.ktor.server.routing.*
2023
import io.ktor.server.websocket.*
2124
import kotlin.time.Duration.Companion.seconds
2225
import kotlin.time.toJavaDuration
26+
import kotlinx.atomicfu.atomic
2327
import org.slf4j.event.Level
2428

29+
const val CALL_ID_PREFIX = "trace-id"
30+
31+
private val counter = atomic(1L)
32+
2533
fun Application.configureHTTP() {
2634
install(Resources)
2735

@@ -57,12 +65,28 @@ fun Application.configureHTTP() {
5765

5866
install(HSTS)
5967

68+
install(CallId) {
69+
header(HttpHeaders.XRequestId)
70+
generate {
71+
when (it.isApi) {
72+
true -> "$CALL_ID_PREFIX-${counter.getAndIncrement()}"
73+
else -> "$CALL_ID_PREFIX-00000"
74+
}
75+
}
76+
verify { it.isNotEmpty() }
77+
}
78+
6079
install(CallLogging) {
6180
level = Level.INFO
6281
disableForStaticContent()
6382
disableDefaultColors()
64-
filter { it.isApiRoute }
83+
84+
// Add MDC entries
6585
mdc("remoteHost") { call -> call.request.origin.remoteHost }
86+
callIdMdc(CALL_ID_PREFIX)
87+
88+
// Enable logging for API routes only
89+
filter { it.isApi }
6690
}
6791

6892
install(WebSockets) {
@@ -73,8 +97,18 @@ fun Application.configureHTTP() {
7397
}
7498
}
7599

100+
fun Application.configureInterceptors() {
101+
intercept(ApplicationCallPipeline.Plugins) {
102+
println("Request: ${call.request.uri}")
103+
if (call.request.headers["Custom-Header"] == "Test") {
104+
call.respond(HttpStatusCode.Forbidden)
105+
finish()
106+
}
107+
}
108+
}
109+
76110
val ApplicationCall.debug
77111
get() = request.queryParameters.contains("debug")
78112

79-
val ApplicationCall.isApiRoute
80-
get() = request.path().startsWith("/api")
113+
val ApplicationCall.isApi
114+
get() = request.path().startsWith("/")

backend/jvm/src/main/kotlin/dev/suresh/plugins/OpenTelemetry.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ class TraceIDResponseCustomizer : HttpServerResponseCustomizer {
2121
responseMutator: HttpServerResponseMutator<T>
2222
) {
2323
val spanContext = Span.fromContextOrNull(serverContext)?.spanContext
24-
if (spanContext?.isValid != true) {
25-
return
26-
}
24+
if (spanContext?.isValid != true) return
2725
responseMutator.appendHeader(response, "X-Trace-Id", spanContext.traceId)
2826
}
2927
}

backend/jvm/src/main/resources/logback.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<!-- [%d{ISO8601}] -->
1414
<property name="pattern"
15-
value="%d{YYYY-MM-dd HH:mm:ss.SSS z, America/Los_Angeles} %-5level %X{remoteHost} [%thread] %X{call-id} %logger{16} - %msg%n%rEx"/>
15+
value="%d{YYYY-MM-dd HH:mm:ss.SSS z, America/Los_Angeles} %X{trace-id} %-5level %X{remoteHost} [%thread] %logger{16} - %msg%n%rEx"/>
1616

1717
<appender name="APP1" class="RollingFileAppender">
1818
<file>${LOG_DIR}/app1.log</file>

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ kotlinx-metadata = "0.9.0"
3636
kotlinx-reflect-lite = "1.1.0"
3737
kotlinx-bcv = "0.16.3"
3838
kotlin-dokka = "1.9.20"
39-
kotlin-wrappers = "1.0.0-pre.795"
39+
kotlin-wrappers = "1.0.0-pre.797"
4040
kotlin-redacted = "1.10.0"
4141
kotlinx-multik = "0.2.3"
4242
kotlinx-dataframe = "0.13.1"
@@ -72,7 +72,7 @@ junit = "5.11.0"
7272
koin = "3.5.6"
7373
kotest = "5.9.0"
7474
mockk = "1.13.12"
75-
mokkery = "2.2.0"
75+
mokkery = "2.3.0"
7676
wiremock = "3.9.1"
7777
wiremock-kotlin = "2.1.1"
7878
okhttp = "5.0.0-alpha.14"

shared/src/commonMain/kotlin/dev/suresh/http/Retry.kt renamed to shared/src/commonMain/kotlin/dev/suresh/http/Config.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dev.suresh.http
22

33
import kotlin.time.Duration
44
import kotlin.time.Duration.Companion.seconds
5+
import kotlinx.serialization.Serializable
56

67
data class Timeout(val connection: Duration, val read: Duration, val write: Duration) {
78
companion object {
@@ -14,3 +15,6 @@ data class Retry(val attempts: Int, val maxDelay: Duration) {
1415
val DEFAULT = Retry(attempts = 2, maxDelay = 2.seconds)
1516
}
1617
}
18+
19+
@Serializable
20+
data class ErrorStatus(val code: Int, val message: String, val details: String? = null)

0 commit comments

Comments
 (0)