Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/main/java/com/logtail/logback/LogtailAppender.java
Original file line number Diff line number Diff line change
Expand Up @@ -551,8 +551,10 @@ public boolean isDisabled() {

@Override
public void start() {
// The sender runs on a daemon thread, so a JVM exiting on its own would take the queued logs with it
shutdownHook = new Thread(this::stop, "logtail-appender-shutdown");
// The sender runs on a daemon thread, so a JVM exiting on its own would take the queued logs with it. The hook
// only sends the queue and leaves the appender running: all shutdown hooks run at once, and frameworks such as
// Spring Boot and Quarkus keep logging while they shut down and stop logback themselves at the very end
shutdownHook = new Thread(this::flushQueue, "logtail-appender-shutdown");
Runtime.getRuntime().addShutdownHook(shutdownHook);
super.start();
}
Expand All @@ -565,7 +567,7 @@ public void stop() {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
} catch (IllegalStateException e) {
// The JVM is already shutting down - stop() is running from the hook itself or from logback's
// The JVM is already shutting down - stop() is running from logback's or a framework's shutdown hook
}
scheduledExecutorService.shutdown();

Expand All @@ -578,4 +580,16 @@ public void stop() {
flushLock.unlock();
}
}

/**
* Waits for a flush in progress on another thread, then sends everything still queued.
*/
protected void flushQueue() {
flushLock.lock();
try {
flush();
} finally {
flushLock.unlock();
}
}
}
60 changes: 54 additions & 6 deletions src/test/java/com/logtail/logback/LogtailAppenderJvmExitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand All @@ -29,12 +30,27 @@

/**
* Logs queued in the appender must reach Better Stack even when the application never stops logback and
* simply lets the JVM exit, and stop() must not return while a flush is still in progress on another thread.
* simply lets the JVM exit, logs written while a framework shuts down must still be sent when it stops logback,
* and stop() must not return while a flush is still in progress on another thread.
*/
public class LogtailAppenderJvmExitTest {

@Test
public void testQueuedLogsAreSentWhenTheJvmExitsWithoutStoppingLogback() throws Exception {
assertEquals(Collections.singletonList(Collections.singletonList("Logged right before the JVM exits")),
messagesSentByApp(ExitingApp.class));
}

@Test
public void testLogsWrittenWhileAFrameworkShutsDownAreSentWhenItStopsLogback() throws Exception {
assertEquals(Arrays.asList("Logged right before the JVM exits", "Logged while the framework shuts down"),
messagesSentByApp(FrameworkApp.class).stream().flatMap(List::stream).collect(Collectors.toList()));
}

/**
* Runs the app in a JVM of its own against a local endpoint and returns the messages of each request it sent.
*/
private List<List<Object>> messagesSentByApp(Class<?> appClass) throws Exception {
List<String> receivedBodies = new CopyOnWriteArrayList<>();
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", exchange -> {
Expand All @@ -47,7 +63,7 @@ public void testQueuedLogsAreSentWhenTheJvmExitsWithoutStoppingLogback() throws
Process app = new ProcessBuilder(
System.getProperty("java.home") + File.separator + "bin" + File.separator + "java",
"-cp", System.getProperty("java.class.path"),
ExitingApp.class.getName(),
appClass.getName(),
"http://127.0.0.1:" + server.getAddress().getPort())
.inheritIO()
.start();
Expand All @@ -60,10 +76,12 @@ public void testQueuedLogsAreSentWhenTheJvmExitsWithoutStoppingLogback() throws
server.stop(0);
}

assertEquals(1, receivedBodies.size());
List<Map<String, Object>> lines = new ObjectMapper().readValue(receivedBodies.get(0), new TypeReference<List<Map<String, Object>>>() {});
assertEquals(Collections.singletonList("Logged right before the JVM exits"),
lines.stream().map(line -> line.get("message")).collect(Collectors.toList()));
List<List<Object>> messages = new ArrayList<>();
for (String body : receivedBodies) {
List<Map<String, Object>> lines = new ObjectMapper().readValue(body, new TypeReference<List<Map<String, Object>>>() {});
messages.add(lines.stream().map(line -> line.get("message")).collect(Collectors.toList()));
}
return messages;
}

/**
Expand All @@ -85,6 +103,36 @@ public static void main(String[] args) {
}
}

/**
* Run in a JVM of its own: like Spring Boot or Quarkus, its shutdown hook keeps logging while it shuts down and
* stops logback at the very end.
*/
public static class FrameworkApp {
public static void main(String[] args) {
LoggerContext context = new LoggerContext();
LogtailAppender appender = new LogtailAppender();
appender.setContext(context);
appender.setAppName("FrameworkApp");
appender.setSourceToken("source-token");
appender.setIngestUrl(args[0]);
appender.start();

Logger logger = context.getLogger("FrameworkApp");
logger.addAppender(appender);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
// All shutdown hooks start together - by now the appender's own hook has done its part
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("Logged while the framework shuts down");
context.stop();
}));
logger.info("Logged right before the JVM exits");
}
}

@Test
public void testStopWaitsForTheFlushInProgressAndSendsWhatQueuedBehindIt() throws Exception {
CountDownLatch requestStarted = new CountDownLatch(1);
Expand Down
Loading