From d5d4b21434806a27ada23d2436bbe2f05929bbc0 Mon Sep 17 00:00:00 2001
From: Ralph Soika
Date: Sat, 30 Nov 2024 16:00:40 +0100
Subject: [PATCH] =?UTF-8?q?AGL=20Metrics=20eingef=C3=BChrt=20(draft)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docker-compose.yml | 2 +
.../MetricDebitorRestService.java | 208 +
.../MetricDebitorService.java | 178 +
workflow/rechnungsausgang-de-1.0.14.bpmn | 4280 +++++++++++++++++
4 files changed, 4668 insertions(+)
create mode 100644 office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java
create mode 100644 office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java
create mode 100644 workflow/rechnungsausgang-de-1.0.14.bpmn
diff --git a/docker-compose.yml b/docker-compose.yml
index 00772be..e908d2c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -40,6 +40,8 @@ services:
LLM_SERVICE_ENDPOINT_USER: "admin"
LLM_SERVICE_ENDPOINT_PASSWORD: "imixs4.null"
+ METRICS_ENABLED: "true"
+
ports:
- "8080:8080"
- "9990:9990"
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java
new file mode 100644
index 0000000..0aea3e1
--- /dev/null
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorRestService.java
@@ -0,0 +1,208 @@
+package com.alexanderlogistics;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import org.imixs.marty.team.TeamService;
+import org.imixs.workflow.ItemCollection;
+import org.imixs.workflow.ItemCollectionComparator;
+import org.imixs.workflow.engine.DocumentService;
+import org.imixs.workflow.exceptions.PluginException;
+import org.imixs.workflow.exceptions.QueryException;
+
+import jakarta.ejb.Stateless;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+
+@Path("metrics")
+@Stateless
+public class MetricDebitorRestService {
+
+ private static Logger logger = Logger.getLogger(MetricDebitorRestService.class.getName());
+
+ @Inject
+ DocumentService documentService;
+
+ @Inject
+ MetricDebitorService metricService;
+
+ @Inject
+ TeamService teamService;
+
+ @GET
+ @Path("/ping")
+ @Produces({ MediaType.TEXT_PLAIN })
+ public String ping() {
+ logger.info("GET ping");
+ return "ping: " + System.currentTimeMillis();
+ }
+
+ /**
+ * This method refreshes all debitor metrics by iterating through the metric
+ * entities.
+ *
+ * @return
+ */
+ @GET
+ @Path("/refresh")
+ @Produces({ MediaType.TEXT_PLAIN })
+ public Response refreshMetrics() {
+ try {
+ // Alle Debitoren Metriken laden
+ String query = "(type:" + MetricDebitorService.TYPE_METRIC_DEBITOR + ")";
+ List metrics = documentService.find(query, 9999, 0);
+
+ // Metriken initialisieren
+ for (ItemCollection metric : metrics) {
+ metricService.initMetric(metric);
+ }
+
+ return Response.ok().entity("Initialized " + metrics.size() + " debitor metrics").build();
+
+ } catch (Exception e) {
+ return Response.serverError()
+ .entity("Failed to initialize metrics: " + e.getMessage())
+ .build();
+ }
+ }
+
+ /**
+ * This method initializes the metrics for all debitors with open invoices.
+ * The method creates or updates the metric entires for each debitor.
+ *
+ * @return
+ */
+ @GET
+ @Path("/init")
+ @Produces({ MediaType.TEXT_PLAIN })
+ public Response initMetrics() {
+ Map dbtrDataCache = new HashMap();
+ long l = System.currentTimeMillis();
+ logger.info("├── init metrics...");
+ try {
+ // compute all departments
+ List spaceNames = new ArrayList();
+ List spaces = teamService.getSpaces();
+ // sort by space.name
+ Collections.sort(spaces, new ItemCollectionComparator("space.name", true));
+ for (ItemCollection space : spaces) {
+ String spaceName = space.getItemValueString("space.name");
+ spaceNames.add(spaceName);
+ logger.info("│ ├── grouping invoices by debitor...");
+ groupInvoicesByWeek(space.getUniqueID(), spaceName, dbtrDataCache);
+ }
+ logger.info("│ ├── grouping invoices finished in " + (System.currentTimeMillis() - l) + "ms");
+
+ rebuildMetrics(dbtrDataCache);
+ logger.info("├── init metrics completed in " + (System.currentTimeMillis() - l) + "ms");
+ return Response.ok().entity("init metrics completed in " + (System.currentTimeMillis() - l) + "ms").build();
+
+ } catch (Exception e) {
+ return Response.serverError()
+ .entity("Failed to initialize metrics: " + e.getMessage())
+ .build();
+ }
+ }
+
+ private void rebuildMetrics(Map dbtrDataCache) throws PluginException {
+ logger.info("│ ├── rebuild metrics...");
+
+ for (String dbtrNumber : dbtrDataCache.keySet()) {
+ DbtrData dbtrData = dbtrDataCache.get(dbtrNumber);
+ String dbtrName = dbtrData.dbtrName;
+
+ ItemCollection metric = metricService.loadMetric(dbtrNumber, dbtrName);
+ metric.setItemValue(MetricDebitorService.ITEM_SALDO, dbtrData.getTotal("EUR"));
+ documentService.save(metric);
+ metricService.initMetric(metric);
+ }
+ }
+
+ /**
+ * Diese Methode gruppiert eine Rechnungsliste nach debitoren
+ *
+ * @param spaceID - Space Ref to select a list of invoices associated with
+ * a
+ * space
+ * @param dbtrDataCache - a local cache storing all invoices by week
+ *
+ *
+ */
+ private void groupInvoicesByWeek(String spaceID, String spaceName,
+ Map dbtrDataCache) {
+
+ logger.info("│ │ ├── group invoices for " + spaceName + "/" + spaceID);
+ try {
+ List invoices = documentService.find(
+ "$modelversion:rechnungsausgang-* AND type:workitem AND $uniqueidref:" + spaceID, 9999, 0,
+ "invoice.number", false);
+
+ logger.fine(" found " + invoices.size() + " for space " + spaceName);
+ for (ItemCollection invoice : invoices) {
+
+ String debitorName = invoice.getItemValueString("dbtr.name");
+ String debitorNumber = invoice.getItemValueString("dbtr.number");
+
+ DbtrData invoiceData = dbtrDataCache.get(debitorNumber);
+ if (invoiceData == null) {
+ invoiceData = new DbtrData(debitorNumber, debitorName);
+ }
+
+ // Jetzt Rechnung addieren
+ invoiceData.add(invoice);
+ dbtrDataCache.put(debitorNumber, invoiceData);
+ }
+
+ } catch (QueryException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ /**
+ * Data Element for invoice totals by debitor.
+ * The object holds multiple currencies.
+ */
+ class DbtrData {
+
+ String dbtrName;
+ String dbtrNumber;
+
+ Map totals = new HashMap<>();
+
+ public DbtrData(String dbtrNumber, String dbtrName) {
+ this.dbtrName = dbtrName;
+ this.dbtrNumber = dbtrNumber;
+ }
+
+ public void add(ItemCollection invoice) {
+ String currency = invoice.getItemValueString("invoice.currency");
+ Double total = invoice.getItemValueDouble("invoice.saldo");
+ Double totalCurrency = totals.get(currency);
+ if (totalCurrency == null) {
+ totalCurrency = 0.0;
+ }
+ totalCurrency = totalCurrency + total;
+ totals.put(currency, InvoiceUtil.round(totalCurrency));
+ }
+
+ public double getTotal(String currency) {
+ Double result = totals.get(currency);
+ if (result != null) {
+ return result;
+ } else {
+ return 0.0;
+ }
+ }
+
+ }
+
+}
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java
new file mode 100644
index 0000000..f617359
--- /dev/null
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/MetricDebitorService.java
@@ -0,0 +1,178 @@
+package com.alexanderlogistics;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+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 jakarta.annotation.security.DeclareRoles;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.annotation.security.RunAs;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+
+/**
+ * Dieser Service reagiert auch ProcessingEvents und speichert/aktualisert die
+ * zugehörige Debitoren Metric Entity.
+ *
+ */
+@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 currentSaldos = new ConcurrentHashMap<>();
+
+ public static final String METRIC_INVOICES = "invoices";
+ public static final String TYPE_METRIC_DEBITOR = "metric.debitor";
+ public static final String ITEM_LAST_METRIC = "metric.saldo";
+ public static final String ITEM_SALDO = "invoice.saldo";
+
+ @Inject
+ @RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE)
+ MetricRegistry metricRegistry;
+
+ @Inject
+ DocumentService documentService;
+
+ /**
+ * Process Metric only if some data has changed....
+ *
+ * @param processingEvent
+ * @throws PluginException
+ */
+ public void onProcessingEvent(@Observes ProcessingEvent processingEvent) throws PluginException {
+ 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 (invoiceSaldo == invoice.getItemValueDouble(ITEM_LAST_METRIC)) {
+ // no change - no metric update!
+ return;
+ }
+
+ // update metric and cache last value
+ if (processingEvent.getEventType() == ProcessingEvent.AFTER_PROCESS) {
+ String dbtrNumber = invoice.getItemValueString("dbtr.number");
+ String dbtrName = invoice.getItemValueString("dbtr.name");
+
+ ItemCollection metric = loadMetric(dbtrNumber, dbtrName);
+
+ // Saldo-Berechnung wie bisher
+ debitorSaldo = metric.getItemValueDouble(ITEM_SALDO);
+ if (invoice.hasItem(ITEM_LAST_METRIC)) {
+ debitorSaldo = debitorSaldo - invoice.getItemValueDouble(ITEM_LAST_METRIC);
+ }
+ debitorSaldo = InvoiceUtil.round(debitorSaldo + invoiceSaldo);
+
+ // Speichern in der Datenbank
+ metric.setItemValue(ITEM_SALDO, debitorSaldo);
+ logger.info("--- update Debitor Saldo " + dbtrNumber + " -> " + debitorSaldo);
+ documentService.save(metric);
+
+ // Aktuellen Saldo im Memory speichern
+ currentSaldos.put(dbtrNumber, debitorSaldo);
+ // Gauge registrieren
+ registerGauge(dbtrNumber, metric.getItemValueString("dbtr.name"));
+ logger.info("--- new gauge value for " + dbtrNumber + " -> " + debitorSaldo);
+ }
+ }
+
+ /**
+ * 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 = new ItemCollection();
+ debitorMetric.setType(TYPE_METRIC_DEBITOR);
+ debitorMetric.setItemValue("name", dbtrNumber);
+ debitorMetric.setItemValue("dbtr.number", dbtrNumber);
+ debitorMetric.setItemValue("dbtr.name", dbtrName);
+ }
+ } catch (QueryException e) {
+ throw new PluginException(PluginException.class.getName(), "QUERY ERROR", e.getMessage(), e);
+ }
+
+ return debitorMetric;
+ }
+
+ /**
+ * Hilfsmethode um Metriken initial aufzubauen. Wird vom Metric Rest Service
+ * verwendet.
+ *
+ */
+ public void initMetric(ItemCollection metric) {
+ String dbtrNumber = metric.getItemValueString("dbtr.number");
+ double saldo = metric.getItemValueDouble(ITEM_SALDO);
+
+ // Aktuellen Saldo in Map speichern
+ currentSaldos.put(dbtrNumber, saldo);
+ // Gauge registrieren
+ registerGauge(dbtrNumber, metric.getItemValueString("dbtr.name"));
+ logger.info("Initialized metric for debitor " + dbtrNumber + " -> " + saldo);
+ }
+
+ /**
+ * Helper method to register a gauge for a debitor
+ *
+ * @param dbtrNumber - the debitor number
+ * @param dbtrName - the debitor name
+ */
+ private void registerGauge(String dbtrNumber, String dbtrName) {
+ List tags = new ArrayList<>();
+ tags.add(new Tag("type", "dbtr"));
+ tags.add(new Tag("number", dbtrNumber));
+ tags.add(new Tag("name", dbtrName));
+
+ Metadata metadata = Metadata.builder()
+ .withName("dbtr.invoice.saldo")
+ .withDescription("Debitor Balance")
+ .withUnit("EUR")
+ .build();
+
+ metricRegistry.gauge(metadata,
+ () -> currentSaldos.getOrDefault(dbtrNumber, 0.0),
+ tags.toArray(new Tag[0]));
+ }
+}
\ No newline at end of file
diff --git a/workflow/rechnungsausgang-de-1.0.14.bpmn b/workflow/rechnungsausgang-de-1.0.14.bpmn
new file mode 100644
index 0000000..eb04d26
--- /dev/null
+++ b/workflow/rechnungsausgang-de-1.0.14.bpmn
@@ -0,0 +1,4280 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+$workflowstatus
+
+]]>
+
+
+ In case you have already made the payment, please disregard this notice. If you require any information regarding your account, please do not hesitate to contact us.
+Thank you for your cooperation.
+
+Kind regards
Alexander Global Logistics GmbH
+
+