No description
Find a file
Christian Keller 7beb13527a
All checks were successful
/ release (push) Successful in 1m30s
feat(shortening): log until first addressable frame / no debug classes (#17)
fix(shortening): Exclusions for short gaps
fix(shortening): Suggest plumbing exclusions

Reviewed-on: #17
2026-09-02 09:24:18 +00:00
.forgejo/workflows build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
_bom build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
_parent build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
api feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
base feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
core build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
logback feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
okhttp build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
otel build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
rest-template build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
servlet build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
spring-aop build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
spring-boot-starter feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
.editorconfig feat: Add AI supporting docs and metadata (#10) 2026-08-16 11:11:47 +00:00
.gitignore build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
AI.md feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
CLAUDE.md feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
CONTEXT.md feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
LICENSE.txt doc: Improve README.md, added module specific readmes (#14) 2026-08-17 06:35:43 +00:00
pom.xml build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00
README.md feat(shortening): log until first addressable frame / no debug classes (#17) 2026-09-02 09:24:18 +00:00
renovate.json build: ci-friendly pattern for pom versions (#18) 2026-09-02 08:15:28 +00:00

LT :: Logging

Structured logging for Kotlin apps, built on slf4j/logback: lazy asynchronous message building, hierarchical request ids across services, and drop-in logging for inbound and outbound HTTP.

What you get

  • Log - slf4j wrapper with lazy lambda messages and async dispatch
  • RequestId - hierarchical correlation ids like 8ee36c0c-h1-s0-db2, mirrored into the MDC
  • @LogIt - annotation-based entry/exit logging with execution time
  • Begin/End logging for HTTP in and out, with method, path, status, bytes and duration in the MDC
  • Stack trace shortening - keep the throwing line, your own packages and some context, drop the rest (... 38 frames omitted)

Integrations

  • Servlet: LoggingFilter for inbound HTTP logging
  • Spring AOP: @LogIt / @LogValue annotations
  • Spring Boot starter: autoconfiguration for everything below
  • OkHttp: Interceptor for outbound HTTP logging
  • Spring RestTemplate: ClientHttpRequestInterceptor for outbound HTTP logging
  • OpenTelemetry: lt-logging-otel keeps the OTel context current across the async log dispatch, so OTel log correlation works with lt-logging

Modules (groupId de.loosetie.logging)

Artifact Purpose Details
lt-logging-bom Version alignment for all modules below -
lt-logging-api Log, RequestId, LogLevel, MDC key constants; no dependencies AI.md
lt-logging-base LogFactory + LogProvider SPI, RequestIdService, LoggingContext; backend-neutral (slf4j-api only) AI.md
lt-logging-core lt-logging-base + lt-logging-logback: the batteries-included dependency, pulled in by every integration AI.md
lt-logging-servlet LoggingFilter for inbound HTTP servlet/README.md
lt-logging-spring-aop @LogIt / @LogValue + LogItAspect spring-aop/README.md
lt-logging-spring-boot-starter Autoconfiguration + lt.logging.* properties spring-boot-starter/README.md
lt-logging-okhttp OkHttp Interceptor for outbound HTTP okhttp/README.md
lt-logging-rest-template RestTemplate ClientHttpRequestInterceptor for outbound HTTP rest-template/README.md
lt-logging-otel Carries the OpenTelemetry Context through async log writes and coroutines otel/README.md
lt-logging-logback The logback backend (LogLogback, registered via ServiceLoader) + ShorteningThrowableConverter for grep-style stack trace shortening; part of lt-logging-core logback/README.md

Requirements: Java 21+, lt-logging-core on the classpath (comes with the starter and the servlet, okhttp, rest-template and spring-aop modules; lt-logging-base alone needs a backend module) and logback-classic as the slf4j backend (see Logback).

Full usage reference - all properties, MDC keys, behavior contracts and pitfalls - lives in AI.md. It is written so you can also feed it directly to your AI coding assistant.

Quick start

The latest version is the newest tag at git.denktmit.tech/DenktMit-OSS/lt-logging/tags; substitute it for {version} below.

  1. Add the DenktMit-OSS Maven registry
    <repositories>
        <repository>
            <id>denktmit-oss</id>
            <url>https://git.denktmit.tech/api/packages/DenktMit-OSS/maven</url>
        </repository>
    </repositories>
    
    repositories {
        maven("https://git.denktmit.tech/api/packages/DenktMit-OSS/maven")
    }
    
  2. Add the BOM
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>de.loosetie.logging</groupId>
                <artifactId>lt-logging-bom</artifactId>
                <version>{version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    dependencies {
        implementation(platform("de.loosetie.logging:lt-logging-bom:{version}"))
    }
    
  3. Choose modules to use (see the module table above)
    <dependencies>
        <dependency>
            <groupId>de.loosetie.logging</groupId>
            <artifactId>lt-logging-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>de.loosetie.logging</groupId>
            <artifactId>lt-logging-servlet</artifactId>
        </dependency>
    </dependencies>
    
    dependencies {
        implementation("de.loosetie.logging:lt-logging-spring-boot-starter")
        implementation("de.loosetie.logging:lt-logging-servlet")
    }
    
    The starter activates each integration automatically when its module is on the classpath. Configuration uses Spring properties under the lt.logging prefix (see AI.md, section Spring Boot starter, for the full table). Without Spring Boot see Manual setup.

Logback

LogFactory creates loggers through the backend module lt-logging-logback (part of lt-logging-core), which requires logback-classic as the slf4j backend; with any other binding on the classpath (slf4j-simple, Log4j bridges, ...) logger creation fails with an IllegalStateException. Exclude competing bindings. Without the backend module (bare lt-logging-base) logger creation fails the same way, naming the missing module.

The library ships no logback.xml. To see the RequestId and the other MDC keys, add them to your encoder pattern.

Plain Logback - its default pattern is defined by TTLLLayout, used by BasicConfigurator when no config file is found (see the Logback manual). The same pattern with the requestId in front of the logger name:

<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} -%kvp- %msg%n</pattern>

Spring Boot - its console pattern (CONSOLE_LOG_PATTERN in org/springframework/boot/logging/logback/defaults.xml, inside the spring-boot jar) already reserves a LOG_CORRELATION_PATTERN slot after the thread name. Fill it via logging.pattern.correlation instead of overriding the whole pattern:

logging.pattern.correlation=[%X{requestId}]

Sample output for one inbound request calling a @LogIt(prefix = "s") service that makes an outbound call, plain Logback pattern from above:

10:41:12.003 [http-nio-8080-exec-1] INFO  [8ee36c0c-h1] d.l.logging.servlet.LoggingFilter -- Begin inbound HTTP/1.1 GET /invoices/42 with null
10:41:12.005 [http-nio-8080-exec-1] DEBUG [8ee36c0c-h1-s0] com.example.InvoiceService -- Begin Invoice load(long)
10:41:12.006 [http-nio-8080-exec-1] INFO  [8ee36c0c-h1-s0-r0] d.l.l.o.LoggingOkHttpInterceptor -- Begin outbound GET https://tax.example/rates
10:41:12.087 [http-nio-8080-exec-1] INFO  [8ee36c0c-h1-s0-r0] d.l.l.o.LoggingOkHttpInterceptor -- End outbound GET returned 200 in 81 ms
10:41:12.090 [http-nio-8080-exec-1] DEBUG [8ee36c0c-h1-s0] com.example.InvoiceService -- End Invoice load(long) returned in 85 ms
10:41:12.091 [http-nio-8080-exec-1] INFO  [8ee36c0c-h1] d.l.logging.servlet.LoggingFilter -- End inbound HTTP/1.1 GET /invoices/42 returned in 88 ms with 200 application/json, in 0 bytes, out 312 bytes

Stack trace shortening

lt-logging-logback adds a conversion word that prints the throwing line, the frames of the packages you name plus one caller frame each, and replaces the rest by ... N frames omitted; exclude entries drop plumbing such as reflection, proxies and filter chains (a list to copy is in AI.md). Register it and use it instead of %ex:

<conversionRule conversionWord="shortEx"
                class="de.loosetie.logging.logback.ShorteningThrowableConverter"/>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} -%kvp- %msg%n%shortEx</pattern>

Spring Boot: logging.exception-conversion-word=%shortEx plus the conversionRule in a logback-spring.xml (see logback/README.md).

then say which frames to keep (Spring Boot; without it assign ShorteningProperties.current yourself):

lt.logging.shortening:
  include:
    de.loosetie:
    org.springframework.transaction:

Nothing configured means nothing shortened. Include/exclude entries, context size and the exact rules: logback/README.md and AI.md, section Stack trace shortening.

Logging

import de.loosetie.logging.logFactory

class Example {
  val log by logFactory()

  fun run() {
    log.info { "started" }           // lambda only evaluated if INFO is enabled
    log.debug(cause) { "detail: $state" }
  }
}

Log calls are dispatched asynchronously; the current MDC and RequestId are captured at the call site. LogFactory.getLogger is available if the delegate does not fit. Consequences of the async dispatch:

  • Message lambdas must be side-effect free and must not read mutable state - they run later, on another thread. Snapshot into a local val first.
  • Output order across threads is not guaranteed; every entry carries a globally increasing MDC value seq - sort by it to reconstruct order.
  • There is no flush-on-shutdown guarantee; messages in flight at an abrupt JVM exit can be lost.

The full contract is in AI.md, section Async dispatch contract.

RequestId

The RequestId is structured hierarchically, so you can follow the context of your request across services and scopes. Pattern: <AppId>[-<Prefix><Sequence>...]

  • AppId: a random integer generated at startup
  • Prefix: a customizable prefix to quickly identify your context (keep it short :))
  • Sequence: a number counted for every generated RequestId in the same scope

Prefix and Sequence may be repeated if a RequestId is generated at a sub-scope.

8ee36c0c-h1           # HttpFilter got a request
8ee36c0c-h1-s0        # A service is called
8ee36c0c-h1-s0-db1    # DB access
8ee36c0c-h1-s0-db2
8ee36c0c-h1-s0        # Return of the service processing

8ee36c0c-h1-s1        # Second service
8ee36c0c-h1-s1-r0     # REST call
8ee36c0c-h1

Open a scope manually with RequestIdService.id("s").use { ... }. The HTTP integrations and @LogIt open scopes automatically.

Across services the id travels in the Correlation-ID header: the outbound interceptors send it, LoggingFilter continues it, so 8ee36c0c-h1-r0 in the caller becomes 8ee36c0c-h1-r0-h0 in the callee. The header name is set once for all integrations: RequestIdService.requestIdHeader (Spring: lt.logging.request-id-header).

LogIt

@LogIt on a Spring bean (class or function level) logs Begin/End with execution time; @LogValue additionally logs parameter or return values.

@Service
@LogIt(prefix = "s")
open class InvoiceService {
  open fun create(@LogValue request: InvoiceRequest): Invoice { ... }
}

Hint: as for all aspect-oriented features this only works on open/non-final functions of Spring beans.

Coroutines

Kotlin coroutines are a separate concept from threads, so the logging context does not follow them automatically. Add LoggingContext when crossing a coroutine boundary:

import de.loosetie.logging.coroutines.LoggingContext
//...
fun example() {
    runBlocking {
        launch(Dispatchers.Default + LoggingContext()) {
            // Slf4j MDC and RequestId context is fine here
        }
    }
}

See also: kotlinx-coroutines-slf4j

LoggingContext also carries the state of any registered ThreadStateCarrier. With lt-logging-otel on the classpath that includes the OpenTelemetry Context, both for your coroutines and for the asynchronous log writes - no configuration needed. See AI.md, section OpenTelemetry, and otel/README.md.

Manual setup (without Spring Boot)

The integration modules do not depend on Spring Boot; wire them yourself. The servlet, okhttp, rest-template and spring-aop modules bring lt-logging-core and with it the backend module lt-logging-logback; with lt-logging-base (or base plus lt-logging-otel only) add a backend module explicitly.

// inbound: register as a servlet filter, as early in the chain as possible
val filter = LoggingFilter(LoggingFilterProperties(level = LogLevel.INFO, prefix = "h"))

// outbound OkHttp
val client = OkHttpClient.Builder()
  .addInterceptor(LoggingOkHttpInterceptor(LoggingOkHttpProperties(prefix = "r")))
  .build()

// outbound RestTemplate (Spring Framework without Boot)
restTemplate.interceptors.add(LoggingRestTemplateInterceptor())

// header used for cross-service ids, default "Correlation-ID"; null or blank disables it
RequestIdService.requestIdHeader = "Correlation-ID"

// stack trace shortening (lt-logging-logback), read by the converter on every event
ShorteningProperties.current = ShorteningProperties(include = mapOf("de.loosetie" to ""))

@LogIt needs a Spring context with AspectJ auto-proxying and a LogItAspect bean; lt-logging-otel needs nothing (registered via ServiceLoader). Options, log formats and details are in the module READMEs: servlet, okhttp, rest-template, spring-aop, spring-boot-starter, otel, logback.

Development