Добавляемые файлы в JCR
Я собираюсь запустить процесс, который может занять минуты или даже часы. Чтобы отслеживать историю таких запусков, я создаю узел пользовательского типа для каждого прогона с соответствующими метаданными процесса, хранящимися в нем. Дополнительно я хочу хранить файл журнала под таким узлом. Это кажется более последовательным и удобным подходом, а не хранит файл журнала на диске отдельно от мета-процесса.
Сейчас nt:file
Сам тип узла имеет jcr:content
подузел с jcr:data
свойство, которое позволяет мне хранить двоичный контент. Это хорошо для одноразового или нечастого изменения содержимого файла.
Однако я собираюсь постоянно добавлять новый контент в этот файл и, кроме того, опрашивать его содержимое в отдельных потоках (для отслеживания прогресса).
JCR API в лице javax.jcr.ValueFactory
, javax.jcr.Binary
Кажется, на самом деле не поддерживается такой подход, я бы предпочел переопределять этот файл (или, точнее, двоичное свойство) снова и снова каждый раз, когда добавляю одну строку журнала. Я беспокоюсь о производительности.
Я искал документацию для инструментов, которые позволили бы мне открыть выходной поток для этого файла и периодически сбрасывать изменения из этого потока в JCR, но, похоже, ничего подобного не доступно.
Так есть ли что-нибудь более умное, чем идти с простым javax.jcr.ValueFactory
а также javax.jcr.Binary
?
1 ответ
Рассмотрев пока все варианты у меня тут:
Храните журналы в памяти, сохраняйте их в CRX каждый раз, когда пользователь вызывает
info/warn/error
, Плюсы: журналы хранятся в том же месте, что и метаданные задачи миграции, легко найти и получить доступ. Минусы: Потенциально самый медленный и наименее эффективный ресурс из всех подходов в случае большого количества записей в журнале.Храните журналы в памяти, сохраняйте их в JCR только в конце миграции. Плюсы: простое решение для рефакторинга текущего решения, меньшее давление на CRX во время процесса миграции. Минусы: Не удается отследить прогресс в реальном времени, возможную потерю журналов при непредвиденной ошибке или отключении экземпляра.
Создайте узел пользовательского типа для каждой записи в журнале вместо log.txt. Агрегируйте журналы в текстовом файле через специальный сервлет журналов. т.е.
/var/migration/uuid/log.txt
или же/var/migration/uuid/log.json
, Плюсы: ещё JCR способ хранения такого контента. С пользовательским типом узла и индексами должно быть достаточно быстро, чтобы рассмотреть в качестве опции. Имеет разнообразие для поддержки текста и формата журналов JSON. Минусы: нечеткое сравнение производительности с текущим подходом. Потенциальные проблемы из-за большого количества узлов, расположенных на одном уровне. Пользователь должен знать о существовании сервлета журнала, иначе пользователь не сможет увидеть их в удобном формате. Производительность сервлета журнала неясна в случае большого количества записей журнала.Создайте файлы журналов в файловой системе (скажем, в
crx-quickstart/logs/migration/<uuid>.log
), отображать его содержимое (при необходимости) через API с возможностью транслирования ответа API журнала на последние 100-1000 строк. Плюсы: классический и хорошо известный подход к журналам, когда файлы журналов хранятся в файловой системе. Слинг обеспечивает настроенную привязкуslf4j
вLogBack
со всеми необходимыми зависимостями LogBack, экспортированными для использования в ваших пользовательских пакетах. Минусы: разделение логов и метаданных задачи. Пользователь должен знать расположение файлов журнала на диске.
Начав с варианта 1, я понял, что количество записей в журнале потенциально может достигать сотен тысяч - редкий, но возможный сценарий. Поэтому в итоге решил перейти с варианта 4.
На случай, если кто-нибудь столкнется с подобной задачей, я публикую здесь подробности реализации для варианта 4, поскольку это не так тривиально, как может показаться на первый взгляд.
я использую AEM 6.2
(Felix-Jackrabbit-Sling под капотом), и я хочу, чтобы каждая задача миграции, которая по сути является просто отдельным потоком, создала свой собственный файл журнала со специальным именем - уникальным идентификатором этого процесса миграции.
Теперь сам Sling позволяет вам определять несколько конфигураций журнала через org.apache.sling.commons.log.LogManager.factory.config
Конфигурация OSGi. Однако эти конфигурации журнала слишком просты для этого случая - вы не можете создать с его помощью то, что в LogBack называется SiftingAppender - особый случай приложения для журнала, который будет создавать экземпляры приложений для определенных регистраторов в потоке, а не один раз и для всего приложения - другими словами, вы не может указать LogBack создать файл для каждого потока, используя конфигурацию OSGi.
Таким образом, логично подумать, что вы захотите программно взять конфигурацию LogBack Sling во время выполнения (например, в тот момент, когда вы загрузили свой пользовательский пакет и активировали его) и использовать его для настройки такого приложения для определенных регистраторов. К сожалению, хотя есть много документации о том, как настроить LogBack через logback.xml
Есть довольно мало документов, которые описывают, как сделать это программно с помощью объектов Java LogBack, таких как ch.qos.logback.classic.LoggerContext
и как ноль из них объясняют, как настроить таким образом SiftingAppender
,
Итак, после прочтения исходных кодов и тестов LogBack я получил этот вспомогательный класс:
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")
И использовать его для создания файлов журнала, таких как crq-quickstart/logs/migration/task-<taskId>.log
согласно предоставленной taskId
В соответствии с документами, вы также должны выпустить такой регистратор, как только вы сделали с ним
LogUtil.releaseTaskLogger()
И в значительной степени это все.