LogRingBufferService.java
package com.taxonomy.shared.service;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import jakarta.annotation.PostConstruct;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
/**
* In-memory ring buffer that captures the last N log entries for the admin log viewer.
*/
@Service
public class LogRingBufferService {
private static final String APPENDER_NAME = "admin-ring-buffer";
private static final int MAX_ENTRIES = 500;
private final Deque<LogEntry> entries = new ConcurrentLinkedDeque<>();
private final AtomicInteger size = new AtomicInteger();
@PostConstruct
public void init() {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
// Guard against duplicate appenders on Spring context refresh (e.g. in tests)
if (rootLogger.getAppender(APPENDER_NAME) != null) {
return;
}
var appender = new AppenderBase<ILoggingEvent>() {
@Override
protected void append(ILoggingEvent event) {
var entry = new LogEntry(
Instant.ofEpochMilli(event.getTimeStamp()),
event.getLevel().toString(),
event.getLoggerName(),
event.getFormattedMessage()
);
entries.addLast(entry);
if (size.incrementAndGet() > MAX_ENTRIES) {
entries.pollFirst();
size.decrementAndGet();
}
}
};
appender.setContext(loggerContext);
appender.setName(APPENDER_NAME);
appender.start();
rootLogger.addAppender(appender);
}
public List<LogEntry> getEntries(String level, String component, int limit) {
var result = new ArrayList<LogEntry>();
String componentLower = (component != null && !component.isEmpty())
? component.toLowerCase(Locale.ROOT) : null;
// Iterate newest-first to collect the most recent matching entries
var iter = entries.descendingIterator();
int count = 0;
while (iter.hasNext() && count < limit) {
var entry = iter.next();
if (level != null && !level.isEmpty() && !entry.level().equalsIgnoreCase(level)) continue;
if (componentLower != null
&& !entry.logger().toLowerCase(Locale.ROOT).contains(componentLower)) continue;
result.add(entry);
count++;
}
Collections.reverse(result);
return result;
}
public record LogEntry(Instant timestamp, String level, String logger, String message) {}
}