Skip to content

Commit

Permalink
feat(springboot): ENTESB-18661 implement LogQueryMBean for Logback/Sp…
Browse files Browse the repository at this point in the history
…ring Boot

Fix #2722
  • Loading branch information
tadayosi committed Mar 31, 2022
1 parent e1453cc commit 87be9d4
Show file tree
Hide file tree
Showing 16 changed files with 956 additions and 117 deletions.
52 changes: 52 additions & 0 deletions hawtio-log-logback/pom.xml
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>io.hawt</groupId>
<artifactId>project</artifactId>
<version>2.15-SNAPSHOT</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>hawtio-log-logback</artifactId>
<name>${project.artifactId}</name>
<description>hawtio :: hawtio-log-logback</description>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.hawt</groupId>
<artifactId>hawtio-log</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>

<!-- testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core-version}</version>
</dependency>
</dependencies>

</project>
@@ -0,0 +1,153 @@
package io.hawt.log.logback;

import java.util.ArrayList;
import java.util.List;

import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.spi.AppenderAttachable;
import io.hawt.log.LogEvent;
import io.hawt.log.LogFilter;
import io.hawt.log.LogResults;
import io.hawt.log.support.LogQueryBase;
import io.hawt.log.support.LruList;
import io.hawt.log.support.Predicate;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* An implementation of {@link LogbackLogQueryMBean}.
*/
public class LogbackLogQuery extends LogQueryBase implements LogbackLogQueryMBean {

private static final transient Logger LOG = LoggerFactory.getLogger(LogbackLogQuery.class);

private static final String APPENDER_NAME = "LogQuery";

private int size = 2000;
private LruList<LoggingEvent> events;

private final Appender<LoggingEvent> appender = new HawtioAppender();

private final LoggingEventMapper eventMapper;

public LogbackLogQuery() {
super();
eventMapper = new LoggingEventMapper(getHostName());
}

@Override
public void start() {
super.start();
appender.start();
reconnectAppender();
}

@Override
public void reconnectAppender() {
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
Logger root = loggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);

if (root instanceof AppenderAttachable) {
appender.setContext((Context) loggerFactory);
@SuppressWarnings("unchecked")
AppenderAttachable<LoggingEvent> attachable = (AppenderAttachable<LoggingEvent>) root;
attachable.addAppender(appender);
LOG.info("Connected to Logback appender to trap logs with Hawtio log plugin");
} else {
LOG.warn("No appender-attachable root logger found so cannot attach Hawtio log appender!");
}
}

@Override
public void logMessage(LoggingEvent record) {
getEvents().add(record);
}

@Override
public LogResults getLogResults(int count) {
return filterLogResults(null, count);
}

@Override
public LogResults queryLogResults(LogFilter filter) {
Predicate<LogEvent> predicate = createPredicate(filter);
int count = filter == null ? -1 : filter.getCount();
return filterLogResults(predicate, count);
}

protected LogResults filterLogResults(Predicate<LogEvent> predicate, int maxCount) {
int matched = 0;
long from = Long.MAX_VALUE;
long to = Long.MIN_VALUE;
List<LogEvent> list = new ArrayList<>();
Iterable<LoggingEvent> elements = getEvents().getElements();
for (LoggingEvent element : elements) {
LogEvent logEvent = eventMapper.toLogEvent(element);
long timestamp = element.getTimeStamp();
if (timestamp > to) {
to = timestamp;
}
if (timestamp < from) {
from = timestamp;
}
if (predicate == null || predicate.matches(logEvent)) {
list.add(logEvent);
matched += 1;
if (maxCount > 0 && matched >= maxCount) {
break;
}
}
}

LogResults results = new LogResults();
results.setEvents(list);
if (from < Long.MAX_VALUE) {
results.setFromTimestamp(from);
}
if (to > Long.MIN_VALUE) {
results.setToTimestamp(to);
}
LOG.debug("Requested {} logging items, returning {} event(s) from possible {}",
maxCount, results.getEvents().size(), getEvents().size());
return results;
}

// -------------------------------------------------------------------------
// Properties
// -------------------------------------------------------------------------

@Override
public int getSize() {
return size;
}

@Override
public void setSize(int size) {
this.size = size;
if (events != null) {
events.resize(size);
}
}

public LruList<LoggingEvent> getEvents() {
if (events == null) {
events = new LruList<>(LoggingEvent.class, getSize());
}
return events;
}

private class HawtioAppender extends AppenderBase<LoggingEvent> {
public HawtioAppender() {
setName(APPENDER_NAME);
}

@Override
protected void append(LoggingEvent event) {
logMessage(event);
}
}
}
@@ -0,0 +1,22 @@
package io.hawt.log.logback;

import ch.qos.logback.classic.spi.LoggingEvent;
import io.hawt.log.support.LogQuerySupportMBean;

/**
* The MBean operations for {@link LogbackLogQuery}
*/
public interface LogbackLogQueryMBean extends LogQuerySupportMBean {
/**
* Provides a hook you can call if the underlying log4j
* configuration is reloaded so that you can force the appender
* to get re-registered.
*/
void reconnectAppender();

void logMessage(LoggingEvent record);

int getSize();

void setSize(int size);
}
@@ -0,0 +1,58 @@
package io.hawt.log.logback;

import java.util.Date;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import io.hawt.log.LogEvent;
import io.hawt.log.support.ThrowableFormatter;

public class LoggingEventMapper {

private String hostName;

public LoggingEventMapper(String hostName) {
this.hostName = hostName;
}

public LogEvent toLogEvent(LoggingEvent loggingEvent) {
LogEvent answer = new LogEvent();
answer.setClassName(loggingEvent.getLoggerName());

ThrowableProxy throwable = (ThrowableProxy) loggingEvent.getThrowableProxy();
if (throwable != null) {
ThrowableFormatter formatter = new ThrowableFormatter();
String[] stack = formatter.doRender(throwable.getThrowable());
if (stack != null) {
answer.setException(stack);
}
}

StackTraceElement[] callerData = loggingEvent.getCallerData();
if (callerData != null && callerData.length > 0) {
StackTraceElement ste = callerData[0];
answer.setFileName(ste.getFileName());
answer.setClassName(ste.getClassName());
answer.setMethodName(ste.getMethodName());
answer.setLineNumber(String.valueOf(ste.getLineNumber()));
}

Level level = loggingEvent.getLevel();
if (level != null) {
answer.setLevel(level.toString());
}
answer.setLogger(loggingEvent.getLoggerName());
String message = loggingEvent.getFormattedMessage();
if (message != null) {
answer.setMessage(message);
}
answer.setProperties(loggingEvent.getMDCPropertyMap());
answer.setSeq(loggingEvent.getTimeStamp());
answer.setTimestamp(new Date(loggingEvent.getTimeStamp()));
answer.setThread(loggingEvent.getThreadName());
answer.setHost(hostName);

return answer;
}
}
@@ -0,0 +1,69 @@
package io.hawt.log.logback;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import io.hawt.log.LogEvent;
import io.hawt.log.LogFilter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;

public class LogbackLogQueryTest {
private static final transient Logger LOG = LoggerFactory.getLogger(LogbackLogQueryTest.class);

private LogbackLogQuery logQuery;

@Before
public void setUp() {
logQuery = new LogbackLogQuery();
logQuery.start();
}

@After
public void tearDown() {
logQuery.stop();
}

@Test
public void testGetLogResults() throws Exception {
// Given
String message = "testGetLogResults - Hello Hawtio";
IntStream.range(0, 10).forEach(i -> LOG.info(message + " #{}", i));

// When
List<LogEvent> events = logQuery.getLogResults(10).getEvents();

// Then
assertThat(events.size()).isEqualTo(10);
List<String> messages = events.stream()
.map(LogEvent::getMessage)
.collect(Collectors.toList());
assertThat(messages).contains(message + " #5");
}

@Test
public void testQueryLogResults() throws Exception {
// Given
String message = "testQueryLogResults - Hello Hawtio";
IntStream.range(0, 10).forEach(i -> LOG.info(message + " #{}", i));

// When
LogFilter filter = new LogFilter();
filter.setMatchesText(message);
filter.setCount(10);
List<LogEvent> events = logQuery.queryLogResults(filter).getEvents();

// Then
assertThat(events.size()).isEqualTo(10);
events.stream()
.map(LogEvent::getMessage)
.forEach(m -> assertThat(m).contains(message));
}

}

0 comments on commit 87be9d4

Please sign in to comment.