This is a demonstration of proof-of-concept testing.
- Concept 1: involves using SLF4J + LOGBACK.
- Concept 2: involves using LOGBACK to output logs to the database.
- Concept 3: uses PostgreSQL as the database.
Create Quarkus Application
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=quarkus-demo \
-DclassName="com.example.GreetingResource" \
-Dpath="/hello"
Add extensions
Add Support for PostgreSQL and Logback.
cd quarkus-demo
mvn quarkus:add-extension -Dextensions="logging-logback"
mvn quarkus:add-extension -Dextensions="quarkus-jdbc-postgresql"
You can use the following command to search for available extensions.
mvn quarkus:list-extensions
Round 1 - Use Logback's built-in DBAppender
https://logback.qos.ch/manual/appenders.html
As of logback version 1.2.8 DBAppender no longer ships with logback-classic. However, DBAppender for logback-classic is available under the following Maven coordinates:
ch.qos.logback.db:logback-classic-db:1.2.11.1
Round 1 : Fail.
Round 2 - Use ch.qos.logback.db:logback-classic-db:1.2.11.1 DBAppender
The logback version referenced by logback-classic-db is incompatible with the version referenced by quarkus logback.
Round 2 : Fail.
Round 3 - Write your own custom Logback appender.
quarkus-demo/src/main/java/com/example/log
├── AsyncPostgresAppender.java
└── PostgresAppender.java
PostgresAppender.java
package com.example.log;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class PostgresAppender extends AppenderBase<ILoggingEvent> {
private String url;
private String user;
private String password;
@Override
protected void append(ILoggingEvent event) {
try (Connection conn = DriverManager.getConnection(url, user, password)) {
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO app_log(level, logger, message, ts) VALUES (?, ?, ?, now())"
);
stmt.setString(1, event.getLevel().toString());
stmt.setString(2, event.getLoggerName());
stmt.setString(3, event.getFormattedMessage());
stmt.executeUpdate();
} catch (Exception e) {
addError("Failed to write log to DB", e);
}
}
public void setUrl(String url) {
this.url = url;
}
public void setUser(String user) {
this.user = user;
}
public void setPassword(String password) {
this.password = password;
}
}
AsyncPostgresAppender.java
package com.example.log;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class AsyncPostgresAppender extends AppenderBase<ILoggingEvent> {
private String url;
private String user;
private String password;
private int queueSize = 1024;
private BlockingQueue<ILoggingEvent> queue;
private Thread worker;
private volatile boolean running = true;
@Override
public void start() {
queue = new ArrayBlockingQueue<>(queueSize);
worker = new Thread(() -> {
while (running || !queue.isEmpty()) {
try {
// Wait Event
ILoggingEvent event = queue.take();
writeToDB(event);
} catch (InterruptedException ignored) {
} catch (Exception e) {
addError("Error writing log to DB", e);
}
}
}, "AsyncPostgresAppender-Worker");
worker.setDaemon(true);
worker.start();
super.start();
}
@Override
protected void append(ILoggingEvent event) {
if (!running) return;
if (!queue.offer(event)) {
addError("Queue full, dropping log: " + event.getFormattedMessage());
}
}
private void writeToDB(ILoggingEvent event) {
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO app_log(level, logger, message, ts) VALUES (?, ?, ?, now())")) {
stmt.setString(1, event.getLevel().toString());
stmt.setString(2, event.getLoggerName());
stmt.setString(3, event.getFormattedMessage());
stmt.executeUpdate();
} catch (Exception e) {
addError("Failed to write log to DB", e);
}
}
@Override
public void stop() {
running = false;
if (worker != null) {
worker.interrupt();
try {
worker.join(5000);
} catch (InterruptedException ignored) {}
}
super.stop();
}
public void setUrl(String url) {
this.url = url;
}
public void setUser(String user) {
this.user = user;
}
public void setPassword(String password) {
this.password = password;
}
public void setQueueSize(int queueSize) {
this.queueSize = queueSize;
}
}
Database
init_logback_my.sql
CREATE TABLE app_log (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ DEFAULT NOW(), -- now()
level VARCHAR(16) NOT NULL, -- event.getLevel().toString()
logger VARCHAR(255) NOT NULL, -- event.getLoggerName()
message TEXT NOT NULL -- event.getFormattedMessage()
);
CREATE INDEX idx_app_log_ts ON app_log(ts DESC);
CREATE INDEX idx_app_log_level ON app_log(level);
Use Docker to provide the PostgreSQL Server
docker run \
--name my-postgres-log \
-e POSTGRES_USER=demouser \
-e POSTGRES_PASSWORD=Passw0rd! \
-e POSTGRES_DB=demodb \
-v $(pwd)/init_logback_my.sql:/docker-entrypoint-initdb.d/init.sql \
-p 5432:5432 \
-d \
postgres:18.0-alpine3.22
Logback Config File
quarkus-demo/src/main/resources
├── application.properties
└── logback.xml
logback.xml
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %-5level %logger{36} - ***>>> %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>app.log</file>
<encoder>
<pattern>%d %-5level %logger{36} - ***>>> %msg%n</pattern>
</encoder>
<append>true</append>
</appender>
<appender name="DB" class="com.example.log.PostgresAppender">
<url>jdbc:postgresql://localhost:5432/demodb</url>
<user>demouser</user>
<password>Passw0rd!</password>
</appender>
<!--
<appender name="ASYNC_DB" class="com.example.log.AsyncPostgresAppender">
<url>jdbc:postgresql://localhost:5432/demodb</url>
<user>demouser</user>
<password>Passw0rd!</password>
<queueSize>2048</queueSize>
</appender>
-->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="DB"/>
<!--
<appender-ref ref="ASYNC_DB"/>
-->
</root>
</configuration>
Appfile
quarkus-demo/src/main/java/com/example
├── GreetingResource.java
└── log
├── AsyncPostgresAppender.java
└── PostgresAppender.java
GreetingResource.java
- add slf4j Logger and LoggerFactory.
package com.example;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/hello")
public class GreetingResource {
private static final Logger logger = LoggerFactory.getLogger(GreetingResource.class);
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
logger.info("call hello().");
return "Hello from Quarkus REST";
}
// helloWithParameter - PathParam
// /hello/John
@GET
@Path("/{name}")
@Produces(MediaType.TEXT_PLAIN)
public String helloWithParameter(@PathParam("name") String name) {
logger.info("Received name: {}", name);
return "Hello, " + name + "!";
}
// /hello/with-query?name=John
@GET
@Path("/with-query")
@Produces(MediaType.TEXT_PLAIN)
public String helloWithQuery(@QueryParam("name") String name) {
logger.info("Received query name: {}", name);
return "Hello, " + name + "!";
}
}
pom.xml
- Remove dependencies related to "test".
- Confirm the following content has been added:
<dependency>
<groupId>io.quarkiverse.logging.logback</groupId>
<artifactId>quarkus-logging-logback</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<scope>compile</scope>
</dependency>
full pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>quarkus-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.14.1</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.29.2</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.4</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.logging.logback</groupId>
<artifactId>quarkus-logging-logback</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED</argLine>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>
Final
quarkus-demo
├── pom.xml <-- Change
├── README.md
├── init_logback_my.sql <-- New
└── src
└── main
├── docker
...
├── java
│ └── com
│ └── example
│ ├── GreetingResource.java <-- Change
│ └── log
│ ├── AsyncPostgresAppender.java <-- New
│ └── PostgresAppender.java <-- New
└── resources
├── application.properties
└── logback.xml <-- New
Build For JVM
mvn clean package
Run
java -jar target/quarkus-app/quarkus-run.jar
Test
curl http://localhost:8080/hello
curl http://localhost:8080/hello/John
curl http://localhost:8080/hello/with-query?name=John
Verify
Using database tools, connect to PostgreSQL, inspect the table, and see if there are any records of the logs we just wrote.
.
Build For Native
mvn clean package -Dnative
Run
./target/quarkus-demo-1.0.0-SNAPSHOT-runner
Test
curl http://localhost:8080/hello
curl http://localhost:8080/hello/John
curl http://localhost:8080/hello/with-query?name=John
Console Output:
$ ./target/quarkus-demo-1.0.0-SNAPSHOT-runner
2025-11-12 15:47:43,128 INFO io.quarkus - ***>>> quarkus-demo 1.0.0-SNAPSHOT native (powered by Quarkus 3.29.2) started in 0.085s. Listening on: http://0.0.0.0:8080
2025-11-12 15:47:43,187 INFO io.quarkus - ***>>> Profile prod activated.
2025-11-12 15:47:43,237 INFO io.quarkus - ***>>> Installed features: [agroal, cdi, jdbc-postgresql, logging-logback, narayana-jta, rest, smallrye-context-propagation, vertx]
2025-11-12 15:47:59,785 INFO com.example.GreetingResource - ***>>> call hello().
2025-11-12 15:47:59,864 INFO com.example.GreetingResource - ***>>> Received name: John
2025-11-12 15:47:59,939 INFO com.example.GreetingResource - ***>>> Received query name: John
Verify
Using database tools, connect to PostgreSQL, inspect the table, and see if there are any records of the logs we just wrote.
.
The above is executable. It's not perfect, but it's executable.
There are many more test combinations (Round 4, 5, 6,....), but they all failed.
Only this imperfect but executable example is retained.