Simple Logger

Discord Source API Docs Issues Releases
Discord GitHub Javadoc GitHub



This is a lightweight SLF4J logger that writes JSON structured log messages to System.out by default. It can also write plain text messages for local development and test output.

It supports MDC, SLF4J 2 fluent addKeyValue() entries, and OpenTelemetry trace correlation in both JSON and plain formats. It is designed to be used by applications that will run in K8s or Lambda.


Dependency

Add avaje-simple-logger as a dependency.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-simple-logger</artifactId>
  <version>{simple-logger.version}</version>
</dependency>

avaje-logger.properties

Add a src/main/resources/avaje-logger.properties to configure the logger.

## specify the default log level
logger.defaultLogLevel=warn

## specify some log levels
log.level.com.foo.bar=DEBUG
log.level.io.avaje=INFO

avaje-logger-test.properties

For testing, we might prefer plain format rather than JSON format. We also might want to define some test specific log levels.

Add a src/test/resources/avaje-logger-test.properties to configure the logger when running tests. Plain format still includes trace fields, MDC entries, and fluent key/value entries before the message.

## for testing we prefer plain format rather than json format
logger.format=plain

## default log level to use when running tests
logger.defaultLogLevel=INFO

## some test specific log levels
log.level.io.ebean.test.containers=TRACE
log.level.io.ebean.DDL=TRACE
#log.level.io.ebean.SQL=DEBUG
#log.level.io.ebean.TXN=DEBUG

Debugging

To debug avaje-simple-logger set the log level for io.avaje.simplelogger to DEBUG.

log.level.io.avaje.simplelogger=DEBUG

If you are also using io.avaje:avaje-aws-appconfig, then you can additionally set io.avaje.config to TRACE.

log.level.io.avaje.config=TRACE

Configure

Configure the logger via main resource avaje-logger.properties and test resource avaje-logger-test.properties

## specify the default log level to use
logger.defaultLogLevel=warn

## specify to log as `json` or `plain` format (defaults to json)
#logger.format=json
logger.format=plain

## specify if the logger name is abbreviated. Values:
## - full - use the full logger name
## - short - use the class name / suffix part of the logger name after the last `.`
## - (some integer e.g. 100) - abbreviate the logger name to the target length (shorten the package names)

#logger.nameTargetLength=full
#logger.nameTargetLength=short
#logger.nameTargetLength=100

logger.nameTargetLength=100

## specify an explicit timezone to use, defaults to using default timezone
logger.timezone=UTC

## specify an explicit timestamp format to use, defaults to ISO_OFFSET_DATE_TIME
## valid values: ISO_OFFSET_DATE_TIME, ISO_ZONED_DATE_TIME, ISO_LOCAL_DATE_TIME, ISO_DATE_TIME, ISO_INSTANT
logger.timestampPattern=ISO_OFFSET_DATE_TIME

## JSON field naming convention: underscore (default), camel, or legacy
logger.naming=underscore

## optionally override specific built-in JSON field names
#logger.propertyNames=logger_name=loggerName,trace_id=traceId,span_id=spanId

Structured JSON

By default, the log format is JSON. Built-in fields such as logger_name, message, thread, and optional trace fields are emitted as structured JSON fields.

{
  "component":"my-application",
  "env":"dev",
  "timestamp":"2025-07-14T13:44:44.230959+12:00",
  "level":"INFO",
  "logger_name":"io.avaje.config",
  "message":"load from [resource:application-test.properties]",
  "thread":"main"
}

JSON output also includes MDC entries, SLF4J 2 fluent key/value entries, and OpenTelemetry trace fields when present.

JSON field names

We can control the built-in JSON field names via logger.naming and logger.propertyNames in avaje-logger.properties.

## underscore (default) => logger_name, trace_id, span_id
logger.naming=underscore

## camel => loggerName, traceId, spanId
#logger.naming=camel

## legacy => logger, trace_id, span_id
#logger.naming=legacy

## delimited by comma and equals
#logger.propertyNames=logger_name=loggerName,env=environment,timestamp=@timestamp,trace_id=traceId,span_id=spanId

MDC keys and fluent addKeyValue() entries keep the exact names supplied by the application.

component

A component key value is added if:

Examples:

## a literal value
logger.component=my-application

## uses system property SERVICE_NAME or environment variable SERVICE_NAME
logger.component=${SERVICE_NAME}

## uses system property service.name or environment variable SERVICE_NAME
logger.component=${service.name}

env

An env key value is added to represent the environment the application is running in.

An env key value is added automatically if:

Examples:

## uses system property `app.env` or environment variable `APP_ENV`, defaults to `localdev`
logger.environment=${app.env:localdev}

## literal value
logger.environment=DEV

MDC

Use MDC for request-scoped or job-scoped context that should appear on multiple log lines, such as requestId, tenant, or userId.

try (var requestId = MDC.putCloseable("requestId", "req-42");
     var tenant = MDC.putCloseable("tenant", "blue")) {
  log.info("Loaded order {}", orderId);
}

Example JSON output:

{
  "level":"INFO",
  "logger_name":"com.example.OrderService",
  "message":"Loaded order 42",
  "requestId":"req-42",
  "tenant":"blue"
}

Example plain output:

2026-05-13T10:00:00+12:00 INFO com.example.OrderService - requestId=req-42 tenant=blue Loaded order 42

SLF4J 2 fluent key/value

Use the fluent API for values that belong to a single log event rather than the full request scope.

log.atInfo()
  .addKeyValue("orderId", 42)
  .addKeyValue("processed", true)
  .log("Order processed");

Example JSON output:

{
  "level":"INFO",
  "logger_name":"com.example.OrderService",
  "message":"Order processed",
  "orderId":42,
  "processed":true
}

The same fields are included in plain output as key=value entries before the message.

OpenTelemetry

avaje-simple-logger emits trace_id and span_id from the active OpenTelemetry span. No extra logger property is required, but an active span must be present when the log statement runs.

If your tracing setup does not already provide the OpenTelemetry API on the classpath, add it explicitly and align the version with your tracing distribution.

<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-api</artifactId>
  <version><!-- align with your OpenTelemetry version --></version>
</dependency>

With an active span, ordinary log statements automatically include trace fields:

log.info("Handling order {}", orderId);
{
  "level":"INFO",
  "logger_name":"com.example.OrderService",
  "message":"Handling order 42",
  "trace_id":"0af7651916cd43dd8448eb211c80319c",
  "span_id":"b7ad6b7169203331"
}

If your OpenTelemetry agent also injects trace_id and span_id into MDC, avaje-simple-logger filters those MDC keys so the trace fields are emitted once.

Plain format

For logger.format=plain, contextual data is written before the message in a predictable order:

  1. trace_id and span_id from the active span
  2. Remaining MDC entries
  3. SLF4J fluent addKeyValue() entries
  4. The rendered message
2026-05-13T10:00:00+12:00 INFO com.example.OrderService - trace_id=0af7651916cd43dd8448eb211c80319c span_id=b7ad6b7169203331 requestId=req-42 orderId=42 processed=true Order processed

Use plain format for local development and test output when console readability is more important than JSON ingestion.

Dynamic log levels

avaje-simple-logger automatically registers with avaje-config such that any configuration changes that start with log.level. are logging level configuration changes, and these are applied.

avaje-config supports plugins like AWS AppConfig, where configuration changes can be dynamically made to the application. For example, log.level. changes can be dynamically made this way.

Dynamic configuration applies to log.level.*. MDC entries, fluent key/value fields, and OpenTelemetry trace context still come from application code and tracing instrumentation.

Don't need dynamic configuration?

If an application does not need dynamic configuration, then we can just use avaje-simple-json-logger. This excludes the avaje-config dependency.

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-simple-json-logger</artifactId>
  <version>1.0/version>
</dependency>

Note that log levels can still be modified programmatically via:

Map<!String>, String> nameLevels = new HashMap<>();
nameLevels.put("com.foo", "debug");
nameLevels.put("com.foo.bar", "info");

LoggerContext.get().putAll(nameLevels);