Kora облачно ориентированный серверный фреймворк написанный на Java для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный выходцами из Т-Банк / Тинькофф

Kora is a cloud-oriented server-side Java framework for writing Java / Kotlin applications with a focus on performance, efficiency and transparency

Skip to content

Logging

Kora uses slf4j-api as the common logging facade across the framework. SLF4J separates application code from the concrete logging implementation, and Kora expects Logback to be used as the main implementation.

The logging module is responsible for obtaining a Logger through the standard SLF4J factory, managing logging levels through Kora configuration, and passing structured data to log records. Structured data can be added through StructuredArgument, Marker, and MDC so that it is emitted together with the regular text message.

For a step-by-step walkthrough before the reference details, see Observability.

Usage

A Logger is created through the SLF4J factory:

Logger logger = LoggerFactory.getLogger(SomeService.class);
val logger = LoggerFactory.getLogger(SomeService::class.java)

Configuration

Logging levels are described by the LoggingConfig class. The configuration sets a level for ROOT, a package, or a specific class:

logging {
  levels {  //(1)!
    "ROOT": "WARN"
    "ru.tinkoff.kora": "INFO"
    "ru.tinkoff.kora.http.server.common.telemetry": "INFO"
    "ru.tinkoff.kora.http.client.common.telemetry.DefaultHttpClientTelemetry": "INFO"
  }
}
  1. Logging levels for ROOT, classes, and packages (default: not specified, optional).
logging:
  levels: #(1)!
    ROOT: "WARN"
    ru.tinkoff.kora: "INFO"
    ru.tinkoff.kora.http.server.common.telemetry: "INFO"
    ru.tinkoff.kora.http.client.common.telemetry.DefaultHttpClientTelemetry: "INFO"
  1. Logging levels for ROOT, classes, and packages (default: not specified, optional).

The section key may be written as either levels or level — both are accepted as aliases. Logger names may be listed as flat dotted strings (as above) or as a nested object; Kora flattens nested objects into dotted logger names. The ROOT logger name is matched case-insensitively, so ROOT and root are equivalent. For example, the shipped examples use the singular level alias with a nested lowercase root:

logging.level {
  "root": "WARN"
  "ru.tinkoff.kora": "INFO"
  "ru.tinkoff.kora.example": "INFO"
}

Note

When the logging section is absent, Kora applies no level map of its own. The Logback implementation, however, resets all loggers on every (re)apply: ROOT is normalized to INFO and every other per-logger level is cleared so that it inherits from its parent, after which the configured levels are applied on top. As a result, the <root level="..."> value from logback.xml is effectively replaced by INFO at startup unless a ROOT level is set in the configuration.

Runtime level refresh

Configured levels are applied by the LoggingLevelRefresher — a root component that on startup resets all loggers and re-applies the levels from the logging section through the LoggingLevelApplier. It re-runs on every configuration refresh, so when the Config Watcher is active, changing a level in the configuration file takes effect at runtime without restarting the application.

Logging parameters for specific modules are described in the documentation for those modules, for example HTTP server, HTTP client, gRPC client.

Modules

Logging for specific modules is enabled and disabled in the configuration of those modules through telemetry.logging.enabled.

By default, logging is disabled for all modules, so the configuration below shows how to enable logging for most modules:

db.telemetry.logging.enabled = true //(1)!
cassandra.telemetry.logging.enabled = true //(2)!
grpcServer.telemetry.logging.enabled = true //(3)!
httpServer.telemetry.logging.enabled = true //(4)!
scheduling.telemetry.logging.enabled = true //(5)!
grpcClient.SomeGrpcServiceName.telemetry.logging.enabled = true //(6)!
soapClient.SomeSoapServiceName.telemetry.logging.enabled = true //(7)!
SomePathToConfigHttpClient.telemetry.logging.enabled = true //(8)!
SomePathToConfigKafkaConsumer.telemetry.logging.enabled = true //(9)!
SomePathToConfigKafkaProducer.telemetry.logging.enabled = true //(10)!
  1. Logging for JDBC, R2DBC, or Vertx database requests (default: false).
  2. Logging for Cassandra database requests (default: false).
  3. Logging for gRPC server requests (default: false).
  4. Logging for HTTP server requests (default: false).
  5. Logging for scheduler executions (default: false).
  6. Logging for gRPC client requests, specified for a particular service (default: false).
  7. Logging for SOAP client requests, specified for a particular service (default: false).
  8. Logging for HTTP client requests, specified for a particular client (default: false).
  9. Logging for a Kafka consumer, specified for a particular consumer (default: false).
  10. Logging for a Kafka producer, specified for a particular producer (default: false).
db.telemetry.logging.enabled: true #(1)!
cassandra.telemetry.logging.enabled: true #(2)!
grpcServer.telemetry.logging.enabled: true #(3)!
httpServer.telemetry.logging.enabled: true #(4)!
scheduling.telemetry.logging.enabled: true #(5)!
grpcClient.SomeGrpcServiceName.telemetry.logging.enabled: true #(6)!
soapClient.SomeSoapServiceName.telemetry.logging.enabled: true #(7)!
SomePathToConfigHttpClient.telemetry.logging.enabled: true #(8)!
SomePathToConfigKafkaConsumer.telemetry.logging.enabled: true #(9)!
SomePathToConfigKafkaProducer.telemetry.logging.enabled: true #(10)!
  1. Logging for JDBC, R2DBC, or Vertx database requests (default: false).
  2. Logging for Cassandra database requests (default: false).
  3. Logging for gRPC server requests (default: false).
  4. Logging for HTTP server requests (default: false).
  5. Logging for scheduler executions (default: false).
  6. Logging for gRPC client requests, specified for a particular service (default: false).
  7. Logging for SOAP client requests, specified for a particular service (default: false).
  8. Logging for HTTP client requests, specified for a particular client (default: false).
  9. Logging for a Kafka consumer, specified for a particular consumer (default: false).
  10. Logging for a Kafka producer, specified for a particular producer (default: false).

Logback

The module provides a logging implementation based on Logback, adds support for structured logs, and allows logging levels to be managed through the configuration file.

Dependency

Dependency build.gradle:

implementation "ru.tinkoff.kora:logging-logback"

Module:

@KoraApp
public interface Application extends LogbackModule { }

Dependency build.gradle.kts:

implementation("ru.tinkoff.kora:logging-logback")

Module:

@KoraApp
interface Application : LogbackModule

Configuration

Logback is configured through logback.xml, while Kora configuration usually contains only logging levels. Example logback.xml:

<configuration debug="false">
    <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ru.tinkoff.kora.logging.logback.ConsoleTextRecordEncoder"/>
    </appender>

    <appender name="ASYNC" class="ru.tinkoff.kora.logging.logback.KoraAsyncAppender">
        <appender-ref ref="STDOUT"/>
    </appender>

    <root level="WARN">
        <appender-ref ref="ASYNC"/>
    </root>
</configuration>

ConsoleTextRecordEncoder writes a text log record and adds structured data from StructuredArgument, Marker, SLF4J key-value pairs, and MDC. This is the only encoder shipped by the module: it produces text with structured fields appended, not a single JSON document. A record is emitted as a plain text line — timestamp level [thread] logger - <mdc prefixes>message — followed, when structured fields are present, by tab-indented fieldName={json} lines:

2026-07-02 10:15:30.123 INFO  [main] r.t.k.example.SomeService - userId=42 user logged in
    role="admin"

KoraAsyncAppender is used for asynchronous log writing: it stores MDC values from the current context in KoraLoggingEvent so they are not lost when the record is passed to another thread.

Custom pattern

Instead of ConsoleTextRecordEncoder, a standard PatternLayoutEncoder can be used together with the converters that render Kora structured data. KoraMdcConverter renders the Kora context MDC and KoraLoggingMarkerConverter renders a StructuredArgument marker; register them as conversion words and reference them in the pattern:

<configuration>
    <conversionRule conversionWord="koraMdc" converterClass="ru.tinkoff.kora.logging.logback.KoraMdcConverter"/>
    <conversionRule conversionWord="koraMarker" converterClass="ru.tinkoff.kora.logging.logback.KoraLoggingMarkerConverter"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger - %koraMdc%msg %koraMarker%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

Other Implementation

Kora uses slf4j-api as the logging facade, so any compatible implementation can be connected. The base module adds common components for structured logs and logging-level management through the configuration file.

Dependency

The common logging module must be connected:

Dependency build.gradle:

implementation "ru.tinkoff.kora:logging-common"

Module:

@KoraApp
public interface Application extends LoggingModule { }

Dependency build.gradle.kts:

implementation("ru.tinkoff.kora:logging-common")

Module:

@KoraApp
interface Application : LoggingModule

Usage

When using a custom implementation, provide a LoggingLevelApplier component that can apply a logging level for the specified Logger and reset levels to their initial state.

If the application uses structured data, the custom implementation must also support writing StructuredArgument, StructuredArgumentWriter, and MDC.

Structured Logs

Structured logs make it possible to pass not only text but also named fields to a log record. These fields are convenient for log collection tools and can be used for search, filtering, and views.

Structured data can be passed to a log record in two ways:

  • through Marker;
  • through a message parameter.

The marker and arg methods also accept Long, Integer, String, Boolean, and Map<String, String> values. For more complex objects, pass a custom StructuredArgumentWriter or JsonWriter.

Marker

Marker adds a structured field to a log record and does not take a parameter slot in the text message:

var logger = LoggerFactory.getLogger(getClass());
var marker = StructuredArgument.marker("key", "value");
logger.info(marker, "message");
val logger = LoggerFactory.getLogger(javaClass)
val marker = StructuredArgument.marker("key", "value")
logger.info(marker, "message")

Parameter

A message parameter adds a structured field through the regular SLF4J argument array:

var logger = LoggerFactory.getLogger(getClass());
var parameter = StructuredArgument.arg("key", "value");
logger.info("message", parameter);
val logger = LoggerFactory.getLogger(javaClass)
val parameter = StructuredArgument.arg("key", "value")
logger.info("message", parameter)

Complex object

For values that are not a String, number, Boolean, or Map<String, String>, pass a JsonWriter<T> (the same @Json writer generated for the type) or a raw StructuredArgumentWriter lambda that writes the field value directly to the JsonGenerator. Both arg and marker provide these overloads:

var logger = LoggerFactory.getLogger(getClass());
var parameter = StructuredArgument.arg("user", gen -> {
    gen.writeStartObject();
    gen.writeStringField("id", "42");
    gen.writeStringField("role", "admin");
    gen.writeEndObject();
});
logger.info("user logged in", parameter);
val logger = LoggerFactory.getLogger(javaClass)
val parameter = StructuredArgument.arg("user") { gen ->
    gen.writeStartObject()
    gen.writeStringField("id", "42")
    gen.writeStringField("role", "admin")
    gen.writeEndObject()
}
logger.info("user logged in", parameter)

MDC

Structured data can be attached to all records within the current context using the ru.tinkoff.kora.logging.common.MDC class. The value will be added to every log record until it is removed from MDC:

Import

Use ru.tinkoff.kora.logging.common.MDC, not org.slf4j.MDC. Kora keeps its MDC inside the Kora context rather than in a thread-local, so values placed into org.slf4j.MDC are not rendered by the Kora encoders and do not propagate across asynchronous boundaries. For a declarative alternative see @Mdc.

MDC.put("key", "value");
try {
    logger.info("message");
} finally {
    MDC.remove("key");
}
MDC.put("key", "value")
try {
    logger.info("message")
} finally {
    MDC.remove("key")
}

put accepts String, Integer, Long, and Boolean values, as well as a raw StructuredArgumentWriter for arbitrary JSON; typed values are rendered as their JSON type rather than as text. There is also a put(Context, key, value) overload for writing into an explicitly provided context instead of the current one:

MDC.put("userId", 42); //(1)!
logger.info("user resolved");
  1. Rendered as a JSON number (userId=42), not as a string.
MDC.put("userId", 42) //(1)!
logger.info("user resolved")
  1. Rendered as a JSON number (userId=42), not as a string.

Because it lives in the Kora context, the MDC propagates across asynchronous and reactive boundaries together with the context.

If AsyncAppender is used, use ru.tinkoff.kora.logging.logback.KoraAsyncAppender to pass MDC parameters correctly. It snapshots the current context MDC at append time and passes ru.tinkoff.kora.logging.logback.KoraLoggingEvent to the delegate, so the structured MDC is preserved when the record is handed to the async worker thread.