Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
14 changes: 4 additions & 10 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,6 @@ DynamicImport-Package: *
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.event</artifactId>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.log</artifactId>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.framework</artifactId>
Expand All @@ -172,12 +168,10 @@ DynamicImport-Package: *
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.commons.classloader</artifactId>
<version>1.4.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.jcr</groupId>
Expand Down
85 changes: 36 additions & 49 deletions core/src/main/java/dev/vml/es/acm/core/code/CodePrintStream.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package dev.vml.es.acm.core.code;

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 dev.vml.es.acm.core.AcmException;
import dev.vml.es.acm.core.code.log.LogInterceptor;
import dev.vml.es.acm.core.code.log.LogInterceptorManager;
import dev.vml.es.acm.core.code.log.LogMessage;
import java.io.OutputStream;
import java.io.PrintStream;
import java.time.Instant;
Expand All @@ -15,6 +14,7 @@
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
Expand All @@ -32,84 +32,71 @@ public class CodePrintStream extends PrintStream {

private final Logger logger;

private final LoggerContext loggerContext;
private final LogInterceptorManager logInterceptorManager;

private final Set<String> loggerNames;

private boolean loggerTimestamps;

private final LogAppender logAppender;
private LogInterceptor.Handle interceptorHandle;

private boolean printerTimestamps;

public CodePrintStream(OutputStream output, String id) {
public CodePrintStream(OutputStream output, String id, LogInterceptorManager logInterceptorManager) {
super(output);

this.loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
this.logInterceptorManager = logInterceptorManager;
this.loggerNames = new HashSet<>();
this.loggerTimestamps = true;
this.logger = loggerContext.getLogger(id);
this.logAppender = new LogAppender();

this.logger = LoggerFactory.getLogger(id);
this.printerTimestamps = true;
}

private class LogAppender extends AppenderBase<ILoggingEvent> {
@Override
protected void append(ILoggingEvent event) {
String loggerName = event.getLoggerName();
for (String loggerPrefix : loggerNames) {
if (StringUtils.startsWith(loggerName, loggerPrefix)) {
String level = event.getLevel().toString();
if (loggerTimestamps) {
LocalDateTime eventTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(event.getTimeStamp()), ZoneId.systemDefault());
String timestamp = eventTime.format(TIMESTAMP_FORMATTER);
println(timestamp + " [" + level + "] " + event.getFormattedMessage());
} else {
println('[' + level + "] " + event.getFormattedMessage());
}
break;
}
}
}
}

public void fromLogger(String loggerName) {
if (StringUtils.isBlank(loggerName)) {
throw new AcmException("Logger name cannot be blank!");
}
enableAppender();
loggerNames.add(loggerName);
updateInterceptor();
}

@Override
public void close() {
disableAppender();
detachInterceptor();
super.close();
}

private void enableAppender() {
if (logAppender.isStarted()) {
return;
private void updateInterceptor() {
detachInterceptor();
if (!loggerNames.isEmpty() && logInterceptorManager != null) {
interceptorHandle =
logInterceptorManager.attach(this::handleLogMessage, loggerNames.toArray(new String[0]));
}
Logger rootLogger = getRootLogger();
rootLogger.addAppender(logAppender);
logAppender.setContext(loggerContext);
logAppender.start();
}

private void disableAppender() {
if (!logAppender.isStarted()) {
return;
private void detachInterceptor() {
if (interceptorHandle != null) {
interceptorHandle.detach();
interceptorHandle = null;
}
logAppender.stop();
Logger rootLogger = getRootLogger();
rootLogger.detachAppender(logAppender);
}

private Logger getRootLogger() {
return loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
private void handleLogMessage(LogMessage event) {
String loggerName = event.getLoggerName();
for (String loggerPrefix : loggerNames) {
if (StringUtils.startsWith(loggerName, loggerPrefix)) {
String level = event.getLevel();
if (loggerTimestamps) {
LocalDateTime eventTime =
LocalDateTime.ofInstant(Instant.ofEpochMilli(event.getTimestamp()), ZoneId.systemDefault());
String timestamp = eventTime.format(TIMESTAMP_FORMATTER);
println(timestamp + " [" + level + "] " + event.getMessage());
} else {
println('[' + level + "] " + event.getMessage());
}
break;
}
}
}

public Logger getLogger() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.vml.es.acm.core.code;

import dev.vml.es.acm.core.AcmConstants;
import dev.vml.es.acm.core.code.log.LogInterceptorManager;
import dev.vml.es.acm.core.repo.Repo;
import dev.vml.es.acm.core.util.ResolverUtils;
import groovy.lang.Binding;
Expand Down Expand Up @@ -70,7 +71,13 @@ public ExecutionContext(
this.inputValues = inputValues;
this.codeContext = codeContext;
this.codeOutput = codeOutput;
this.printStream = new CodePrintStream(codeOutput.write(), String.format("%s|%s", executable.getId(), id));
this.printStream = new CodePrintStream(
codeOutput.write(),
String.format("%s|%s", executable.getId(), id),
codeContext
.getOsgiContext()
.findService(LogInterceptorManager.class)
.orElse(null));
this.schedules = new Schedules();
this.conditions = new Conditions(this);
this.inputs = new Inputs();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.vml.es.acm.core.code.log;

import java.util.function.Consumer;

/**
* Intercepts log messages from specified loggers for automatic capture in script output.
*
* <p>Opt-in feature controlled by {@link dev.vml.es.acm.core.code.Executor.Config#logPrintingEnabled()}.
* Scripts can always log manually via SLF4J regardless of this setting.</p>
*/
public interface LogInterceptor {

boolean isAvailable();

Handle attach(Consumer<LogMessage> listener, String... loggerNames);

@FunctionalInterface
interface Handle {
void detach();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package dev.vml.es.acm.core.code.log;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import org.osgi.service.component.annotations.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Selects the best available {@link LogInterceptor} implementation.
*
* <p><strong>Why not OSGi Log Service 1.4?</strong> Its {@code LogReaderService} only captures
* logs sent directly to OSGi Log Service. SLF4J/Logback logs are bridged one-way (SLF4J → OSGi),
* so {@code LogReaderService} doesn't receive them. We use Sling AppenderTracker instead.</p>
*
* @see SlingLogInterceptor
*/
@Component(service = LogInterceptorManager.class)
public class LogInterceptorManager {

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

private final List<LogInterceptor> interceptors = new CopyOnWriteArrayList<>();

@Reference(
service = LogInterceptor.class,
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
protected void bindInterceptor(LogInterceptor interceptor) {
interceptors.add(interceptor);
}

protected void unbindInterceptor(LogInterceptor interceptor) {
interceptors.remove(interceptor);
}

public LogInterceptor.Handle attach(Consumer<LogMessage> listener, String... loggerNames) {
return interceptors.stream()
.filter(LogInterceptor::isAvailable)
.findFirst()
.map(i -> {
LOG.debug("Using log interceptor: {}", i.getClass().getSimpleName());
return i.attach(listener, loggerNames);
})
.orElseGet(() -> {
LOG.warn("No log interceptor available");
return () -> {};
});
}

public boolean isAvailable() {
return interceptors.stream().anyMatch(LogInterceptor::isAvailable);
}
}
35 changes: 35 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/code/log/LogMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.vml.es.acm.core.code.log;

public class LogMessage {

private final String loggerName;

private final String level;

private final String message;

private final long timestamp;

public LogMessage(String loggerName, String level, String message, long timestamp) {
this.loggerName = loggerName;
this.level = level;
this.message = message;
this.timestamp = timestamp;
}

public String getLoggerName() {
return loggerName;
}

public String getLevel() {
return level;
}

public String getMessage() {
return message;
}

public long getTimestamp() {
return timestamp;
}
}
Loading