package com.alexanderlogistics.metrics; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Logger; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.metrics.Metadata; import org.eclipse.microprofile.metrics.MetricRegistry; import org.eclipse.microprofile.metrics.Tag; import org.eclipse.microprofile.metrics.annotation.RegistryScope; import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.ProcessingEvent; import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.QueryException; import com.alexanderlogistics.InvoiceUtil; import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RunAs; import jakarta.ejb.TransactionAttribute; import jakarta.ejb.TransactionAttributeType; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; /** * Dieser Service reagiert auch ProcessingEvents und speichert/aktualisiert die * zugehörige Debitoren Metric Entity. *

* Der Service liest in einem AFTER_PROCESS Event den alten invoice.saldo aus. * Hierzu wird die Rechnung in einer neuen Transaktion geladen was einem * 'Dirty-Read' entspricht. Dadurch kennt die Methode den letzten saldo der * Rechnung. Hat sich nun der aktuelle Saldo geändert, aktualisiert der Serivce * die entsprechende Metric. * * */ @DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.MANAGERACCESS" }) @RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.MANAGERACCESS" }) @RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS") @ApplicationScoped public class MetricDebitorService { private static Logger logger = Logger.getLogger(MetricDebitorService.class.getName()); private final ConcurrentHashMap metricCache = new ConcurrentHashMap<>(); private final Set registeredGauges = ConcurrentHashMap.newKeySet(); public static final String TYPE_METRIC_DEBITOR = "metric.debitor"; public static final String ITEM_SALDO = "invoice.saldo"; @Inject @RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE) MetricRegistry metricRegistry; @Inject DocumentService documentService; @Inject @ConfigProperty(name = "metrics.enabled", defaultValue = "false") private boolean metricsEnabled; /** * Process Metric only if some data has changed.... * * @param processingEvent * @throws PluginException */ public void onProcessingEvent(@Observes ProcessingEvent processingEvent) throws PluginException { if (!metricsEnabled) { return; } double debitorSaldo = 0; ItemCollection invoice = processingEvent.getDocument(); if (!invoice.getModelVersion().startsWith("rechnungsausgang-")) { // skip event return; } // verify if saldo has changed..... double invoiceSaldo = invoice.getItemValueDouble(ITEM_SALDO); if (!"workitem".equals(invoice.getType())) { // vorgang ist archiviert oder gelöscht worden => saldo = 0! invoiceSaldo = 0.0; } String dbtrNumber = invoice.getItemValueString("dbtr.number"); String dbtrName = invoice.getItemValueString("dbtr.name"); String currency = invoice.getItemValueString("invoice.currency"); // load last metric... double lastInvoiceSaldo = readDirtySaldo(invoice.getUniqueID()); if (invoiceSaldo == lastInvoiceSaldo) { // no change - no metric update! return; } // update metric and the metric cache if (processingEvent.getEventType() == ProcessingEvent.AFTER_PROCESS) { ItemCollection metricData = metricCache.get(dbtrNumber); if (metricData == null) { metricData = loadMetric(dbtrNumber, dbtrName); } // Saldo-Berechnung debitorSaldo = metricData.getItemValueDouble(ITEM_SALDO + "." + currency); // totals.get(currency); debitorSaldo = debitorSaldo - lastInvoiceSaldo; // update debitor saldo debitorSaldo = InvoiceUtil.round(debitorSaldo + invoiceSaldo); metricData.setItemValue(ITEM_SALDO + "." + currency, debitorSaldo); metricCache.put(dbtrNumber, metricData); // Speichern in der Datenbank logger.info("Update metric for debitor: " + dbtrNumber + " -> " + debitorSaldo); documentService.save(metricData); // Gauge registrieren updateGauge(metricData, currency); } } /** * This helper method reads the 'dirty' saldo in a new transaction. This is used * for calculating the new total saldo * * @param uniqueid */ @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) public double readDirtySaldo(String uniqueid) { double result = 0; ItemCollection dirtyInvoice = documentService.load(uniqueid); if (dirtyInvoice == null) { return 0.0; } result = dirtyInvoice.getItemValueDouble(ITEM_SALDO); // logger.info("---Dirty last Saldo was : " + result); return result; } /** * This method loads a metric entity. If no metric entity exits, the method * creates a new one. * * @param dbtrID * @return * @throws PluginException */ public ItemCollection loadMetric(String dbtrNumber, String dbtrName) throws PluginException { ItemCollection debitorMetric = null; if (dbtrNumber == null || dbtrNumber.isEmpty()) { throw new PluginException(PluginException.class.getName(), "QUERY ERROR", "missing debitor number"); } try { String query = "(type:" + TYPE_METRIC_DEBITOR + ") AND (name:" + dbtrNumber + ")"; List result = documentService.find(query, 1, 0, "$modified", true); if (result.size() > 0) { debitorMetric = result.get(0); } if (debitorMetric == null) { // create a new instance // logger.info("creating new debitor metric"); debitorMetric = createMetaData(dbtrNumber, dbtrName); } } catch (QueryException e) { throw new PluginException(PluginException.class.getName(), "QUERY ERROR", e.getMessage(), e); } return debitorMetric; } /** * Creates an empty Debitor Meta Data Object (ItemCollection) *

* The ItemCollection stores the name and number and also all saldos for all * currencies * * @param dbtrNumber * @param dbtrName * @return */ private ItemCollection createMetaData(String dbtrNumber, String dbtrName) { ItemCollection metricData = new ItemCollection(); metricData = new ItemCollection(); metricData.setType(TYPE_METRIC_DEBITOR); metricData.setItemValue("name", dbtrNumber); metricData.setItemValue("dbtr.number", dbtrNumber); metricData.setItemValue("dbtr.name", dbtrName); return metricData; } /** * Helper method to register a gauge for a debitor * * @param dbtrNumber - the debitor number * @param dbtrName - the debitor name */ private void updateGauge(ItemCollection metricData, String currency) { String dbtrNumber = metricData.getItemValueString("dbtr.number"); String dbtrName = metricData.getItemValueString("dbtr.name"); String metricKey = "dbtr_" + dbtrNumber + "_" + dbtrName + "_" + currency; // Prüfen ob Gauge bereits registriert ist if (registeredGauges.add(metricKey)) { // returns true newly added List tags = new ArrayList<>(); tags.add(new Tag("type", "dbtr")); tags.add(new Tag("number", dbtrNumber)); tags.add(new Tag("name", dbtrName)); tags.add(new Tag("currency", currency)); logger.info("register new metric for debitor: " + dbtrNumber + " -> " + currency); Metadata metadata = Metadata.builder() .withName("dbtr.invoice.saldo") .withDescription("Debitor Balance by Currency") .build(); metricRegistry.gauge(metadata, () -> metricCache.getOrDefault(dbtrNumber, createMetaData(dbtrNumber, dbtrName)) .getItemValueDouble(ITEM_SALDO + "." + currency), tags.toArray(new Tag[0])); } else { logger.fine("Gauge already registered for debitor: " + dbtrNumber + " -> " + currency); } } /** * Hilfsmethode um Metriken initial aufzubauen. Wird vom Metric Rest Service * verwendet. * */ protected void initMetric(ItemCollection metric) { String dbtrNumber = metric.getItemValueString("dbtr.number"); // Aktuellen Saldo in Map speichern metricCache.put(dbtrNumber, metric); // Gauge für jede Währung registrieren for (String itemName : metric.getItemNames()) { if (itemName.startsWith(ITEM_SALDO + ".")) { // currency value found int pos = (ITEM_SALDO + ".").length(); String currency = itemName.substring(pos).toUpperCase(); logger.info("│   │   ├──init metric for debitor " + dbtrNumber + " -> " + currency); updateGauge(metric, currency); } } } }