This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-mcp-server-contributions.git
commit d2683fa191c536f0d5afb12c77a6d1c044ed2bd5 Author: Robert Munteanu <[email protected]> AuthorDate: Fri Apr 24 15:41:51 2026 +0200 fix(mcp-server-contributions): the log tool now has access to logback messages Previously it would only get events from the OSGi LogService, which are a small subset of all logging events of interest. --- pom.xml | 33 +++- .../server/impl/contribs/LogToolContribution.java | 218 +++++---------------- .../server/impl/contribs/internal/LogSnapshot.java | 42 ++++ .../contribs/internal/StructuredLogBuffer.java | 100 ++++++++++ .../internal/StructuredLogBufferAppender.java | 173 ++++++++++++++++ .../internal/StructuredLogBufferAppenderTest.java | 62 ++++++ .../contribs/internal/StructuredLogBufferTest.java | 64 ++++++ 7 files changed, 515 insertions(+), 177 deletions(-) diff --git a/pom.xml b/pom.xml index f63c232..6299a91 100644 --- a/pom.xml +++ b/pom.xml @@ -108,9 +108,21 @@ <scope>provided</scope> </dependency> <dependency> - <groupId>org.osgi</groupId> - <artifactId>org.osgi.service.log</artifactId> - <version>1.3.0</version> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.7.32</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-core</artifactId> + <version>1.2.13</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>1.2.13</version> <scope>provided</scope> </dependency> <dependency> @@ -119,6 +131,21 @@ <version>0.1.0</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.osgi</groupId> + <artifactId>org.osgi.util.converter</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java index 14d549d..0ec2db3 100644 --- a/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java +++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/LogToolContribution.java @@ -18,29 +18,23 @@ */ package org.apache.sling.mcp.server.impl.contribs; -import java.io.PrintWriter; -import java.io.StringWriter; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; -import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import ch.qos.logback.classic.Level; import io.modelcontextprotocol.json.McpJsonMapperSupplier; import io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.Tool; +import org.apache.sling.mcp.server.impl.contribs.internal.LogSnapshot; +import org.apache.sling.mcp.server.impl.contribs.internal.StructuredLogBufferAppender; import org.apache.sling.mcp.server.spi.McpServerContribution; -import org.osgi.framework.Bundle; -import org.osgi.framework.Constants; -import org.osgi.framework.ServiceReference; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; -import org.osgi.service.log.LogEntry; -import org.osgi.service.log.LogReaderService; -import org.osgi.service.log.LogService; /** * MCP Tool that provides access to logs with filtering capabilities. @@ -50,7 +44,7 @@ import org.osgi.service.log.LogService; public class LogToolContribution implements McpServerContribution { @Reference - private LogReaderService logReaderService; + private StructuredLogBufferAppender structuredLogBufferAppender; @Reference private McpJsonMapperSupplier jsonMapper; @@ -105,10 +99,10 @@ public class LogToolContribution implements McpServerContribution { maxEntries = Math.min(maxEntries, 1000); // Cap at 1000 } - int minLogLevel = LogService.LOG_ERROR; + Level minLogLevel = Level.ERROR; if (logLevelStr != null && !logLevelStr.isEmpty()) { minLogLevel = parseLogLevel(logLevelStr); - if (minLogLevel == -1) { + if (minLogLevel == null) { return CallToolResult.builder() .addTextContent("Invalid log level: " + logLevelStr + ". Valid options are: ERROR, WARN, INFO, DEBUG, TRACE") @@ -130,8 +124,8 @@ public class LogToolContribution implements McpServerContribution { } } - // Collect and filter logs - List<LogEntry> filteredLogs = collectLogs(pattern, minLogLevel, maxEntries); + List<LogSnapshot> filteredLogs = + structuredLogBufferAppender.getBuffer().getRecent(pattern, minLogLevel, maxEntries); // Format output String result = formatLogs(filteredLogs, regexPattern, minLogLevel, maxEntries); @@ -140,89 +134,18 @@ public class LogToolContribution implements McpServerContribution { })); } - private List<LogEntry> collectLogs(Pattern pattern, int minLogLevel, int maxEntries) { - List<LogEntry> logs = new ArrayList<>(); - - @SuppressWarnings("unchecked") - Enumeration<LogEntry> logEntries = logReaderService.getLog(); - while (logEntries.hasMoreElements() && logs.size() < maxEntries) { - LogEntry entry = logEntries.nextElement(); - - // Filter by log level (lower values = higher severity) - if (entry.getLevel() > minLogLevel) { - continue; - } - - // Filter by regex pattern if provided - search entire log entry - if (pattern != null) { - String fullLogEntry = buildFullLogEntryText(entry); - if (!pattern.matcher(fullLogEntry).find()) { - continue; - } - } - - logs.add(entry); - } - - return logs; - } - - private String buildFullLogEntryText(LogEntry entry) { - StringBuilder text = new StringBuilder(); - - // Add log level - text.append(getLogLevelName(entry.getLevel())).append(" "); - - // Add bundle name - Bundle bundle = entry.getBundle(); - if (bundle != null) { - text.append(getBundleName(bundle)).append(" "); - } - - // Add message - String message = entry.getMessage(); - if (message != null) { - text.append(message).append(" "); - } - - // Add service reference info - ServiceReference<?> serviceRef = entry.getServiceReference(); - if (serviceRef != null) { - String serviceDesc = getServiceDescription(serviceRef); - if (serviceDesc != null && !serviceDesc.isEmpty()) { - text.append(serviceDesc).append(" "); - } - } - - // Add exception info - Throwable exception = entry.getException(); - if (exception != null) { - text.append(exception.getClass().getName()).append(" "); - if (exception.getMessage() != null) { - text.append(exception.getMessage()).append(" "); - } - - // Add stack trace - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - exception.printStackTrace(pw); - text.append(sw.toString()); - } - - return text.toString(); - } - - private int parseLogLevel(String levelStr) { + private Level parseLogLevel(String levelStr) { return switch (levelStr.toUpperCase()) { - case "ERROR" -> LogService.LOG_ERROR; - case "WARN", "WARNING" -> LogService.LOG_WARNING; - case "INFO" -> LogService.LOG_INFO; - case "DEBUG" -> LogService.LOG_DEBUG; - default -> -1; + case "ERROR" -> Level.ERROR; + case "WARN", "WARNING" -> Level.WARN; + case "INFO" -> Level.INFO; + case "DEBUG" -> Level.DEBUG; + case "TRACE" -> Level.TRACE; + default -> null; }; } - private String formatLogs(List<LogEntry> logs, String regexPattern, int minLogLevel, int maxEntries) { + private String formatLogs(List<LogSnapshot> logs, String regexPattern, Level minLogLevel, int maxEntries) { StringBuilder result = new StringBuilder(); result.append("=== Log Entries ===\n\n"); @@ -242,7 +165,7 @@ public class LogToolContribution implements McpServerContribution { result.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"); for (int i = 0; i < logs.size(); i++) { - LogEntry entry = logs.get(i); + LogSnapshot entry = logs.get(i); formatLogEntry(entry, i + 1, result); if (i < logs.size() - 1) { @@ -253,49 +176,29 @@ public class LogToolContribution implements McpServerContribution { return result.toString(); } - private void formatLogEntry(LogEntry entry, int index, StringBuilder result) { + private void formatLogEntry(LogSnapshot entry, int index, StringBuilder result) { result.append("[").append(index).append("] "); - result.append(DATE_FORMAT.format(new Date(entry.getTime()))); - result.append(" [").append(getLogLevelName(entry.getLevel())).append("] "); - - // Add bundle information - Bundle bundle = entry.getBundle(); - if (bundle != null) { - String bundleName = getBundleName(bundle); - result.append("[").append(bundleName).append("] "); - } + result.append(DATE_FORMAT.format(new Date(entry.timeMillis()))); + result.append(" [").append(getLogLevelName(entry.level())).append("] "); + result.append("[") + .append(entry.loggerName() != null ? entry.loggerName() : "(unknown logger)") + .append("] "); - // Add message - String message = entry.getMessage(); + String message = entry.formattedMessage(); result.append(message != null ? message : "(no message)"); result.append("\n"); - // Add service reference info if available - ServiceReference<?> serviceRef = entry.getServiceReference(); - if (serviceRef != null) { - String serviceDesc = getServiceDescription(serviceRef); - if (serviceDesc != null && !serviceDesc.isEmpty()) { - result.append(" Service: ").append(serviceDesc).append("\n"); - } + if (entry.threadName() != null && !entry.threadName().isEmpty()) { + result.append(" Thread: ").append(entry.threadName()).append("\n"); } - // Add exception info if available - Throwable exception = entry.getException(); - if (exception != null) { - result.append(" Exception: ").append(exception.getClass().getName()); - if (exception.getMessage() != null) { - result.append(": ").append(exception.getMessage()); - } - result.append("\n"); - - // Add stack trace (first few lines) - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - exception.printStackTrace(pw); - String stackTrace = sw.toString(); + if (!entry.mdc().isEmpty()) { + result.append(" MDC: ").append(formatMdc(entry.mdc())).append("\n"); + } - // Limit stack trace to first 10 lines - String[] lines = stackTrace.split("\n"); + String throwableText = entry.throwableText(); + if (throwableText != null && !throwableText.isEmpty()) { + String[] lines = throwableText.split("\n"); int maxLines = Math.min(lines.length, 10); for (int i = 0; i < maxLines; i++) { result.append(" ").append(lines[i]).append("\n"); @@ -306,53 +209,20 @@ public class LogToolContribution implements McpServerContribution { } } - private String getBundleName(Bundle bundle) { - String name = bundle.getHeaders().get(Constants.BUNDLE_NAME); - if (name == null || name.isEmpty()) { - name = bundle.getSymbolicName(); - } - if (name == null || name.isEmpty()) { - name = "Bundle#" + bundle.getBundleId(); - } - return name; - } - - private String getServiceDescription(ServiceReference<?> ref) { - if (ref == null) { - return null; - } - - Object serviceId = ref.getProperty(Constants.SERVICE_ID); - Object objectClass = ref.getProperty(Constants.OBJECTCLASS); - - StringBuilder desc = new StringBuilder(); - if (objectClass instanceof String[]) { - String[] classes = (String[]) objectClass; - if (classes.length > 0) { - desc.append(classes[0]); - if (classes.length > 1) { - desc.append(" (").append(classes.length - 1).append(" more interfaces)"); - } - } - } - - if (serviceId != null) { - if (desc.length() > 0) { - desc.append(" "); + private String formatMdc(Map<String, String> mdc) { + StringBuilder result = new StringBuilder(); + boolean first = true; + for (Map.Entry<String, String> entry : mdc.entrySet()) { + if (!first) { + result.append(", "); } - desc.append("[id=").append(serviceId).append("]"); + result.append(entry.getKey()).append('=').append(entry.getValue()); + first = false; } - - return desc.toString(); + return result.toString(); } - private String getLogLevelName(int level) { - return switch (level) { - case LogService.LOG_ERROR -> "ERROR"; - case LogService.LOG_WARNING -> "WARN"; - case LogService.LOG_INFO -> "INFO"; - case LogService.LOG_DEBUG -> "DEBUG"; - default -> "LEVEL_" + level; - }; + private String getLogLevelName(Level level) { + return level != null ? level.levelStr : "UNKNOWN"; } } diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/LogSnapshot.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/LogSnapshot.java new file mode 100644 index 0000000..80dce2f --- /dev/null +++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/LogSnapshot.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.mcp.server.impl.contribs.internal; + +import java.util.Collections; +import java.util.Map; + +import ch.qos.logback.classic.Level; + +/** + * Stores only the lightweight, stable parts of a log event so the in-memory buffer + * does not retain full {@code ILoggingEvent} object graphs. + */ +public record LogSnapshot( + long timeMillis, + Level level, + String loggerName, + String threadName, + String formattedMessage, + String throwableText, + Map<String, String> mdc) { + + public LogSnapshot { + mdc = mdc == null ? Collections.emptyMap() : Collections.unmodifiableMap(mdc); + } +} diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBuffer.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBuffer.java new file mode 100644 index 0000000..80c64fe --- /dev/null +++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBuffer.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.mcp.server.impl.contribs.internal; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.regex.Pattern; + +import ch.qos.logback.classic.Level; + +public class StructuredLogBuffer { + + private final Object lock = new Object(); + private final Deque<LogSnapshot> entries = new ArrayDeque<>(); + private int maxEntriesKept; + + public StructuredLogBuffer(int maxEntriesKept) { + this.maxEntriesKept = Math.max(1, maxEntriesKept); + } + + public void append(LogSnapshot snapshot) { + synchronized (lock) { + entries.addLast(snapshot); + trimToSize(); + } + } + + public List<LogSnapshot> getRecent(Pattern pattern, Level minLevel, int maxEntries) { + synchronized (lock) { + List<LogSnapshot> matches = new ArrayList<>(); + int remaining = Math.max(1, maxEntries); + + for (var iterator = entries.descendingIterator(); iterator.hasNext() && remaining > 0; ) { + LogSnapshot snapshot = iterator.next(); + if (!matches(snapshot, pattern, minLevel)) { + continue; + } + matches.add(snapshot); + remaining--; + } + + return matches; + } + } + + private boolean matches(LogSnapshot snapshot, Pattern pattern, Level minLevel) { + if (snapshot.level().isGreaterOrEqual(minLevel)) { + if (pattern == null) { + return true; + } + return matchesField(pattern, snapshot.level() != null ? snapshot.level().levelStr : null) + || matchesField(pattern, snapshot.loggerName()) + || matchesField(pattern, snapshot.threadName()) + || matchesField(pattern, snapshot.formattedMessage()) + || matchesField(pattern, snapshot.throwableText()) + || matchesMdc(pattern, snapshot); + } + return false; + } + + private boolean matchesMdc(Pattern pattern, LogSnapshot snapshot) { + if (snapshot.mdc().isEmpty()) { + return false; + } + for (var entry : snapshot.mdc().entrySet()) { + if (matchesField(pattern, entry.getKey()) || matchesField(pattern, entry.getValue())) { + return true; + } + } + return false; + } + + private boolean matchesField(Pattern pattern, String value) { + return value != null && !value.isEmpty() && pattern.matcher(value).find(); + } + + private void trimToSize() { + while (entries.size() > maxEntriesKept) { + entries.removeFirst(); + } + } +} diff --git a/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferAppender.java b/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferAppender.java new file mode 100644 index 0000000..95a02db --- /dev/null +++ b/src/main/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferAppender.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.mcp.server.impl.contribs.internal; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.LinkedHashMap; +import java.util.Map; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AppenderBase; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.Designate; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; + +@Component( + service = {Appender.class, StructuredLogBufferAppender.class}, + property = { + "loggers=ROOT", + "service.description=Structured in-memory MCP log appender", + "service.vendor=The Apache Software Foundation" + }) +@Designate(ocd = StructuredLogBufferAppender.Configuration.class) +public class StructuredLogBufferAppender extends AppenderBase<ILoggingEvent> { + + // Forward compatibility with logback 1.5+, where IThrowableProxy may expose getOverridingMessage(). + private static final MethodHandle GET_OVERRIDING_MESSAGE = findGetOverridingMessage(); + + @ObjectClassDefinition(name = "Apache Sling MCP Structured Log Buffer") + public @interface Configuration { + + @AttributeDefinition(name = "Max entries") + int maxEntries() default 10000; + } + + private final StructuredLogBuffer buffer; + + @Activate + public StructuredLogBufferAppender(Configuration configuration) { + buffer = new StructuredLogBuffer(configuration.maxEntries()); + setName("mcp-structured-log-buffer"); + } + + public StructuredLogBuffer getBuffer() { + return buffer; + } + + @Override + protected void append(ILoggingEvent eventObject) { + if (eventObject == null) { + return; + } + + buffer.append(new LogSnapshot( + eventObject.getTimeStamp(), + eventObject.getLevel(), + eventObject.getLoggerName(), + eventObject.getThreadName(), + eventObject.getFormattedMessage(), + getThrowableText(eventObject), + copyMdc(eventObject))); + } + + private Map<String, String> copyMdc(ILoggingEvent eventObject) { + Map<String, String> mdc = eventObject.getMDCPropertyMap(); + if (mdc == null || mdc.isEmpty()) { + return Map.of(); + } + return new LinkedHashMap<>(mdc); + } + + private String getThrowableText(ILoggingEvent eventObject) { + IThrowableProxy throwableProxy = eventObject.getThrowableProxy(); + if (throwableProxy == null) { + return null; + } + + StringBuilder text = new StringBuilder(); + appendThrowable(text, throwableProxy, null); + return text.toString(); + } + + private void appendThrowable(StringBuilder text, IThrowableProxy throwableProxy, String prefix) { + if (prefix != null) { + text.append(prefix); + } + text.append(getThrowableHeader(throwableProxy)).append('\n'); + + StackTraceElementProxy[] stackTrace = throwableProxy.getStackTraceElementProxyArray(); + if (stackTrace != null) { + int framesToRender = Math.max(0, stackTrace.length - Math.max(0, throwableProxy.getCommonFrames())); + for (int i = 0; i < framesToRender; i++) { + text.append('\t').append(stackTrace[i]).append('\n'); + } + if (throwableProxy.getCommonFrames() > 0) { + text.append("\t... ") + .append(throwableProxy.getCommonFrames()) + .append(" common frames omitted") + .append('\n'); + } + } + + IThrowableProxy[] suppressed = throwableProxy.getSuppressed(); + if (suppressed != null) { + for (IThrowableProxy suppressedThrowable : suppressed) { + appendThrowable(text, suppressedThrowable, "Suppressed: "); + } + } + + IThrowableProxy cause = throwableProxy.getCause(); + if (cause != null) { + appendThrowable(text, cause, "Caused by: "); + } + } + + private String getThrowableHeader(IThrowableProxy throwableProxy) { + String overridingMessage = getOverridingMessage(throwableProxy); + if (overridingMessage != null && !overridingMessage.isEmpty()) { + return overridingMessage; + } + + StringBuilder header = new StringBuilder(throwableProxy.getClassName()); + String message = throwableProxy.getMessage(); + if (message != null && !message.isEmpty()) { + header.append(": ").append(message); + } + return header.toString(); + } + + private String getOverridingMessage(IThrowableProxy throwableProxy) { + if (GET_OVERRIDING_MESSAGE == null) { + return null; + } + + try { + Object overridingMessage = GET_OVERRIDING_MESSAGE.invoke(throwableProxy); + return overridingMessage instanceof String ? (String) overridingMessage : null; + } catch (Throwable e) { + return null; + } + } + + private static MethodHandle findGetOverridingMessage() { + try { + return MethodHandles.publicLookup() + .findVirtual(IThrowableProxy.class, "getOverridingMessage", MethodType.methodType(String.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + return null; + } + } +} diff --git a/src/test/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferAppenderTest.java b/src/test/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferAppenderTest.java new file mode 100644 index 0000000..c9064e7 --- /dev/null +++ b/src/test/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferAppenderTest.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.mcp.server.impl.contribs.internal; + +import java.util.List; +import java.util.Map; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.junit.jupiter.api.Test; +import org.osgi.util.converter.Converters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class StructuredLogBufferAppenderTest { + + @Test + void appenderSnapshotsFormattedMessageAndThrowable() { + StructuredLogBufferAppender appender = new StructuredLogBufferAppender(configuration(5)); + + LoggerContext context = new LoggerContext(); + appender.setContext(context); + Logger logger = context.getLogger("test.logger"); + RuntimeException failure = new RuntimeException("error"); + LoggingEvent event = new LoggingEvent(getClass().getName(), logger, Level.ERROR, "message", failure, null); + event.setMDCPropertyMap(java.util.Map.of()); + event.setThreadName("worker-1"); + + appender.append(event); + + List<LogSnapshot> logs = appender.getBuffer().getRecent(null, Level.TRACE, 10); + assertEquals(1, logs.size()); + assertEquals("message", logs.get(0).formattedMessage()); + assertEquals("worker-1", logs.get(0).threadName()); + assertNotNull(logs.get(0).throwableText()); + } + + private StructuredLogBufferAppender.Configuration configuration(int maxEntries) { + return Converters.standardConverter() + .convert(Map.of("maxEntries", maxEntries)) + .to(StructuredLogBufferAppender.Configuration.class); + } +} diff --git a/src/test/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferTest.java b/src/test/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferTest.java new file mode 100644 index 0000000..d92efce --- /dev/null +++ b/src/test/java/org/apache/sling/mcp/server/impl/contribs/internal/StructuredLogBufferTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sling.mcp.server.impl.contribs.internal; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import ch.qos.logback.classic.Level; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StructuredLogBufferTest { + + @Test + void keepsOnlyNewestEntriesWithinCapacity() { + StructuredLogBuffer buffer = new StructuredLogBuffer(2); + + buffer.append(snapshot(1L, Level.INFO, "first")); + buffer.append(snapshot(2L, Level.INFO, "second")); + buffer.append(snapshot(3L, Level.INFO, "third")); + + List<LogSnapshot> logs = buffer.getRecent(null, Level.TRACE, 10); + assertEquals( + List.of("third", "second"), + logs.stream().map(LogSnapshot::formattedMessage).toList()); + } + + @Test + void filtersByLevelAndRegex() { + StructuredLogBuffer buffer = new StructuredLogBuffer(10); + + buffer.append(snapshot(1L, Level.DEBUG, "debug trace")); + buffer.append(snapshot(2L, Level.INFO, "first user ok")); + buffer.append(snapshot(3L, Level.ERROR, "first user failure")); + + List<LogSnapshot> logs = buffer.getRecent(Pattern.compile("first", Pattern.CASE_INSENSITIVE), Level.INFO, 10); + + assertEquals( + List.of("first user failure", "first user ok"), + logs.stream().map(LogSnapshot::formattedMessage).toList()); + } + + private LogSnapshot snapshot(long timeMillis, Level level, String message) { + return new LogSnapshot(timeMillis, level, "logger", "thread", message, null, Map.of()); + } +}
