diff --git a/citesphere/src/main/java/edu/asu/diging/citesphere/config/core/InMemoryAppender.java b/citesphere/src/main/java/edu/asu/diging/citesphere/config/core/InMemoryAppender.java new file mode 100644 index 000000000..282635fd0 --- /dev/null +++ b/citesphere/src/main/java/edu/asu/diging/citesphere/config/core/InMemoryAppender.java @@ -0,0 +1,65 @@ +package edu.asu.diging.citesphere.config.core; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.layout.PatternLayout; + +@Plugin(name = "InMemoryAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class InMemoryAppender extends AbstractAppender { + + // Thread‑safe buffer to hold events + private static final List BUFFER = Collections.synchronizedList(new ArrayList<>()); + + // Maximum number of events to keep + private static final int MAX_BUFFER_SIZE = 1000; + + protected InMemoryAppender(String name, Filter filter, Layout layout) { + super(name, filter, layout, false); + } + + @PluginFactory + public static InMemoryAppender createAppender( + @PluginAttribute("name") String name, + @PluginElement("Filter") Filter filter + ) { + if (name == null) { + LOGGER.error("No name provided for InMemoryAppender"); + return null; + } + // Default pattern layout if none provided + PatternLayout layout = PatternLayout.newBuilder() + .withPattern("%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n") + .build(); + return new InMemoryAppender(name, filter, layout); + } + + @Override + public void append(LogEvent event) { + // make the event immutable (optional but safer) + LogEvent immutable = event.toImmutable(); + BUFFER.add(immutable); + if (BUFFER.size() > MAX_BUFFER_SIZE) { + BUFFER.remove(0); + } + } + + public static List getEvents() { + synchronized (BUFFER) { + return new ArrayList<>(BUFFER); + } + } +} \ No newline at end of file diff --git a/citesphere/src/main/java/edu/asu/diging/citesphere/config/core/LoggingConfig.java b/citesphere/src/main/java/edu/asu/diging/citesphere/config/core/LoggingConfig.java new file mode 100644 index 000000000..6a0b0f587 --- /dev/null +++ b/citesphere/src/main/java/edu/asu/diging/citesphere/config/core/LoggingConfig.java @@ -0,0 +1,49 @@ +package edu.asu.diging.citesphere.config.core; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +//import org.apache.logging.log4j.core.filter.LoggerNameFilter; + +@Configuration +public class LoggingConfig { + + @Bean + public Appender inMemoryAppender() { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + org.apache.logging.log4j.core.config.Configuration config = ctx.getConfiguration(); + + InMemoryAppender appender = InMemoryAppender.createAppender("InMemoryAppender", null); + appender.start(); + config.addAppender(appender); + + Appender console = config.getAppender("Console"); + + LoggerConfig javersLogger = config.getLoggerConfig("org.javers.core.Javers"); + if (!"org.javers.core.Javers".equals(javersLogger.getName())) { + javersLogger = new LoggerConfig("org.javers.core.Javers", Level.INFO, false); + config.addLogger("org.javers.core.Javers", javersLogger); + } + + javersLogger.addAppender(appender, Level.INFO, null); + javersLogger.addAppender(console, Level.INFO, null); + + String asyncHandlerName = "org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"; + LoggerConfig asyncLogger = config.getLoggerConfig(asyncHandlerName); + if (!asyncLogger.getName().equals(asyncHandlerName)) { + asyncLogger = new LoggerConfig(asyncHandlerName, Level.INFO, false); + config.addLogger(asyncHandlerName, asyncLogger); + } + asyncLogger.addAppender(appender, Level.INFO, null); + asyncLogger.addAppender(console, Level.INFO, null); + + ctx.updateLoggers(); + + return appender; + } +} \ No newline at end of file diff --git a/citesphere/src/main/java/edu/asu/diging/citesphere/web/user/SyncInfoController.java b/citesphere/src/main/java/edu/asu/diging/citesphere/web/user/SyncInfoController.java index bae630b93..8e744ab4f 100644 --- a/citesphere/src/main/java/edu/asu/diging/citesphere/web/user/SyncInfoController.java +++ b/citesphere/src/main/java/edu/asu/diging/citesphere/web/user/SyncInfoController.java @@ -1,10 +1,15 @@ package edu.asu.diging.citesphere.web.user; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import edu.asu.diging.citesphere.config.core.InMemoryAppender; import edu.asu.diging.citesphere.core.model.jobs.impl.GroupSyncJob; import edu.asu.diging.citesphere.core.service.jobs.ISyncJobManager; @@ -25,6 +30,14 @@ public SyncInfo getSyncInfo(@PathVariable("zoteroGroupId") String groupId) { info.total = job.getTotal(); info.current = job.getCurrent(); info.status = job.getStatus() != null ? job.getStatus().name() : ""; + if(info.logs == null || (info.status == "PENDING")) { + info.logs = new ArrayList(); + } + info.logs.addAll(InMemoryAppender.getEvents().stream() + .map(ev -> + ev.getLevel() + " " + ev.getLoggerName() + + " - " + ev.getMessage().getFormattedMessage()) + .collect(Collectors.toList())); } return info; @@ -35,5 +48,6 @@ class SyncInfo { public long total; public long current; public String status; + public List logs; } } diff --git a/citesphere/src/main/webapp/WEB-INF/views/auth/group/items.html b/citesphere/src/main/webapp/WEB-INF/views/auth/group/items.html index 3965aeff8..0c8c20626 100644 --- a/citesphere/src/main/webapp/WEB-INF/views/auth/group/items.html +++ b/citesphere/src/main/webapp/WEB-INF/views/auth/group/items.html @@ -337,7 +337,7 @@

-
+
Up to date
@@ -374,6 +374,28 @@

$("#syncProgress").attr('aria-valuemax', data['total']); $("#syncProgress").addClass("progress-bar-striped active"); $("#syncText").text(Math.round(percent) + "% synced"); + const $logsList = $('#logs-list'); + $logsList.empty(); + if(data['logs'].length == 0) { + $('
  • ').text("No sync logs").appendTo($logsList); + } else { + data['logs'].forEach(log => { + const parts = log.split(' ', 2); + const level = parts[0] || ''; + const message = log.substring(level.length).trim(); + + const $li = $('
  • '); + const $strong = $('').text(level); + + if (level === 'ERROR') { + $strong.addClass('text-danger'); + } + + $li.append($strong).append(' ').append(document.createTextNode(message)); + + $logsList.append($li); + }); + } } setTimeout(pollStatus,1000); } else { @@ -383,11 +405,37 @@

    $("#syncProgress").removeClass("progress-bar-striped active"); $("#syncText").text("Up to date"); $("#table-spinner").hide(); + const $logsList = $('#logs-list'); + $logsList.empty(); + if(data['logs'].length == 0) { + $('
  • ').text("No sync logs").appendTo($logsList); + } else { + data['logs'].forEach(log => { + const parts = log.split(' ', 2); + const level = parts[0] || ''; + const message = log.substring(level.length).trim(); + + const $li = $('
  • '); + const $strong = $('').text(level); + + if (level === 'ERROR') { + $strong.addClass('text-danger'); + } + + $li.append($strong).append(' ').append(document.createTextNode(message)); + + $logsList.append($li); + }); + } } getCurrentItemsData("notFirstLoad"); }); } +function getlogs() { + $('#sync-logs').modal("show"); +} + function getCurrentItemsData(parameter){ let pageNumber = $(".page-item.active").text(); $.ajax({ @@ -825,6 +873,30 @@
  • + +