361 lines
12 KiB
Java
361 lines
12 KiB
Java
package li.strolch.handler.operationslog;
|
|
|
|
import static java.util.Collections.emptyList;
|
|
import static java.util.Collections.singletonList;
|
|
import static java.util.ResourceBundle.getBundle;
|
|
import static li.strolch.agent.api.StrolchAgent.getUniqueId;
|
|
import static li.strolch.model.Tags.AGENT;
|
|
import static li.strolch.model.log.LogMessageState.Information;
|
|
import static li.strolch.runtime.StrolchConstants.SYSTEM_USER_AGENT;
|
|
|
|
import java.util.*;
|
|
import java.util.concurrent.ExecutorService;
|
|
import java.util.concurrent.Future;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.stream.Collectors;
|
|
|
|
import li.strolch.agent.api.ComponentContainer;
|
|
import li.strolch.agent.api.StrolchComponent;
|
|
import li.strolch.agent.api.StrolchRealm;
|
|
import li.strolch.model.Locator;
|
|
import li.strolch.model.log.LogMessage;
|
|
import li.strolch.model.log.LogMessageState;
|
|
import li.strolch.model.log.LogSeverity;
|
|
import li.strolch.persistence.api.LogMessageDao;
|
|
import li.strolch.persistence.api.StrolchTransaction;
|
|
import li.strolch.runtime.configuration.ComponentConfiguration;
|
|
|
|
public class OperationsLog extends StrolchComponent {
|
|
|
|
private LinkedBlockingQueue<LogTask> queue;
|
|
|
|
private Map<String, LinkedHashSet<LogMessage>> logMessagesByRealmAndId;
|
|
private Map<String, LinkedHashMap<Locator, LinkedHashSet<LogMessage>>> logMessagesByLocator;
|
|
private int maxMessages;
|
|
private ExecutorService executorService;
|
|
private Future<?> handleQueueTask;
|
|
private boolean run;
|
|
|
|
public OperationsLog(ComponentContainer container, String componentName) {
|
|
super(container, componentName);
|
|
}
|
|
|
|
@Override
|
|
public void initialize(ComponentConfiguration configuration) throws Exception {
|
|
|
|
this.maxMessages = configuration.getInt("maxMessages", 10000);
|
|
|
|
this.queue = new LinkedBlockingQueue<>();
|
|
this.logMessagesByRealmAndId = new HashMap<>();
|
|
this.logMessagesByLocator = new HashMap<>();
|
|
|
|
this.executorService = getSingleThreadExecutor("OperationsLog");
|
|
|
|
super.initialize(configuration);
|
|
}
|
|
|
|
@Override
|
|
public void start() throws Exception {
|
|
|
|
Set<String> realmNames = getContainer().getRealmNames();
|
|
for (String realmName : realmNames) {
|
|
StrolchRealm realm = getContainer().getRealm(realmName);
|
|
if (!realm.getMode().isTransient())
|
|
this.queue.offer(() -> loadMessages(realmName));
|
|
}
|
|
|
|
this.run = true;
|
|
this.handleQueueTask = this.executorService.submit(this::handleQueue);
|
|
|
|
super.start();
|
|
}
|
|
|
|
@Override
|
|
public void stop() throws Exception {
|
|
this.run = false;
|
|
if (this.handleQueueTask != null)
|
|
this.handleQueueTask.cancel(true);
|
|
if (this.executorService != null)
|
|
this.executorService.shutdownNow();
|
|
super.stop();
|
|
}
|
|
|
|
private void handleQueue() {
|
|
while (this.run) {
|
|
try {
|
|
LogTask poll = this.queue.poll(1, TimeUnit.SECONDS);
|
|
if (poll == null)
|
|
continue;
|
|
|
|
poll.run();
|
|
|
|
} catch (Exception e) {
|
|
if (e instanceof InterruptedException && !this.run)
|
|
logger.warn("Interrupted!");
|
|
else
|
|
logger.error("Failed to perform a task", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void loadMessages(String realmName) {
|
|
try {
|
|
runAsAgent(ctx -> {
|
|
|
|
logger.info("Loading OperationsLog for realm " + realmName + "...");
|
|
|
|
try (StrolchTransaction tx = openTx(realmName, ctx.getCertificate(), true)) {
|
|
LogMessageDao logMessageDao = tx.getPersistenceHandler().getLogMessageDao(tx);
|
|
List<LogMessage> messages = logMessageDao.queryLatest(realmName, this.maxMessages);
|
|
logger.info("Loaded " + messages.size() + " messages for OperationsLog for realm " + realmName);
|
|
this.logMessagesByRealmAndId.computeIfAbsent(realmName, OperationsLog::newHashSet).addAll(messages);
|
|
} catch (RuntimeException e) {
|
|
logger.error("Failed to load operations log for realm " + realmName, e);
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
logger.error("Failed to load operations logs!", e);
|
|
addMessage(
|
|
new LogMessage(realmName, SYSTEM_USER_AGENT, Locator.valueOf(AGENT, "strolch-agent", getUniqueId()),
|
|
LogSeverity.Exception, Information, getBundle("strolch-agent"),
|
|
"operationsLog.load.failed") //
|
|
.value("reason", e.getMessage()) //
|
|
.withException(e));
|
|
}
|
|
}
|
|
|
|
public void setMaxMessages(int maxMessages) {
|
|
this.maxMessages = maxMessages;
|
|
}
|
|
|
|
public void addMessage(LogMessage logMessage) {
|
|
if (this.logMessagesByRealmAndId != null)
|
|
this.queue.offer(() -> _addMessage(logMessage));
|
|
}
|
|
|
|
public void removeMessage(LogMessage message) {
|
|
this.queue.offer(() -> _removeMessage(message));
|
|
}
|
|
|
|
public void removeMessages(Collection<LogMessage> logMessages) {
|
|
this.queue.offer(() -> _removeMessages(logMessages));
|
|
}
|
|
|
|
public void updateState(String realmName, Locator locator, LogMessageState state) {
|
|
this.queue.offer(() -> _updateState(realmName, locator, state));
|
|
}
|
|
|
|
public void updateState(String realmName, String id, LogMessageState state) {
|
|
this.queue.offer(() -> _updateState(realmName, id, state));
|
|
}
|
|
|
|
private void _addMessage(LogMessage logMessage) {
|
|
// store in global list
|
|
String realmName = logMessage.getRealm();
|
|
LinkedHashSet<LogMessage> logMessages = this.logMessagesByRealmAndId.computeIfAbsent(realmName,
|
|
OperationsLog::newHashSet);
|
|
logMessages.add(logMessage);
|
|
|
|
// store under locator
|
|
LinkedHashMap<Locator, LinkedHashSet<LogMessage>> logMessagesLocator = this.logMessagesByLocator.computeIfAbsent(
|
|
realmName, this::newBoundedLocatorMap);
|
|
LinkedHashSet<LogMessage> messages = logMessagesLocator.computeIfAbsent(logMessage.getLocator(),
|
|
OperationsLog::newHashSet);
|
|
messages.add(logMessage);
|
|
|
|
// prune if necessary
|
|
List<LogMessage> messagesToRemove = _pruneMessages(realmName, logMessages);
|
|
|
|
// persist changes for non-transient realms
|
|
StrolchRealm realm = getContainer().getRealm(realmName);
|
|
if (!realm.getMode().isTransient())
|
|
persist(realm, logMessage, messagesToRemove);
|
|
}
|
|
|
|
private void _removeMessage(LogMessage message) {
|
|
String realmName = message.getRealm();
|
|
LinkedHashMap<Locator, LinkedHashSet<LogMessage>> byLocator = this.logMessagesByLocator.get(realmName);
|
|
if (byLocator != null) {
|
|
LinkedHashSet<LogMessage> messages = byLocator.get(message.getLocator());
|
|
if (messages != null) {
|
|
messages.remove(message);
|
|
if (messages.isEmpty())
|
|
byLocator.remove(message.getLocator());
|
|
}
|
|
}
|
|
|
|
LinkedHashSet<LogMessage> messages = this.logMessagesByRealmAndId.get(realmName);
|
|
if (messages != null)
|
|
messages.remove(message);
|
|
|
|
// persist changes for non-transient realms
|
|
StrolchRealm realm = getContainer().getRealm(realmName);
|
|
if (!realm.getMode().isTransient())
|
|
persist(realm, null, singletonList(message));
|
|
}
|
|
|
|
private void _removeMessages(Collection<LogMessage> logMessages) {
|
|
Map<String, List<LogMessage>> messagesByRealm = logMessages.stream()
|
|
.collect(Collectors.groupingBy(LogMessage::getRealm));
|
|
|
|
messagesByRealm.forEach((realmName, messages) -> {
|
|
|
|
LinkedHashMap<Locator, LinkedHashSet<LogMessage>> byLocator = this.logMessagesByLocator.get(realmName);
|
|
if (byLocator != null) {
|
|
messages.forEach(logMessage -> {
|
|
LinkedHashSet<LogMessage> tmp = byLocator.get(logMessage.getLocator());
|
|
if (tmp != null) {
|
|
tmp.remove(logMessage);
|
|
if (tmp.isEmpty())
|
|
byLocator.remove(logMessage.getLocator());
|
|
}
|
|
});
|
|
}
|
|
|
|
LinkedHashSet<LogMessage> byRealm = this.logMessagesByRealmAndId.get(realmName);
|
|
if (byRealm != null)
|
|
messages.removeIf(logMessage -> !byRealm.remove(logMessage));
|
|
|
|
// persist changes for non-transient realms
|
|
StrolchRealm realm = getContainer().getRealm(realmName);
|
|
if (!realm.getMode().isTransient())
|
|
persist(realm, null, messages);
|
|
});
|
|
}
|
|
|
|
private void _updateState(String realmName, Locator locator, LogMessageState state) {
|
|
getMessagesFor(realmName, locator).ifPresent(logMessages -> {
|
|
logMessages.forEach(logMessage -> logMessage.setState(state));
|
|
|
|
StrolchRealm realm = getContainer().getRealm(realmName);
|
|
if (!realm.getMode().isTransient())
|
|
updateStates(realm, logMessages);
|
|
|
|
});
|
|
}
|
|
|
|
private void _updateState(String realmName, String id, LogMessageState state) {
|
|
LinkedHashSet<LogMessage> logMessages = this.logMessagesByRealmAndId.get(realmName);
|
|
if (logMessages == null)
|
|
return;
|
|
|
|
for (LogMessage logMessage : logMessages) {
|
|
if (logMessage.getId().equals(id)) {
|
|
logMessage.setState(state);
|
|
|
|
StrolchRealm realm = getContainer().getRealm(realmName);
|
|
if (!realm.getMode().isTransient())
|
|
updateStates(realm, singletonList(logMessage));
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<LogMessage> _pruneMessages(String realm, LinkedHashSet<LogMessage> logMessages) {
|
|
if (logMessages.size() < this.maxMessages)
|
|
return emptyList();
|
|
|
|
List<LogMessage> messagesToRemove = new ArrayList<>();
|
|
|
|
int maxDelete = Math.max(1, (int) (this.maxMessages * 0.1));
|
|
int nrOfExcessMessages = logMessages.size() - this.maxMessages;
|
|
if (nrOfExcessMessages > 0)
|
|
maxDelete += nrOfExcessMessages;
|
|
|
|
logger.info("Pruning " + maxDelete + " messages from realm " + realm + "...");
|
|
Iterator<LogMessage> iterator = logMessages.iterator();
|
|
while (maxDelete > 0 && iterator.hasNext()) {
|
|
LogMessage messageToRemove = iterator.next();
|
|
messagesToRemove.add(messageToRemove);
|
|
iterator.remove();
|
|
maxDelete--;
|
|
}
|
|
|
|
return messagesToRemove;
|
|
}
|
|
|
|
private void persist(StrolchRealm realm, LogMessage logMessage, List<LogMessage> messagesToRemove) {
|
|
try {
|
|
runAsAgent(ctx -> {
|
|
try (StrolchTransaction tx = realm.openTx(ctx.getCertificate(), getClass(), false)) {
|
|
LogMessageDao logMessageDao = tx.getPersistenceHandler().getLogMessageDao(tx);
|
|
if (messagesToRemove != null && !messagesToRemove.isEmpty())
|
|
logMessageDao.removeAll(messagesToRemove);
|
|
if (logMessage != null)
|
|
logMessageDao.save(logMessage);
|
|
tx.commitOnClose();
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
handleFailedPersist(realm, e);
|
|
}
|
|
}
|
|
|
|
private void updateStates(StrolchRealm realm, Collection<LogMessage> logMessages) {
|
|
try {
|
|
runAsAgent(ctx -> {
|
|
try (StrolchTransaction tx = realm.openTx(ctx.getCertificate(), getClass(), false)) {
|
|
LogMessageDao logMessageDao = tx.getPersistenceHandler().getLogMessageDao(tx);
|
|
logMessageDao.updateStates(logMessages);
|
|
tx.commitOnClose();
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
handleFailedPersist(realm, e);
|
|
}
|
|
}
|
|
|
|
public void clearMessages(String realm, Locator locator) {
|
|
this.queue.offer(() -> {
|
|
LinkedHashMap<Locator, LinkedHashSet<LogMessage>> logMessages = this.logMessagesByLocator.get(realm);
|
|
if (logMessages != null)
|
|
logMessages.remove(locator);
|
|
});
|
|
}
|
|
|
|
public synchronized Optional<Set<LogMessage>> getMessagesFor(String realm, Locator locator) {
|
|
LinkedHashMap<Locator, LinkedHashSet<LogMessage>> logMessages = this.logMessagesByLocator.get(realm);
|
|
if (logMessages == null)
|
|
return Optional.empty();
|
|
LinkedHashSet<LogMessage> result = logMessages.get(locator);
|
|
if (result == null)
|
|
return Optional.empty();
|
|
return Optional.of(new HashSet<>(result));
|
|
}
|
|
|
|
public synchronized List<LogMessage> getMessages(String realm) {
|
|
LinkedHashSet<LogMessage> logMessages = this.logMessagesByRealmAndId.get(realm);
|
|
if (logMessages == null)
|
|
return emptyList();
|
|
|
|
return new ArrayList<>(logMessages);
|
|
}
|
|
|
|
private void handleFailedPersist(StrolchRealm realm, Exception e) {
|
|
logger.error("Failed to persist operations logs!", e);
|
|
addMessage(new LogMessage(realm.getRealm(), SYSTEM_USER_AGENT,
|
|
Locator.valueOf(AGENT, "strolch-agent", getUniqueId()), LogSeverity.Exception, Information,
|
|
getBundle("strolch-agent"), "operationsLog.persist.failed") //
|
|
.value("reason", e.getMessage()) //
|
|
.withException(e));
|
|
}
|
|
|
|
private LinkedHashMap<Locator, LinkedHashSet<LogMessage>> newBoundedLocatorMap(String realm) {
|
|
return new LinkedHashMap<>() {
|
|
@Override
|
|
protected boolean removeEldestEntry(java.util.Map.Entry<Locator, LinkedHashSet<LogMessage>> eldest) {
|
|
return size() > maxMessages;
|
|
}
|
|
};
|
|
}
|
|
|
|
private static LinkedHashSet<LogMessage> newHashSet(Object o) {
|
|
return new LinkedHashSet<>();
|
|
}
|
|
|
|
private interface LogTask {
|
|
void run() throws Exception;
|
|
}
|
|
}
|