package com.alexanderlogistics.metrics; import java.util.ArrayList; import java.util.Collections; 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 com.alexanderlogistics.KreditorDebitorService; 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 Creditor Metric Entity (type=metric.creditor). *

* Beim Rechnungseingang gilt eine Rechnung als Bezahlt wenn diese einen finalen * Status erreicht hat (>=5800) *

* 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 MetricCreditorService { private static Logger logger = Logger.getLogger(MetricCreditorService.class.getName()); private final ConcurrentHashMap metricCache = new ConcurrentHashMap<>(); private final Set registeredGauges = ConcurrentHashMap.newKeySet(); public static final String TYPE_METRIC_CREDITOR = "metric.creditor"; public static final String ITEM_TOTAL = "invoice.total"; public static final String ITEM_SALDO = "invoice.saldo"; @Inject @RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE) MetricRegistry metricRegistry; @Inject DocumentService documentService; @Inject KreditorDebitorService kreditorDebitorService; @Inject MetricDataService metricDataService; @Inject @ConfigProperty(name = "metrics.enabled", defaultValue = "false") private boolean metricsEnabled; /** * Reset the internal metricCache and clears all registered Gauges. */ public void reset() { metricCache.clear(); registeredGauges.clear(); } /** * Reset the internal metricCache and clears all registered Gauges for a list of * metric keys. */ public void reset(List metricKeys) { for (String metricKey : metricKeys) { metricCache.remove(metricKey); metricRegistry.remove(metricKey); } } /** * Process Metric only if some data has changed.... * * @param processingEvent * @throws PluginException */ public void onProcessingEvent(@Observes ProcessingEvent processingEvent) { long l = System.currentTimeMillis(); if (!metricsEnabled) { return; } ItemCollection invoice = processingEvent.getDocument(); if (!InvoiceUtil.isCreditorInvoice(invoice)) { // skip event return; } // update metric and the metric cache if (processingEvent.getEventType() == ProcessingEvent.AFTER_PROCESS) { // load the last invoice metric and reduce the saldo... ItemCollection lastInvoice = metricDataService.readDirtyWorkitem(invoice.getUniqueID()); if (lastInvoice != null) { try { ItemCollection lastMetricData = getMetricByInvoice(lastInvoice); subtractInvoice(lastMetricData, lastInvoice); putMetric(lastMetricData); metricDataService.saveMetric(lastMetricData); } catch (PluginException e) { // invalid invoice - e.g. no cdtr. number } } try { // load the invoice metric and add the saldo... ItemCollection metricData = getMetricByInvoice(invoice); // Saldo-Berechnung addInvoice(metricData, invoice); putMetric(metricData); metricDataService.saveMetric(metricData); // Update the Gauge updateGauge(metricData); logger.info("Metric cdtr update took " + (System.currentTimeMillis() - l) + "ms"); } catch (PluginException e) { // invalid invoice - e.g. no cdtr. number } } } /** * Returns the corresponding metric data object for an invoice. The method uses * an internal cache. If the metric was not yet cached the method loads the * metric from the database. If not metric exists in the database the method * automatically creates a new metric data object. * * The method throws a PluginException if no metric can be build from this * invoice (e.g. missing cdtr.number). * * @param invoice * @return * @throws PluginException */ public ItemCollection getMetricByInvoice(ItemCollection invoice) throws PluginException { if (invoice == null) { return null; } String metricKey = MetricDataService.buildKeyByInvoice(invoice); ItemCollection metricData = metricCache.get(metricKey); if (metricData == null) { metricData = loadMetric(invoice); } // if metric still null we create a new metric data object. if (metricData == null) { metricData = createMetaData(invoice); } return metricData; } /** * Returns a metric data object by key * * @param key * @return */ public ItemCollection getMetric(String key) { return metricCache.get(key); } /** * Puts a metric data object into the cache. * * @param metricData */ public void putMetric(ItemCollection metricData) { metricCache.put(metricData.getItemValueString("name"), metricData); } /** * Returns a list with all cached metric keys. * The method returns an unmodifiable list to prevent modifications. * * @return */ public List getMetricKeys() { return Collections.unmodifiableList(new ArrayList<>(metricCache.keySet())); } /** * This method loads a metric entity for a given invoice workitem. If no metric * entity exits, the method * creates a new metric entity. * * @param invoice * @return * @throws PluginException */ private ItemCollection loadMetric(ItemCollection invoice) throws PluginException { ItemCollection creditorMetric = null; if (invoice == null) { return null; } try { String metricKey = MetricDataService.buildKeyByInvoice(invoice); String query = "(type:" + TYPE_METRIC_CREDITOR + ") AND (name:" + metricKey + ")"; List result = documentService.find(query, 1, 0, "$modified", true); if (result.size() > 0) { creditorMetric = result.get(0); } } catch (IllegalArgumentException | QueryException e) { throw new PluginException(PluginException.class.getName(), "Failed to load metric object for invoice " + invoice.getUniqueID() + ": ", e.getMessage(), e); } return creditorMetric; } /** * Creates an empty Creditor Meta Data Object (ItemCollection) *

* The ItemCollection stores the name and number and also all saldos for all * currencies * * @param invoice - invoice ItemCollection * @return * @throws PluginException */ private ItemCollection createMetaData(ItemCollection invoice) throws PluginException { if (invoice == null) { return null; } String key = MetricDataService.buildKeyByInvoice(invoice); ItemCollection metricData = new ItemCollection(); metricData.setType(TYPE_METRIC_CREDITOR); metricData.setItemValue("name", key); metricData.setItemValue("bp.id", InvoiceUtil.getBPId(invoice)); metricData.setItemValue("bp.name", InvoiceUtil.getBPName(invoice)); metricData.setItemValue("country", invoice.getItemValueString("invoice.country")); metricData.setItemValue("currency", invoice.getItemValueString("invoice.currency")); metricData.setItemValue("department", invoice.getItemValueString("space.name")); return metricData; } /** * Helper method to register a gauge for a creditor * * @param cdtrNumber - the creditor number * @param cdtrName - the creditor name */ public void updateGauge(ItemCollection metricData) { String metricKey = metricData.getItemValueString("name"); String bpName = metricData.getItemValueString("bp.name"); String bpId = metricData.getItemValueString("bp.id"); String country = metricData.getItemValueString("country"); String department = metricData.getItemValueString("department"); String currency = metricData.getItemValueString("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", "cdtr")); tags.add(new Tag("id", bpId)); tags.add(new Tag("name", bpName)); tags.add(new Tag("country", country)); tags.add(new Tag("currency", currency)); tags.add(new Tag("department", department)); logger.fine("register new metric for department: " + department + ", " + metricData.getItemValueString(ITEM_SALDO) + " " + currency); Metadata metadata = Metadata.builder() .withName("cdtr.balance") .withDescription("Creditor Balance") .build(); metricCache.get(metricKey); metricRegistry.gauge(metadata, () -> metricCache.get(metricKey).getItemValueDouble(ITEM_SALDO), tags.toArray(new Tag[0])); } else { logger.fine("Cdtr Gauge already registered for department: " + department); } } /** * Addiert den saldo einer Invoice zu einem metricData object * * @param metricData * @param invoice */ public void addInvoice(ItemCollection metricData, ItemCollection invoice) { double invoiceTotal = invoice.getItemValueDouble(ITEM_TOTAL); if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) { // vorgang ist archiviert oder gelöscht worden => saldo = 0! invoiceTotal = 0.0; } // is the metric new? if (!metricData.hasItem(ITEM_SALDO)) { // init metric with the total value! metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(invoiceTotal)); } else { double lastSaldo = metricData.getItemValueDouble(ITEM_SALDO); metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(lastSaldo + invoiceTotal)); } } public void subtractInvoice(ItemCollection metricData, ItemCollection invoice) { if (metricData == null || invoice == null) { return; } double invoiceTotal = invoice.getItemValueDouble(ITEM_TOTAL); if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) { // vorgang ist archiviert oder gelöscht worden => saldo = 0! invoiceTotal = 0.0; } // subtract only if metric saldo exists if (metricData.hasItem(ITEM_SALDO)) { double lastSaldo = metricData.getItemValueDouble(ITEM_SALDO); metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(lastSaldo - invoiceTotal)); } } /** * Helper Method that refreshes all gauges. The method is called by the * RestService during a rebuild. */ @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) public void refreshGauges() { List keys = getMetricKeys(); refreshGauges(keys); } /** * Helper Method that refreshes all gauges. The method is called by the * RestService during a rebuild. */ @TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW) public void refreshGauges(List keys) { for (String hashKey : keys) { ItemCollection metricData = getMetric(hashKey); documentService.save(metricData); updateGauge(metricData); } } }