KtorでCallIdを使ってクライアントを一意に特定する

タイトルについてまず捕捉する。

そもそもログインしていないアカウントに関しては、サーバーで発行したセッションIDをCookieに持っていない。

これで困るのがロギングのシーンである。未ログイン状態でアクセスしてくるユーザーが入り混じるため、とあるユーザーの行動をログから追跡することができない。

このようなシチュエーションに有用なのが**CallId**である。

# インストール

まずは`build.gradle.kts`にインストール。

```kotlin
dependencies {
    // 略

    // for call id
    implementation("io.ktor:ktor-server-call-id:$ktor_version")

    //略
}
```

次に`Application.kt`からKtorにプラグインをインストールする。`install`の中の`verify`などについては後ほど詳しく説明する。

```kotlin
package example.koin

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.callid.*
import io.ktor.server.plugins.callloging.*

fun main(args: Array<String>) {
    io.ktor.server.tomcat.EngineMain.main(args)
}

fun Application.module() {
    settingKoin()
    configureRouting()
    install(CallId) {
        header(HttpHeaders.XRequestId)
        verify { callId: String ->
            callId.isNotEmpty()
        }
    }
    install(CallLogging) {
        callIdMdc("call-id")
    }
}
```

最後に`logback.xml`に`X-Request-ID`として付与される`CallId`を出力するための設定をいれる。`%X{call-id}`がそう。

```xml
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %X{call-id} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="debug">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
```

https://ktor.io/docs/server-call-id.html#put-call-id-mdc

# CallId を検証する

まずは`X-Request-ID`の有無によるログ出力やレスポンスをみてみる。今回は`inorinrinrin`という`CallId`をマニュアルで付与する。

するとレスポンスヘッダにも同じものが設定されているのがわかる。これは`header()`が取得→送信をひとまとめにしてくれているからである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/012b7fef-558f-4582-8e90-8c09d3e653d0.png)

ログをみると、`X-Request-ID`に紐づいた`CallId`が出力されているのがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/bf9caa16-fffe-4c5a-97c6-d977a1990cb0.png)

今度は`X-Request-ID`を外してリクエストを送信する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/f33b2974-7a35-4795-93ba-79d550d7efa8.png)

すると今度はログにCallIdが出力されていないことがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/1da1e144-c8f4-4eab-a3c9-05ab308d514f.png)

これは`verify`が`X-Request-ID`の妥当性を検証しているからである。それを確認するため、`verify`の条件を`ino`から始まる文字列にした場合にどうなるかをみてみる。

```diff
    install(CallId) {
        header(HttpHeaders.XRequestId)
        verify { callId: String ->
+            callId.startsWith("ino")
        }
    }
```

`X-Request-ID`を`ayaneru`としてみた。このとき、レスポンスヘッダに`X-Request-ID`が返ってこない。ログにも`CallId`は出ていない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/0bc7bf98-d3e7-47a4-882f-66e672ddbfe2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/1586312a-23f0-4e89-8b32-e1274cfe8fd4.png)

以上から、`X-Request-ID`が付与されていれば以降の通信でそれを使いまわせること。ログに出力できることを確認できた。

# CallIdを発行する

あとは`X-Request-ID`を初回リクエスト時に発行してあげればよい。`CallId`を生成し`X-Request-ID`に設定するにはgenerateを使う。

生成方法にはドキュメントに記載があるとおり、文字の種類を渡して適当な長さのIDを生成させるか、自前で生成する方法がある。

今回は自前生成関数`generateRandomString`を定義してみる。

```kotlin
fun generateRandomString(length: Int): String {
    val charPool = ('a'..'z') + ('A'..'Z') + ('0'..'9') // 小文字、大文字、数字
    return (1..length)
        .map { Random.nextInt(charPool.size) }
        .map(charPool::get)
        .joinToString("")
}
```

これを`generate`に渡す。

```diff
    install(CallId) {
        header(HttpHeaders.XRequestId)
+        generate { generateRandomString(10) }
        verify { callId: String ->
            callId.isNotEmpty()
        }
    }
```

では`X-Request-ID`を付与しない初回のリクエストを見てみる。すると10文字の`CallId`が生成され、レスポンスヘッダに付与されたのがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584722/63ac9b18-2817-4c92-9e00-29f2bd705984.png)

# おわりに

Ktorがロギングまで気を配ってくれているので非常にありがたい。この手の仕組みを数行で実現できるのは利用者からするとかなり楽。