- Kotlin 100%
|
All checks were successful
/ release (push) Successful in 1m30s
fix(shortening): Exclusions for short gaps fix(shortening): Suggest plumbing exclusions Reviewed-on: #17 |
||
|---|---|---|
| .forgejo/workflows | ||
| _bom | ||
| _parent | ||
| api | ||
| base | ||
| core | ||
| logback | ||
| okhttp | ||
| otel | ||
| rest-template | ||
| servlet | ||
| spring-aop | ||
| spring-boot-starter | ||
| .editorconfig | ||
| .gitignore | ||
| AI.md | ||
| CLAUDE.md | ||
| CONTEXT.md | ||
| LICENSE.txt | ||
| pom.xml | ||
| README.md | ||
| renovate.json | ||
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 dispatchRequestId- hierarchical correlation ids like8ee36c0c-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:
LoggingFilterfor inbound HTTP logging - Spring AOP:
@LogIt/@LogValueannotations - Spring Boot starter: autoconfiguration for everything below
- OkHttp:
Interceptorfor outbound HTTP logging - Spring RestTemplate:
ClientHttpRequestInterceptorfor outbound HTTP logging - OpenTelemetry:
lt-logging-otelkeeps 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.
- 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") } - 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}")) } - 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>
The starter activates each integration automatically when its module is on the classpath. Configuration uses Spring properties under thedependencies { implementation("de.loosetie.logging:lt-logging-spring-boot-starter") implementation("de.loosetie.logging:lt-logging-servlet") }lt.loggingprefix (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
valfirst. - 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
RequestIdin 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
- Source: git.denktmit.tech/DenktMit-OSS/lt-logging
- Build:
mvn install(Java 21, Maven 3.9.x); tests for one module:mvn test -pl base - Release: push a
v*tag, CI deploys to the DenktMit-OSS registry - License: Apache License 2.0; commercial use requires a commercial license, see LICENSE.txt