/Kotlin-Snowflake-ID

Snowflake IDs in Kotlin for distributed systems

Primary LanguageKotlinMIT LicenseMIT

Snowflake IDs for Ktor.

A SnowflakeFactory to generate unique snowflake IDs in Kotlin. Suitable for Ktor and distributed systems.

For the ID reverse parser, Kotlinx is used for serializaiton and Date types. So, the next dependencies must be included unless you ammed the code to use your own serialization solution and concrete Date types:

implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")

How to integrate the factory in a Ktor server logs.

  1. Add the ktor dependencies for the CallLogging and CallId plugins:
implementation("io.ktor:ktor-server-call-logging:2.3.7")
implementation("io.ktor:ktor-server-call-id:2.3.7")
  1. Register the plugins in your Application module, integrating the snowflake factory:
fun Application.configureCallLogging() {

    // Set the machine ID.
    // In this example we set it as 1, but it could be set from the configuration file,
    // or from an environment variable.
    // For example:
    //      val machineId: Int = environment.config.property("ktor.machineId").getString().toInt()
    //      SnowflakeFactory.setMachine(id = machineId)
    SnowflakeFactory.setMachine(id = 1)

    install(CallLogging) {
        level = Level.INFO

        // Integrates the unique call ID into the Mapped Diagnostic Context (MDC) for logging.
        // This allows the call ID to be included in each log entry, linking logs to specific requests.
        callIdMdc(name = "callid")
    }

    install(CallId) {
        // Generates a unique ID for each call. This ID is used for request tracing and logging.
        // Must be added to the logback.xml file to be included in logs. See %X{id} in logback.xml.
        generate {
            SnowflakeFactory.nextId()
        }

        // Optionally we can also include the IDs to the response headers,
        // so that it can be retrieved by the client for tracing.
        replyToHeader(headerName = HttpHeaders.XRequestId)
    }
}
  1. Finally include the callid tag into your logger's configuration file. For example if using logback.xml, the tag %X{callid} is added as next:
<configuration debug="false">
    <statusListener class="ch.qos.logback.core.status.NopStatusListener" />
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} | [%thread] | %-5level | %X{callid} | %logger | %msg%n</pattern>
        </encoder>
    </appender>
    <root level="TRACE">
        <appender-ref ref="STDOUT"/>
    </root>

    <logger name="Application" level="INFO"/>
</configuration>

Example where a GET request has been received. Notice how the 1iadis897lmgw ID was included in the log. Each received request will have a unique ID shared across all the logs that belong to the same concrete call.

2023-12-26 21:13:13.366 | [eventProxy] | TRACE | 1iadis897lmgw | io.ktor.server.plugins.ratelimit.RateLimit | Using key=kotlin.Unit and weight=1 for /v1/employees
2023-12-26 21:13:13.372 | [eventProxy] | TRACE | 1iadis897lmgw | io.ktor.server.plugins.ratelimit.RateLimit | Allowing /v1/employees
2023-12-26 21:13:13.386 | [eventProxy] | DEBUG | 1iadis897lmgw | Exposed | SELECT COUNT(*) FROM EMPLOYEE LEFT JOIN CONTACT ON EMPLOYEE.EMPLOYEE_ID = CONTACT.EMPLOYEE_ID
2023-12-26 21:13:13.513 | [eventProxy] | TRACE | 1iadis897lmgw | io.ktor.server.plugins.statuspages.StatusPages | No status code found for call: /v1/employees
2023-12-26 21:13:13.528 | [eventProxy] | INFO  | 1iadis897lmgw | Application | Call Metric: [127.0.0.1] GET - /v1/employees - 170ms

If the 1iadis897lmgw ID is parsed back, it will give its concrete detailed information:

{
    "machineId": 1,
    "sequence": 0,
    "utc": "2023-12-26T20:13:13.348",
    "local": "2023-12-26T21:13:13.348"
}