This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch issue/SLING-13188 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-commons-log.git
commit 298f115dfe7a9ee2add7057decd9e396054004e1 Author: Robert Munteanu <[email protected]> AuthorDate: Tue May 19 17:40:20 2026 +0200 SLING-13188 - Provide an optional in-memory store of recent log events Expose a simple APIs to retrieve logging events that are stored in memory. The needed components are disabled by default unless configured. --- .../log/logback/internal/LogConfigManager.java | 6 + .../logback/internal/store/LogStoreAppender.java | 134 +++++++++++++++++++++ .../log/logback/internal/store/LogStoreImpl.java | 121 +++++++++++++++++++ .../logback/internal/store/LogStoreRegistrar.java | 126 +++++++++++++++++++ .../sling/commons/log/logback/store/LogEntry.java | 116 ++++++++++++++++++ .../sling/commons/log/logback/store/LogLevel.java | 30 +++++ .../sling/commons/log/logback/store/LogStore.java | 33 +++++ .../commons/log/logback/store/package-info.java | 31 +++++ .../integration/ITLogStoreRegistrarLifecycle.java | 85 +++++++++++++ .../internal/store/LogStoreAppenderTest.java | 62 ++++++++++ .../logback/internal/store/LogStoreImplTest.java | 91 ++++++++++++++ 11 files changed, 835 insertions(+) diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/LogConfigManager.java b/src/main/java/org/apache/sling/commons/log/logback/internal/LogConfigManager.java index 5131513..998c27e 100644 --- a/src/main/java/org/apache/sling/commons/log/logback/internal/LogConfigManager.java +++ b/src/main/java/org/apache/sling/commons/log/logback/internal/LogConfigManager.java @@ -63,6 +63,7 @@ import ch.qos.logback.core.status.StatusListener; import ch.qos.logback.core.status.StatusListenerAsList; import ch.qos.logback.core.status.StatusUtil; import org.apache.sling.commons.log.logback.internal.AppenderTracker.AppenderInfo; +import org.apache.sling.commons.log.logback.internal.store.LogStoreRegistrar; import org.apache.sling.commons.log.logback.internal.config.ConfigAdminSupport; import org.apache.sling.commons.log.logback.internal.config.ConfigurationException; import org.apache.sling.commons.log.logback.internal.joran.JoranConfiguratorWrapper; @@ -145,6 +146,8 @@ public class LogConfigManager extends LoggerContextAwareBase */ private final ConfigAdminSupport configAdminSupport; + private final LogStoreRegistrar logStoreRegistrar; + /** * The logger for this class */ @@ -306,6 +309,7 @@ public class LogConfigManager extends LoggerContextAwareBase this.osgiIntegrationListener = new OsgiIntegrationListener(this); this.configAdminSupport = new ConfigAdminSupport(); + this.logStoreRegistrar = new LogStoreRegistrar(); } /** @@ -319,6 +323,7 @@ public class LogConfigManager extends LoggerContextAwareBase bridgeHandlerInstalled = maybeInstallSlf4jBridgeHandler(bundleContext); configAdminSupport.start(bundleContext, this); + logStoreRegistrar.start(bundleContext); // enable the LevelChangePropagator during any reset // http://logback.qos.ch/manual/configuration.html#LevelChangePropagator @@ -386,6 +391,7 @@ public class LogConfigManager extends LoggerContextAwareBase loggerContext.removeListener(osgiIntegrationListener); configAdminSupport.stop(); + logStoreRegistrar.stop(); for (ServiceTracker<?, ?> tracker : serviceTrackers) { tracker.close(); diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreAppender.java b/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreAppender.java new file mode 100644 index 0000000..c22f5d4 --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreAppender.java @@ -0,0 +1,134 @@ +/* + * 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.commons.log.logback.internal.store; + +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.AppenderBase; +import org.apache.sling.commons.log.logback.store.LogEntry; +import org.apache.sling.commons.log.logback.store.LogLevel; + +public class LogStoreAppender extends AppenderBase<ILoggingEvent> { + + public static final String APPENDER_NAME = "structured-log-store"; + + private final LogStoreImpl store; + + public LogStoreAppender(LogStoreImpl store) { + this.store = store; + setName(APPENDER_NAME); + } + + @Override + protected void append(ILoggingEvent eventObject) { + if (eventObject == null) { + return; + } + + LogLevel logLevel = getLogLevel(eventObject); + if (logLevel == null) { + return; + } + + store.append(new LogEntry( + eventObject.getTimeStamp(), + logLevel, + eventObject.getLoggerName(), + eventObject.getThreadName(), + eventObject.getFormattedMessage(), + getThrowableText(eventObject), + eventObject.getMDCPropertyMap())); + } + + private LogLevel getLogLevel(ILoggingEvent eventObject) { + switch (eventObject.getLevel().levelInt) { + case ch.qos.logback.classic.Level.TRACE_INT: + return LogLevel.TRACE; + case ch.qos.logback.classic.Level.DEBUG_INT: + return LogLevel.DEBUG; + case ch.qos.logback.classic.Level.INFO_INT: + return LogLevel.INFO; + case ch.qos.logback.classic.Level.WARN_INT: + return LogLevel.WARN; + case ch.qos.logback.classic.Level.ERROR_INT: + return LogLevel.ERROR; + default: + return null; + } + } + + 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 = throwableProxy.getOverridingMessage(); + 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(); + } +} diff --git a/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreImpl.java b/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreImpl.java new file mode 100644 index 0000000..99493c7 --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreImpl.java @@ -0,0 +1,121 @@ +/* + * 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.commons.log.logback.internal.store; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.apache.sling.commons.log.logback.store.LogEntry; +import org.apache.sling.commons.log.logback.store.LogLevel; +import org.apache.sling.commons.log.logback.store.LogStore; + +public class LogStoreImpl implements LogStore { + + static final int DEFAULT_MAX_ENTRIES = 10000; + + private final Object lock = new Object(); + private final Deque<LogEntry> entries = new ArrayDeque<>(); + private int maxEntriesKept; + + public LogStoreImpl() { + this(DEFAULT_MAX_ENTRIES); + } + + public LogStoreImpl(int maxEntriesKept) { + this.maxEntriesKept = Math.max(1, maxEntriesKept); + } + + public void append(LogEntry snapshot) { + synchronized (lock) { + entries.addLast(snapshot); + trimToSize(); + } + } + + @Override + public List<LogEntry> getRecent(Pattern pattern, LogLevel minLevel, int maxEntries) { + LogLevel effectiveMinLevel = minLevel == null ? LogLevel.TRACE : minLevel; + + synchronized (lock) { + List<LogEntry> matches = new ArrayList<>(); + int remaining = Math.max(1, maxEntries); + + for (Iterator<LogEntry> iterator = entries.descendingIterator(); + iterator.hasNext() && remaining > 0; ) { + LogEntry snapshot = iterator.next(); + if (!matches(snapshot, pattern, effectiveMinLevel)) { + continue; + } + matches.add(snapshot); + remaining--; + } + + return matches; + } + } + + public void setMaxEntries(int maxEntriesKept) { + synchronized (lock) { + this.maxEntriesKept = Math.max(1, maxEntriesKept); + trimToSize(); + } + } + + private boolean matches(LogEntry snapshot, Pattern pattern, LogLevel minLevel) { + if (snapshot.level().ordinal() >= minLevel.ordinal()) { + if (pattern == null) { + return true; + } + return matchesField(pattern, snapshot.level().name()) + || 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, LogEntry snapshot) { + if (snapshot.mdc().isEmpty()) { + return false; + } + for (Map.Entry<String, String> 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/commons/log/logback/internal/store/LogStoreRegistrar.java b/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreRegistrar.java new file mode 100644 index 0000000..570c8f5 --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/internal/store/LogStoreRegistrar.java @@ -0,0 +1,126 @@ +/* + * 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.commons.log.logback.internal.store; + +import java.util.Dictionary; +import java.util.Hashtable; + +import ch.qos.logback.core.Appender; +import org.apache.sling.commons.log.logback.internal.LogConstants; +import org.apache.sling.commons.log.logback.store.LogStore; +import org.jetbrains.annotations.Nullable; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.cm.ManagedService; + +public class LogStoreRegistrar { + + static final String PID = "org.apache.sling.commons.log.LogStore"; + static final String PROP_MAX_ENTRIES = "maxEntries"; + + private ServiceRegistration<LogStore> storeRegistration; + private ServiceRegistration<Appender> appenderRegistration; + private ServiceRegistration<ManagedService> configRegistration; + private BundleContext bundleContext; + private LogStoreImpl store; + private LogStoreAppender appender; + + public void start(BundleContext context) { + this.bundleContext = context; + + Dictionary<String, Object> configProps = new Hashtable<>(); + configProps.put(Constants.SERVICE_VENDOR, LogConstants.ASF_SERVICE_VENDOR); + configProps.put(Constants.SERVICE_DESCRIPTION, "Log Store Configurator"); + configProps.put(Constants.SERVICE_PID, PID); + configRegistration = context.registerService(ManagedService.class, this::updated, configProps); + } + + public void stop() { + deactivate(); + + if (configRegistration != null) { + configRegistration.unregister(); + configRegistration = null; + } + bundleContext = null; + } + + void updated(@Nullable Dictionary<String, ?> properties) { + if (properties == null) { + deactivate(); + return; + } + + if (store == null) { + activate(); + } + + store.setMaxEntries(getMaxEntries(properties)); + } + + private void activate() { + if (bundleContext == null || store != null) { + return; + } + + store = new LogStoreImpl(); + appender = new LogStoreAppender(store); + + Dictionary<String, Object> serviceProps = new Hashtable<>(); + serviceProps.put(Constants.SERVICE_VENDOR, LogConstants.ASF_SERVICE_VENDOR); + serviceProps.put(Constants.SERVICE_DESCRIPTION, "Log Store"); + storeRegistration = bundleContext.registerService(LogStore.class, store, serviceProps); + + Dictionary<String, Object> appenderProps = new Hashtable<>(); + appenderProps.put(Constants.SERVICE_VENDOR, LogConstants.ASF_SERVICE_VENDOR); + appenderProps.put(Constants.SERVICE_DESCRIPTION, "Log Store Appender"); + appenderProps.put("loggers", "ROOT"); + appenderRegistration = bundleContext.registerService(Appender.class, appender, appenderProps); + } + + private void deactivate() { + if (appenderRegistration != null) { + appenderRegistration.unregister(); + appenderRegistration = null; + } + if (storeRegistration != null) { + storeRegistration.unregister(); + storeRegistration = null; + } + + appender = null; + store = null; + } + + private int getMaxEntries(Dictionary<String, ?> properties) { + Object value = properties.get(PROP_MAX_ENTRIES); + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof String) { + try { + return Integer.parseInt((String) value); + } catch (NumberFormatException e) { + return LogStoreImpl.DEFAULT_MAX_ENTRIES; + } + } + return LogStoreImpl.DEFAULT_MAX_ENTRIES; + } +} diff --git a/src/main/java/org/apache/sling/commons/log/logback/store/LogEntry.java b/src/main/java/org/apache/sling/commons/log/logback/store/LogEntry.java new file mode 100644 index 0000000..7b50769 --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/store/LogEntry.java @@ -0,0 +1,116 @@ +/* + * 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.commons.log.logback.store; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Snapshot of a log entry. + * + * <p>Stores only the lightweight, stable parts of a log event so the log store + * does not retain full logging event object graphs.</p> + */ +public final class LogEntry { + + private final long timeMillis; + private final LogLevel level; + private final String loggerName; + private final String threadName; + private final String formattedMessage; + private final String throwableText; + private final Map<String, String> mdc; + + public LogEntry( + long timeMillis, + LogLevel level, + String loggerName, + String threadName, + String formattedMessage, + String throwableText, + Map<String, String> mdc) { + this.timeMillis = timeMillis; + this.level = level; + this.loggerName = loggerName; + this.threadName = threadName; + this.formattedMessage = formattedMessage; + this.throwableText = throwableText; + this.mdc = mdc.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(mdc)); + } + + public long timeMillis() { + return timeMillis; + } + + public LogLevel level() { + return level; + } + + public String loggerName() { + return loggerName; + } + + public String threadName() { + return threadName; + } + + public String formattedMessage() { + return formattedMessage; + } + + public String throwableText() { + return throwableText; + } + + public Map<String, String> mdc() { + return mdc; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LogEntry)) { + return false; + } + LogEntry that = (LogEntry) other; + return timeMillis == that.timeMillis + && Objects.equals(level, that.level) + && Objects.equals(loggerName, that.loggerName) + && Objects.equals(threadName, that.threadName) + && Objects.equals(formattedMessage, that.formattedMessage) + && Objects.equals(throwableText, that.throwableText) + && Objects.equals(mdc, that.mdc); + } + + @Override + public int hashCode() { + return Objects.hash(timeMillis, level, loggerName, threadName, formattedMessage, throwableText, mdc); + } + + @Override + public String toString() { + return "LogEntry{" + "timeMillis=" + timeMillis + ", level='" + level + '\'' + ", loggerName='" + + loggerName + '\'' + ", threadName='" + threadName + '\'' + ", formattedMessage='" + + formattedMessage + '\'' + ", throwableText='" + throwableText + '\'' + ", mdc=" + mdc + '}'; + } +} diff --git a/src/main/java/org/apache/sling/commons/log/logback/store/LogLevel.java b/src/main/java/org/apache/sling/commons/log/logback/store/LogLevel.java new file mode 100644 index 0000000..d4590c5 --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/store/LogLevel.java @@ -0,0 +1,30 @@ +/* + * 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.commons.log.logback.store; + +/** + * Severity levels supported by the log store. + */ +public enum LogLevel { + TRACE, + DEBUG, + INFO, + WARN, + ERROR; +} diff --git a/src/main/java/org/apache/sling/commons/log/logback/store/LogStore.java b/src/main/java/org/apache/sling/commons/log/logback/store/LogStore.java new file mode 100644 index 0000000..8c6b99a --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/store/LogStore.java @@ -0,0 +1,33 @@ +/* + * 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.commons.log.logback.store; + +import java.util.List; +import java.util.regex.Pattern; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Queriable store of structured log entries. + */ +@ProviderType +public interface LogStore { + + List<LogEntry> getRecent(Pattern pattern, LogLevel minLevel, int maxEntries); +} diff --git a/src/main/java/org/apache/sling/commons/log/logback/store/package-info.java b/src/main/java/org/apache/sling/commons/log/logback/store/package-info.java new file mode 100644 index 0000000..a37710d --- /dev/null +++ b/src/main/java/org/apache/sling/commons/log/logback/store/package-info.java @@ -0,0 +1,31 @@ +/* + * 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. + */ + +/** + * Provides an in-memory log store for logging events. + * + * <p>The API is intentionally decoupled from slf4j and logback to prevent binding to those APIs + * directly, which hinders evolution and is problematic when upgrading dependency versions.</p> + * + * @version 1.0 + */ +@Version("1.0.0") +package org.apache.sling.commons.log.logback.store; + +import org.osgi.annotation.versioning.Version; diff --git a/src/test/java/org/apache/sling/commons/log/logback/integration/ITLogStoreRegistrarLifecycle.java b/src/test/java/org/apache/sling/commons/log/logback/integration/ITLogStoreRegistrarLifecycle.java new file mode 100644 index 0000000..13b09a0 --- /dev/null +++ b/src/test/java/org/apache/sling/commons/log/logback/integration/ITLogStoreRegistrarLifecycle.java @@ -0,0 +1,85 @@ +/* + * 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.commons.log.logback.integration; + +import javax.inject.Inject; + +import java.util.Dictionary; +import java.util.Hashtable; + +import ch.qos.logback.core.Appender; +import org.apache.sling.commons.log.logback.store.LogStore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.Option; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerClass; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +import static org.junit.Assert.assertEquals; +import static org.ops4j.pax.exam.CoreOptions.composite; +import static org.ops4j.pax.exam.CoreOptions.mavenBundle; + +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerClass.class) +public class ITLogStoreRegistrarLifecycle extends LogTestBase { + + private static final String LOG_STORE_PID = "org.apache.sling.commons.log.LogStore"; + private static final String MAX_ENTRIES = "maxEntries"; + + @Inject + private ConfigurationAdmin ca; + + @Override + protected Option addExtraOptions() { + return composite(configAdmin(), mavenBundle("commons-io", "commons-io").versionAsInProject()); + } + + @Test + public void testLifecycle() throws Exception { + Configuration config = ca.getConfiguration(LOG_STORE_PID, null); + try { + assertEquals(0, bundleContext.getServiceReferences(LogStore.class, null).size()); + assertEquals(0, bundleContext.getServiceReferences(Appender.class, null).size()); + + Dictionary<String, Object> properties = new Hashtable<String, Object>(); + properties.put(MAX_ENTRIES, 5); + config.update(properties); + delay(); + + assertEquals(1, bundleContext.getServiceReferences(LogStore.class, null).size()); + assertEquals(1, bundleContext.getServiceReferences(Appender.class, null).size()); + + properties.put(MAX_ENTRIES, 7); + config.update(properties); + delay(); + + assertEquals(1, bundleContext.getServiceReferences(LogStore.class, null).size()); + assertEquals(1, bundleContext.getServiceReferences(Appender.class, null).size()); + } finally { + config.delete(); + delay(); + } + + assertEquals(0, bundleContext.getServiceReferences(LogStore.class, null).size()); + assertEquals(0, bundleContext.getServiceReferences(Appender.class, null).size()); + } +} diff --git a/src/test/java/org/apache/sling/commons/log/logback/internal/store/LogStoreAppenderTest.java b/src/test/java/org/apache/sling/commons/log/logback/internal/store/LogStoreAppenderTest.java new file mode 100644 index 0000000..10ba978 --- /dev/null +++ b/src/test/java/org/apache/sling/commons/log/logback/internal/store/LogStoreAppenderTest.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.commons.log.logback.internal.store; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; +import java.util.Map; + +import org.apache.sling.commons.log.logback.store.LogEntry; +import org.apache.sling.commons.log.logback.store.LogLevel; +import org.junit.jupiter.api.Test; + +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; + +class LogStoreAppenderTest { + + @Test + void appenderSnapshotsFormattedMessageAndThrowable() { + LogStoreImpl store = new LogStoreImpl(5); + LogStoreAppender appender = new LogStoreAppender(store); + + 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(Map.of("requestId", "123")); + event.setThreadName("worker-1"); + + appender.append(event); + + List<LogEntry> logs = store.getRecent(null, LogLevel.TRACE, 10); + assertEquals(1, logs.size()); + assertEquals("message", logs.get(0).formattedMessage()); + assertEquals("worker-1", logs.get(0).threadName()); + assertEquals(LogLevel.ERROR, logs.get(0).level()); + assertEquals(Map.of("requestId", "123"), logs.get(0).mdc()); + assertNotNull(logs.get(0).throwableText()); + } + +} diff --git a/src/test/java/org/apache/sling/commons/log/logback/internal/store/LogStoreImplTest.java b/src/test/java/org/apache/sling/commons/log/logback/internal/store/LogStoreImplTest.java new file mode 100644 index 0000000..86db938 --- /dev/null +++ b/src/test/java/org/apache/sling/commons/log/logback/internal/store/LogStoreImplTest.java @@ -0,0 +1,91 @@ +/* + * 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.commons.log.logback.internal.store; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.sling.commons.log.logback.store.LogEntry; +import org.apache.sling.commons.log.logback.store.LogLevel; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class LogStoreImplTest { + + @Test + void keepsOnlyNewestEntriesWithinCapacity() { + LogStoreImpl store = new LogStoreImpl(2); + + store.append(logEntry(1L, LogLevel.INFO, "first")); + store.append(logEntry(2L, LogLevel.INFO, "second")); + store.append(logEntry(3L, LogLevel.INFO, "third")); + + List<LogEntry> logs = store.getRecent(null, LogLevel.TRACE, 10); + assertEquals( + List.of("third", "second"), + logs.stream().map(LogEntry::formattedMessage).collect(Collectors.toList())); + } + + @Test + void filtersByLevelAndRegex() { + LogStoreImpl store = new LogStoreImpl(10); + + store.append(logEntry(1L, LogLevel.DEBUG, "debug trace")); + store.append(logEntry(2L, LogLevel.INFO, "first user ok")); + store.append(logEntry(3L, LogLevel.ERROR, "first user failure")); + + List<LogEntry> logs = store.getRecent(Pattern.compile("first", Pattern.CASE_INSENSITIVE), LogLevel.INFO, 10); + + assertEquals( + List.of("first user failure", "first user ok"), + logs.stream().map(LogEntry::formattedMessage).collect(Collectors.toList())); + } + + @Test + void defaultsToTraceWhenMinLevelIsNull() { + LogStoreImpl store = new LogStoreImpl(10); + store.append(logEntry(1L, LogLevel.DEBUG, "debug trace")); + + List<LogEntry> logs = store.getRecent(null, null, 10); + + assertEquals(List.of("debug trace"), logs.stream().map(LogEntry::formattedMessage).collect(Collectors.toList())); + } + + @Test + void shrinksStoreWhenMaxEntriesIsLowered() { + LogStoreImpl store = new LogStoreImpl(3); + + store.append(logEntry(1L, LogLevel.INFO, "first")); + store.append(logEntry(2L, LogLevel.INFO, "second")); + store.append(logEntry(3L, LogLevel.INFO, "third")); + store.setMaxEntries(2); + + List<LogEntry> logs = store.getRecent(null, LogLevel.TRACE, 10); + assertEquals( + List.of("third", "second"), + logs.stream().map(LogEntry::formattedMessage).collect(Collectors.toList())); + } + + private LogEntry logEntry(long timeMillis, LogLevel level, String message) { + return new LogEntry(timeMillis, level, "logger", "thread", message, null, Map.of()); + } +}
