JCR 中的附加文件

Appendable files in JCR

我将要 运行 一个可能需要几分钟甚至几小时的过程。为了跟踪此类 运行 的历史记录,我根据 运行 创建了一个自定义类型的节点,其中存储了相关的流程元数据。另外我想在这样的节点下存储日志文件。这似乎是更一致和方便的方法,而不是将日志文件与进程元分开存储在磁盘上。

现在 nt:file 节点类型本身有一个带有 jcr:data 属性 的 jcr:content 子节点,允许我存储二进制内容。这适用于一次性或不经常更改文件内容。

不过,我将不断向该文件追加新内容,除此之外,还会在单独的线程中轮询其内容(以跟踪进度)。

面对 javax.jcr.ValueFactoryjavax.jcr.Binary 的 JCR API 似乎并不真正支持这种方法,我宁愿被迫覆盖该文件(或更准确地说- 二进制 属性) 每次我添加一行日志时一遍又一遍。我很关心性能。

我在文档中搜索了允许我打开该文件的输出流并定期将更改从该流刷新到 JCR 的工具,但似乎没有可用的工具。

那么有什么比使用普通 javax.jcr.ValueFactoryjavax.jcr.Binary 更聪明的方法了吗?

考虑了一段时间后,我这里有所有选项:

  1. 将日志保存在内存中,每次用户调用时将它们保存到 CRX info/warn/error优点: 日志与迁移任务元数据存储在同一位置,易于查找和访问。 缺点: 在大量日志条目的情况下,可能是所有方法中最慢且资源效率最低的方法。

  2. 将日志保留在内存中,仅在迁移结束时将它们保存到 JCR。 优点: 重构当前解决方案简单,迁移过程中对 CRX 的压力较小。 缺点: 无法跟踪实时进度,在意外错误或实例关闭期间可能会丢失日志。

  3. 为每个日志条目而不是 log.txt 创建一个自定义类型的节点。通过特殊的日志 servlet 在文本文件中聚合日志。即 /var/migration/uuid/log.txt/var/migration/uuid/log.json优点: 更多的 JCR 方式来存储此类内容。使用自定义节点类型和索引应该足够快,可以考虑作为一种选择。具有多样性以支持日志的文本和 json 格式。 缺点: 与当前方法的性能比较不明确。由于位于同一级别的大量节点而导致的潜在问题。用户应该知道日志 servlet 的存在,否则用户无法以方便的格式查看它们。在大量日志条目的情况下,日志 servlet 性能不明确。

  4. 在文件系统上创建日志文件(假设在 crx-quickstart/logs/migration/<uuid>.log),通过 API 显示其内容(如果需要),能够 t运行kate log API 对最后 100-1000 行的响应。 优点: 当日志文件存储在文件系统上时,经典且众所周知的日志方法。 Sling 提供已配置的绑定 slf4jLogBack,并导出所有需要的 LogBack 依赖项以供在自定义包中使用。 缺点: 日志和任务元数据的分离。用户应该知道日志文件在磁盘上的位置。

选项 1 开始,然后我意识到日志条目的数量可能会扩展到数十万 - 罕见但可能发生的情况。所以最后决定使用 选项 4.

如果有人会面临类似的任务,我将在此处发布选项 4 的实施细节,因为它并不像一开始看起来那么微不足道。

我正在使用 AEM 6.2(引擎盖下的 Felix-Jackrabbit-Sling)并且我希望每个迁移任务 运行 - 这实际上只是一个单独的线程 - 创建它自己的日志文件特殊名称 - 该迁移过程的唯一标识符。

现在,Sling 本身允许您通过 org.apache.sling.commons.log.LogManager.factory.config OSGi 配置定义多个日志配置。然而,这些日志配置对于这种情况来说太简单了——你不能用它创建在 LogBack 中所谓的 SiftingAppender——日志附加器的特殊情况,它将为特定记录器实例化附加器每个线程,而不是一次和整个应用程序 - 换句话说,您不能指示 LogBack 使用 OSGi 配置为每个线程创建文件。

所以从逻辑上讲,您会希望在 运行 时间以编程方式获取 Sling 的 LogBack 配置(例如,在您上传自定义包并激活它的那一刻),并使用它来配置这样的附加程序特定的记录器。不幸的是,虽然有很多关于如何通过 logback.xml 配置 LogBack 的文档,但很少有文档描述如何通过 LogBack 的 Java 对象(如 ch.qos.logback.classic.LoggerContext 等)以编程方式进行配置其中零个解释了如何配置 SiftingAppender.

所以在阅读了 LogBack 源代码和测试之后,我得到了这个助手 class:

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.sift.MDCBasedDiscriminator;
import ch.qos.logback.classic.sift.SiftingAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.sift.AppenderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.Objects;

/**
 * This class dynamically adds configuration to AEM's LogBack logging implementation behind slf4j.
 * The point is to provide loggers bound to specific task ID and therefore specific log file, so
 * each migration task run will be written in it's standalone log file.
 * */
public class LogUtil {

    static {
        LoggerContext rootContext = (LoggerContext) LoggerFactory.getILoggerFactory();

        ch.qos.logback.classic.Logger logger = rootContext.getLogger("migration-logger");

        //since appender lives until AEM instance restarted
        //we are checking if appender had being registered previously 
        //to ensure we won't do it more than once
        if(logger.getAppender("MIGRATION-TASK-SIFT") == null) {
            MDCBasedDiscriminator mdcBasedDiscriminator = new MDCBasedDiscriminator();
            mdcBasedDiscriminator.setContext(rootContext);
            mdcBasedDiscriminator.setKey("taskId");
            mdcBasedDiscriminator.setDefaultValue("no-task-id");
            mdcBasedDiscriminator.start();

            SiftingAppender siftingAppender = new SiftingAppender();
            siftingAppender.setContext(rootContext);
            siftingAppender.setName("MIGRATION-TASK-SIFT");
            siftingAppender.setDiscriminator(mdcBasedDiscriminator);
            siftingAppender.setAppenderFactory(new FileAppenderFactory());
            siftingAppender.start();

            logger.setAdditive(false);
            logger.setLevel(ch.qos.logback.classic.Level.ALL);
            logger.addAppender(siftingAppender);
        }
    }

    public static class FileAppenderFactory implements AppenderFactory<ILoggingEvent> {

        @Override
        public Appender<ILoggingEvent> buildAppender(Context context, String taskId) throws JoranException {
            PatternLayoutEncoder logEncoder = new PatternLayoutEncoder();
            logEncoder.setContext(context);
            logEncoder.setPattern("%-12date{YYYY-MM-dd HH:mm:ss.SSS} %-5level - %msg%n");
            logEncoder.start();

            FileAppender<ILoggingEvent> appender = new FileAppender<>();
            appender.setContext(context);
            appender.setName("migration-log-file");
            appender.setFile("crx-quickstart/logs/migration/task-" + taskId + ".log");
            appender.setEncoder(logEncoder);
            appender.setAppend(true);
            appender.start();

            //need to add cleanup configuration for old logs ?

            return appender;
        }
    }

    private LogUtil(){
    }

    public static Logger getTaskLogger(String taskId) {
        Objects.requireNonNull(taskId);
        MDC.put("taskId", taskId);
        return LoggerFactory.getLogger("migration-logger");
    }

    public static void releaseTaskLogger() {
        MDC.remove("taskId");
    }
}

要注意的部分是 SiftingAppender 要求您实现 AppenderFactory 接口,这将为每个线程的记录器生成配置的附加程序。

现在您可以通过以下方式获取记录器:

LogUtil.getTaskLogger("some-task-uuid")

并根据提供的 taskId

使用它创建 crq-quickstart/logs/migration/task-<taskId>.log 之类的日志文件

根据文档,您还需要在完成使用后释放此类记录器

LogUtil.releaseTaskLogger()

差不多就是这样。