Simple, zero-dependency Java logging library inspired by the log4j2 architecture (levels, appenders, formatters, file-based configuration, asynchronous writing, file rotation).
The library is compiled with the --release 11 flag (see build.gradle.kts).
This means:
- The resulting
.jarcan be used in projects running on Java 11 and any newer JDK (17, 21, 25, etc.) — the JVM is backward-compatible with older bytecode. - You can build the library with any installed JDK (21, 25, …). The
--releaseflag makes the compiler check that you're not using any API introduced after Java 11, no matter which JDK you're compiling with. - The opposite is not true: a
.jarbuilt for Java 17+ will not run on JVM 11. The lower the target, the wider the audience that can use the library.
If later we need newer language features (records, sealed classes, virtual threads, etc.), we can raise the target — but then some users on older JDKs will lose the ability to use the library.
log2jv/
├── build.gradle.kts # build, target Java 11
├── settings.gradle.kts
├── src/main/java/io/log2jv/
│ ├── Level.java # TRACE .. OFF
│ ├── LogRecord.java # immutable log event
│ ├── Formatter.java # formatter interface
│ ├── PatternFormatter.java # patterns like %d{...} %level %logger %msg %n %ex
│ ├── Appender.java # "where to write" interface
│ ├── ConsoleAppender.java
│ ├── FileAppender.java # file writing + size-based rotation
│ ├── AsyncAppender.java # decorator: asynchronous writing via a queue
│ ├── Config.java # default configuration values
│ ├── ConfigLoader.java # reading .properties (system property / classpath)
│ ├── Logger.java # public interface for application code
│ ├── LoggerImpl.java # implementation (package-private)
│ ├── LogManager.java # entry point: LogManager.getLogger(...)
│ ├── MessageFormatter.java # ${} message placeholders
│ ├── Pair.java # simple key-value pair helper
│ └── UncheckedLoggerException.java
├── src/test/java/... # JUnit tests + Demo.java with main()
├── src/test/resources/log2jv.properties # config for running the Demo
└── examples/log2jv.properties # config template for library users
- On the first access
LogManagerloads the configuration once viaConfigLoaderand builds the list of appenders (ConsoleAppender,FileAppender, optionally wrapped inAsyncAppender). LogManager.getLogger(...)returns aLoggerImplthat shares the same appender list and the level threshold from the configuration.- When you call
log.info(...)aLogRecordis created and sent to every appender. Each appender formats the record with its ownFormatterand decides where to write it (stdout/stderr, a file on disk). FileAppendermonitors the file size and, when the limit is exceeded, renamesapp.log → app.log.1 → app.log.2 → ….AsyncAppendersimply puts theLogRecordinto aBlockingQueueand performs the real write on a background daemon thread — the caller never waits for I/O.
You need Gradle installed (or use the included wrapper):
./gradlew build # compile + tests + jar
./gradlew test # tests onlyThe finished artifact appears in:
build/libs/log2jv-0.6.2.jar
dependencies {
implementation("io.github.stepan2521:log2jv:0.6.2")
}dependencies {
implementation 'io.github.stepan2521:log2jv:0.6.2'
}<dependency>
<groupId>io.github.stepan2521</groupId>
<artifactId>log2jv</artifactId>
<version>0.6.2</version>
</dependency>If the artifact is not yet available on Maven Central, use a jar from GitHub Releases or JitPack:
repositories { mavenCentral() maven("https://jitpack.io") } dependencies { implementation("com.github.stepan2521:Log2JV:v0.6.2") }
dependencies {
implementation(files("libs/log2jv-0.6.2.jar"))
}Copy examples/log2jv.properties into src/main/resources/log2jv.properties
of your project and adjust it to your needs.
| Key | Default | Description |
|---|---|---|
logger.level |
INFO |
TRACE / DEBUG / INFO / WARN / ERROR / FATAL / OFF |
console.enabled |
true |
Enable console output |
console.pattern |
see Config |
Formatting pattern for the console |
console.colors |
true |
Enable ANSI colors on the console |
file.enabled |
false |
Enable writing to a file |
file.path |
logs/app.log |
Path to the log file |
file.pattern |
see Config |
Formatting pattern for the file |
file.maxSizeBytes |
10485760 |
Maximum file size before rotation (bytes) |
file.maxBackups |
5 |
How many old copies to keep |
async.enabled |
false |
Asynchronous writing via a background thread |
async.queueCapacity |
1024 |
Queue size for asynchronous writing |
Pattern tokens: %d / %d{date-pattern}, %level, %logger, %thread,
%msg, %ex (exception stack trace), %n (line separator),
%source (Class.method(File.java:line)), %class, %method, %file, %line.
import io.log2jv.Logger;
import io.log2jv.LogManager;
import io.log2jv.Pair;
public class MyService {
private static final Logger log = LogManager.getLogger(MyService.class);
public void doWork() {
log.info("Processing started");
log.info("User ${} connected from ${}", "Stepan", "127.0.0.1");
log.info("Array: ${}; pair: ${}", new int[]{1, 2, 3}, Pair.of("timeout", 5000));
log.info("Cost is $${} dollars", 5); // -> "Cost is $5 dollars"
log.info("$$ is a dollar literal"); // -> "$ is a dollar literal"
try {
// ...
} catch (Exception e) {
log.error("Processing failed", e);
}
}
}${}— next argument (any type). Arrays andPairare formatted automatically.$$— literal dollar sign$.$${}— literal$followed by a placeholder.
The number of placeholders is checked at runtime.
Applied only by ConsoleAppender:
- FATAL — bold magenta
- ERROR — bold red
- WARN — bold yellow
- INFO — italic white
- DEBUG — bold white
- TRACE — italic gray
ERROR and FATAL go to stderr; everything else goes to stdout.
Calling any fatal(...) method logs the message and then throws, terminating the process.
Disable ANSI colors:
console.colors=falseSee Releases.