refactoring inmemory metrics

This commit is contained in:
Ralph Soika 2025-02-01 12:49:05 +01:00
parent cde1b2a2eb
commit 51ccd40f18
5 changed files with 148 additions and 455 deletions

View file

@ -1,6 +1,5 @@
package com.alexanderlogistics.metrics; package com.alexanderlogistics.metrics;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -15,7 +14,6 @@ import jakarta.ejb.Stateless;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@ -62,9 +60,6 @@ public class MetricCreditorRestService {
// first clear the metric cache // first clear the metric cache
metricCreditorService.reset(); metricCreditorService.reset();
log("│   ├── reset metric cache", messageBuffer); log("│   ├── reset metric cache", messageBuffer);
// run in new transaction!
metricDataService.deleteAllMetrics(MetricCreditorService.TYPE_METRIC_CREDITOR);
log("│   ├── delete metrics", messageBuffer);
computeMetrics(); computeMetrics();
log("│   ├── computing metrics finished in " + (System.currentTimeMillis() - l) + "ms", messageBuffer); log("│   ├── computing metrics finished in " + (System.currentTimeMillis() - l) + "ms", messageBuffer);
@ -87,81 +82,6 @@ public class MetricCreditorRestService {
} }
} }
/**
* This method initializes the metrics for all creditors with open invoices.
*
* The method deletes all existing metrics and creates new metric entires for
* each creditor.
*
* @return
*/
@GET
@Path("/rebuild/{cdtrnumber}")
@Produces({ MediaType.TEXT_PLAIN })
public Response rebuildMetricsForCreditor(@PathParam("cdtrnumber") String cdtrnumber) {
StringBuffer messageBuffer = new StringBuffer();
long l = System.currentTimeMillis();
log("├── init cdtr metrics for " + cdtrnumber + "...", messageBuffer);
try {
logger.info("│   │   ├── group invoices by creditor " + cdtrnumber + "...");
int count = 0;
// First we need to remove all metrics for this creditor
String bpID = InvoiceUtil.buildBPID(cdtrnumber);
logger.info("│   │   ├── delete metrics for " + bpID + "...");
List<String> oldMetricKeys = metricDataService
.deleteAllMetricsByBPID(MetricCreditorService.TYPE_METRIC_CREDITOR, bpID);
logger.info("│   │   ├── found " + oldMetricKeys.size() + " metrics for " + bpID + "...");
metricCreditorService.reset(oldMetricKeys);
// next rebuild gauges and metrics for this creditor
List<ItemCollection> invoices = documentService.find(
"($modelversion:rechnungseingang-* OR $modelversion:gutschriftabgleich-*) " +
" AND type:workitem AND cdtr.number:" + cdtrnumber,
9999, 0, "invoice.number", false);
logger.info("│   │   ├──found " + invoices.size() + " open invoices");
List<String> newMetricKeys = new ArrayList<>();
for (ItemCollection invoice : invoices) {
try {
ItemCollection metricData = metricCreditorService.getMetricByInvoice(invoice);
newMetricKeys.add(metricData.getItemValueString("name"));
// Jetzt Rechnung addieren
metricCreditorService.addInvoice(metricData, invoice);
logger.info("│   │   │   ├──update metric " + InvoiceUtil.getBPId(invoice) + " Invoice: "
+ invoice.getItemValueString("invoice.number") + " Saldo: "
+ invoice.getItemValueDouble(MetricCreditorService.ITEM_TOTAL));
metricCreditorService.putMetric(metricData);
count++;
} catch (PluginException e) {
// invalid invoice - e.g. no cdtr. number
}
}
logger.info("│   │   ├── updated metric for " + count + " invoices.");
log("│   ├── computing metrics finished in " + (System.currentTimeMillis() - l) + "ms", messageBuffer);
logger.info("│   ├── save and init metrics...");
// run in new transaction!
metricCreditorService.refreshGauges(newMetricKeys);
String message = "├── rebuild cdtr metrics completed in "
+ (System.currentTimeMillis() - l)
+ "ms";
log(message, messageBuffer);
return Response.ok().entity(messageBuffer.toString()).build();
} catch (Exception e) {
e.printStackTrace();
return Response.serverError()
.entity("Failed to initialize metrics: " + e.getMessage())
.build();
}
}
/** /**
* Diese Methode berechnet alle Metriken auf basis der existierenden Rechnungen * Diese Methode berechnet alle Metriken auf basis der existierenden Rechnungen
* neu * neu
@ -184,7 +104,7 @@ public class MetricCreditorRestService {
// Jetzt Rechnung addieren // Jetzt Rechnung addieren
metricCreditorService.addInvoice(metricData, invoice); metricCreditorService.addInvoice(metricData, invoice);
logger.info("│   │   │   ├──update metric " + InvoiceUtil.getBPId(invoice)); logger.info("│   │   │   ├──update metric " + InvoiceUtil.getBPId(invoice));
metricCreditorService.putMetric(metricData); metricCreditorService.updateMetric(metricData);
count++; count++;
} catch (PluginException e) { } catch (PluginException e) {
// invalid invoice - e.g. no cdtr. number // invalid invoice - e.g. no cdtr. number

View file

@ -16,7 +16,6 @@ import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.ProcessingEvent; import org.imixs.workflow.engine.ProcessingEvent;
import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.QueryException;
import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.InvoiceUtil;
import com.alexanderlogistics.KreditorDebitorService; import com.alexanderlogistics.KreditorDebitorService;
@ -24,8 +23,6 @@ import com.alexanderlogistics.KreditorDebitorService;
import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs; import jakarta.annotation.security.RunAs;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes; import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@ -60,8 +57,10 @@ public class MetricCreditorService {
private final Set<String> registeredGauges = ConcurrentHashMap.newKeySet(); private final Set<String> registeredGauges = ConcurrentHashMap.newKeySet();
public static final String TYPE_METRIC_CREDITOR = "metric.creditor"; public static final String TYPE_METRIC_CREDITOR = "metric.creditor";
public static final String ITEM_TOTAL = "invoice.total"; public static final String ITEM_INVOICE_TOTAL = "invoice.total";
public static final String ITEM_SALDO = "invoice.saldo"; public static final String ITEM_INVOICE_SALDO = "invoice.saldo";
public static final String ITEM_METRIC_BALANCE = "invoice.balance";
public static final String ITEM_METRIC_SALES = "invoice.sales";
@Inject @Inject
@RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE) @RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE)
@ -88,17 +87,6 @@ public class MetricCreditorService {
registeredGauges.clear(); registeredGauges.clear();
} }
/**
* Reset the internal metricCache and clears all registered Gauges for a list of
* metric keys.
*/
public void reset(List<String> metricKeys) {
for (String metricKey : metricKeys) {
metricCache.remove(metricKey);
metricRegistry.remove(metricKey);
}
}
/** /**
* Process Metric only if some data has changed.... * Process Metric only if some data has changed....
* *
@ -124,9 +112,11 @@ public class MetricCreditorService {
if (lastInvoice != null) { if (lastInvoice != null) {
try { try {
ItemCollection lastMetricData = getMetricByInvoice(lastInvoice); ItemCollection lastMetricData = getMetricByInvoice(lastInvoice);
subtractInvoice(lastMetricData, lastInvoice); // update last metric only if exists...
putMetric(lastMetricData); if (!isNewMetric(lastMetricData)) {
metricDataService.saveMetric(lastMetricData); subtractInvoice(lastMetricData, lastInvoice);
updateMetric(lastMetricData);
}
} catch (PluginException e) { } catch (PluginException e) {
// invalid invoice - e.g. no cdtr. number // invalid invoice - e.g. no cdtr. number
} }
@ -137,11 +127,7 @@ public class MetricCreditorService {
ItemCollection metricData = getMetricByInvoice(invoice); ItemCollection metricData = getMetricByInvoice(invoice);
// Saldo-Berechnung // Saldo-Berechnung
addInvoice(metricData, invoice); addInvoice(metricData, invoice);
putMetric(metricData); updateMetric(metricData);
metricDataService.saveMetric(metricData);
// Update the Gauge
updateGauge(metricData);
logger.info("Metric cdtr update took " + (System.currentTimeMillis() - l) + "ms"); logger.info("Metric cdtr update took " + (System.currentTimeMillis() - l) + "ms");
} catch (PluginException e) { } catch (PluginException e) {
// invalid invoice - e.g. no cdtr. number // invalid invoice - e.g. no cdtr. number
@ -150,6 +136,17 @@ public class MetricCreditorService {
} }
/**
* Returns true if the metric is not yet registered. This means we do not have
* sales or balances for this metric
*
* @param metricData
* @return
*/
public boolean isNewMetric(ItemCollection metricData) {
return (!metricCache.containsKey(metricData.getItemValueString("name")));
}
/** /**
* Returns the corresponding metric data object for an invoice. The method uses * 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 * an internal cache. If the metric was not yet cached the method loads the
@ -169,12 +166,9 @@ public class MetricCreditorService {
} }
String metricKey = MetricDataService.buildKeyByInvoice(invoice); String metricKey = MetricDataService.buildKeyByInvoice(invoice);
ItemCollection metricData = metricCache.get(metricKey); ItemCollection metricData = metricCache.get(metricKey);
if (metricData == null) {
metricData = loadMetric(invoice);
}
// if metric still null we create a new metric data object. // if metric still null we create a new metric data object.
if (metricData == null) { if (metricData == null) {
metricData = createMetaData(invoice); metricData = createMetricData(invoice);
} }
return metricData; return metricData;
} }
@ -189,15 +183,6 @@ public class MetricCreditorService {
return metricCache.get(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. * Returns a list with all cached metric keys.
* The method returns an unmodifiable list to prevent modifications. * The method returns an unmodifiable list to prevent modifications.
@ -209,44 +194,17 @@ public class MetricCreditorService {
} }
/** /**
* This method loads a metric entity for a given invoice workitem. If no metric * Creates an empty Creditor Metric Data Object (ItemCollection)
* 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<ItemCollection> 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)
* <p> * <p>
* The ItemCollection stores the name and number and also all saldos for all * The ItemCollection stores the name and number and all categories.
* currencies * A new metric object does not yet have the items 'invoice.saldo' and
* 'invoice.total'
* *
* @param invoice - invoice ItemCollection * @param invoice - invoice ItemCollection
* @return * @return
* @throws PluginException * @throws PluginException
*/ */
private ItemCollection createMetaData(ItemCollection invoice) throws PluginException { private ItemCollection createMetricData(ItemCollection invoice) throws PluginException {
if (invoice == null) { if (invoice == null) {
return null; return null;
} }
@ -269,17 +227,20 @@ public class MetricCreditorService {
* @param cdtrNumber - the creditor number * @param cdtrNumber - the creditor number
* @param cdtrName - the creditor name * @param cdtrName - the creditor name
*/ */
public void updateGauge(ItemCollection metricData) { public void updateMetric(ItemCollection metricData) {
String metricKey = metricData.getItemValueString("name"); String metricKey = metricData.getItemValueString("name");
String bpName = metricData.getItemValueString("bp.name"); // Cache aktualisieren
String bpId = metricData.getItemValueString("bp.id"); metricCache.put(metricKey, metricData);
String country = metricData.getItemValueString("country");
String department = metricData.getItemValueString("department");
String currency = metricData.getItemValueString("currency");
// Prüfen ob Gauge bereits registriert ist // Prüfen ob Gauge bereits registriert ist
if (registeredGauges.add(metricKey)) { // returns true newly added if (registeredGauges.add(metricKey)) { // returns true newly added
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");
List<Tag> tags = new ArrayList<>(); List<Tag> tags = new ArrayList<>();
tags.add(new Tag("type", "cdtr")); tags.add(new Tag("type", "cdtr"));
tags.add(new Tag("id", bpId)); tags.add(new Tag("id", bpId));
@ -287,20 +248,24 @@ public class MetricCreditorService {
tags.add(new Tag("country", country)); tags.add(new Tag("country", country));
tags.add(new Tag("currency", currency)); tags.add(new Tag("currency", currency));
tags.add(new Tag("department", department)); tags.add(new Tag("department", department));
logger.fine("register new metric for department: " + department +
", " + metricData.getItemValueString(ITEM_SALDO) + // Saldo Gauge
" " + currency); Metadata balanceMetadata = Metadata.builder()
Metadata metadata = Metadata.builder()
.withName("cdtr.balance") .withName("cdtr.balance")
.withDescription("Creditor Balance") .withDescription("Creditor Balance")
.build(); .build();
metricRegistry.gauge(balanceMetadata,
metricCache.get(metricKey); () -> metricCache.get(metricKey).getItemValueDouble(ITEM_METRIC_BALANCE),
metricRegistry.gauge(metadata, tags.toArray(new Tag[0]));
() -> metricCache.get(metricKey).getItemValueDouble(ITEM_SALDO),
// Umsatz Gauge
Metadata revenueMetadata = Metadata.builder()
.withName("cdtr.sales")
.withDescription("Creditor Sales")
.build();
metricRegistry.gauge(revenueMetadata,
() -> metricCache.get(metricKey).getItemValueDouble(ITEM_METRIC_SALES),
tags.toArray(new Tag[0])); tags.toArray(new Tag[0]));
} else {
logger.fine("Cdtr Gauge already registered for department: " + department);
} }
} }
@ -312,57 +277,54 @@ public class MetricCreditorService {
* @param invoice * @param invoice
*/ */
public void addInvoice(ItemCollection metricData, ItemCollection invoice) { public void addInvoice(ItemCollection metricData, ItemCollection invoice) {
double invoiceTotal = invoice.getItemValueDouble(ITEM_TOTAL); if (metricData == null || invoice == null) {
return;
}
double invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
double invoiceTotal = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) { if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) {
// vorgang ist archiviert oder gelöscht worden => saldo = 0! // vorgang ist archiviert oder gelöscht worden => saldo = 0!
invoiceTotal = 0.0; invoiceSaldo = 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));
} }
// update balance
double lastBalance = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastBalance + invoiceSaldo));
// update sales
double lastTotal = metricData.getItemValueDouble(ITEM_METRIC_SALES);
metricData.setItemValue(ITEM_METRIC_SALES, InvoiceUtil.round(lastTotal + invoiceTotal));
} }
public void subtractInvoice(ItemCollection metricData, ItemCollection invoice) { public void subtractInvoice(ItemCollection metricData, ItemCollection invoice) {
if (metricData == null || invoice == null) { if (metricData == null || invoice == null) {
return; return;
} }
double invoiceTotal = invoice.getItemValueDouble(ITEM_TOTAL); double invoiceTotal = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
double invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) { if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) {
// vorgang ist archiviert oder gelöscht worden => saldo = 0! // vorgang ist archiviert oder gelöscht worden => saldo = 0!
invoiceTotal = 0.0; invoiceSaldo = 0.0;
} }
// subtract only if metric saldo exists // subtract only if metric saldo exists
if (metricData.hasItem(ITEM_SALDO)) { double lastBalance = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
double lastSaldo = metricData.getItemValueDouble(ITEM_SALDO); metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastBalance - invoiceSaldo));
metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(lastSaldo - invoiceTotal));
} // Neue Umsatz Logik
double lastSales = metricData.getItemValueDouble(ITEM_METRIC_SALES);
metricData.setItemValue(ITEM_METRIC_SALES, InvoiceUtil.round(lastSales - invoiceTotal));
} }
/** /**
* Helper Method that refreshes all gauges. The method is called by the * Helper Method that refreshes all gauges. The method is called by the
* RestService during a rebuild. * RestService during a rebuild.
*/ */
@TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW)
public void refreshGauges() { public void refreshGauges() {
List<String> keys = getMetricKeys(); List<String> 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<String> keys) {
for (String hashKey : keys) { for (String hashKey : keys) {
ItemCollection metricData = getMetric(hashKey); ItemCollection metricData = getMetric(hashKey);
documentService.save(metricData); updateMetric(metricData);
updateGauge(metricData);
} }
} }

View file

@ -1,14 +1,10 @@
package com.alexanderlogistics.metrics; package com.alexanderlogistics.metrics;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.imixs.workflow.ItemCollection; import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.index.SearchService;
import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.QueryException;
import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.InvoiceUtil;
@ -34,15 +30,6 @@ public class MetricDataService {
@Inject @Inject
DocumentService documentService; DocumentService documentService;
/**
* Runs with Manager access
*
* @param metricData
*/
public void saveMetric(ItemCollection metricData) {
documentService.save(metricData);
}
/** /**
* This helper method reads a 'dirty' workitem in a new transaction. This is * This helper method reads a 'dirty' workitem in a new transaction. This is
* used for calculating the new metric values * used for calculating the new metric values
@ -55,53 +42,6 @@ public class MetricDataService {
return dirtyInvoice; return dirtyInvoice;
} }
/**
* This method deletes all metrics
*
* @throws PluginException
*
*/
@TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW)
public void deleteAllMetrics(String metricType) throws PluginException {
try {
String query = "(type:" + metricType + ")";
List<ItemCollection> result = documentService.find(query, SearchService.DEFAULT_MAX_SEARCH_RESULT, 0);
for (ItemCollection metric : result) {
documentService.remove(metric);
}
} catch (IllegalArgumentException | QueryException e) {
throw new PluginException(MetricDataService.class.getName(),
"Failed to delete metrics", e.getMessage(), e);
}
}
/**
* This method deletes all metrics for a given BP ID
*
* @throws PluginException
*
*/
@TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW)
public List<String> deleteAllMetricsByBPID(String metricType, String bpID) throws PluginException {
List<String> result = new ArrayList<>();
try {
String query = "(type:" + metricType + ")";
List<ItemCollection> metricList = documentService.find(query, SearchService.DEFAULT_MAX_SEARCH_RESULT, 0);
for (ItemCollection metric : metricList) {
if (metric.getItemValueString("bp.id").equals(bpID)) {
result.add(metric.getItemValueString("name"));
documentService.remove(metric);
}
}
} catch (IllegalArgumentException | QueryException e) {
throw new PluginException(MetricDataService.class.getName(),
"Failed to delete metrics", e.getMessage(), e);
}
return result;
}
/** /**
* Builds the metric hash key by the invoice attributes. The returned key can be * Builds the metric hash key by the invoice attributes. The returned key can be
* used for caching the metric. * used for caching the metric.

View file

@ -1,6 +1,5 @@
package com.alexanderlogistics.metrics; package com.alexanderlogistics.metrics;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -15,7 +14,6 @@ import jakarta.ejb.Stateless;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@ -64,9 +62,6 @@ public class MetricDebitorRestService {
// first clear the metric cache // first clear the metric cache
metricDebitorService.reset(); metricDebitorService.reset();
logger.info("│   ├── reset metric cache"); logger.info("│   ├── reset metric cache");
// run in new transaction!
metricDataService.deleteAllMetrics(MetricDebitorService.TYPE_METRIC_DEBITOR);
logger.info("│   ├── delete metrics");
computeMetrics(); computeMetrics();
logger.info("│   ├── computing metrics finished in " + (System.currentTimeMillis() - l) + "ms"); logger.info("│   ├── computing metrics finished in " + (System.currentTimeMillis() - l) + "ms");
@ -89,92 +84,6 @@ public class MetricDebitorRestService {
} }
} }
/**
* This method initializes the metrics for all creditors with open invoices.
*
* The method deletes all existing metrics and creates new metric entires for
* each creditor.
*
* @return
*/
@GET
@Path("/rebuild/{dbtrnumber}")
@Produces({ MediaType.TEXT_PLAIN })
public Response rebuildMetricsForDebitor(@PathParam("dbtrnumber") String dbtrnumber) {
StringBuffer messageBuffer = new StringBuffer();
long l = System.currentTimeMillis();
log("├── init dbtr metrics for " + dbtrnumber + "...", messageBuffer);
try {
logger.info("│   │   ├── group invoices by creditor " + dbtrnumber + "...");
int count = 0;
// First we need to remove all metrics for this creditor
String bpID = InvoiceUtil.buildBPID(dbtrnumber);
logger.info("│   │   ├── delete metrics for " + bpID + "...");
List<String> oldMetricKeys = metricDataService
.deleteAllMetricsByBPID(MetricDebitorService.TYPE_METRIC_DEBITOR, bpID);
logger.info("│   │   ├── found " + oldMetricKeys.size() + " metrics for " + bpID + "...");
metricDebitorService.reset(oldMetricKeys);
// next rebuild gauges and metrics for this creditor
String queryDbtrNumber = dbtrnumber;
if (queryDbtrNumber.startsWith("D")) {
queryDbtrNumber = queryDbtrNumber.substring(1); // DAS IST DER CARGOSOFT IRRSINN
}
String query = "$modelversion:rechnungsausgang-* AND type:workitem " +
" AND type:workitem AND dbtr.number:" + queryDbtrNumber;
logger.fine("Query = " + query);
List<ItemCollection> invoices = documentService.find(
query,
9999, 0, "invoice.number", false);
logger.info("│   │   ├──found " + invoices.size() + " open invoices");
List<String> newMetricKeys = new ArrayList<>();
for (ItemCollection invoice : invoices) {
try {
ItemCollection metricData = metricDebitorService.getMetricByInvoice(invoice);
String metricKey = metricData.getItemValueString("name");
newMetricKeys.add(metricKey);
// Jetzt Rechnung addieren
logger.info("│   │   │   ├──update metric (" + metricKey + ")" + InvoiceUtil.getBPId(invoice)
+ " Invoice: "
+ invoice.getItemValueString("invoice.number") + " Saldo: "
+ invoice.getItemValueDouble(MetricDebitorService.ITEM_SALDO));
logger.info("│   │   │   │   ├── last metric saldo: "
+ metricData.getItemValueDouble(MetricDebitorService.ITEM_SALDO));
metricDebitorService.addInvoice(metricData, invoice);
logger.info("│   │   │   │   ├── new metric saldo: "
+ metricData.getItemValueDouble(MetricDebitorService.ITEM_SALDO));
metricDebitorService.putMetric(metricData);
count++;
} catch (PluginException e) {
// invalid invoice - e.g. no dbtr. number
}
}
logger.info("│   │   ├── updated metric for " + count + " invoices.");
log("│   ├── computing metrics finished in " + (System.currentTimeMillis() - l) + "ms", messageBuffer);
logger.info("│   ├── save and init metrics...");
// run in new transaction!
metricDebitorService.refreshGauges(newMetricKeys);
String message = "├── rebuild dbtr metrics completed in "
+ (System.currentTimeMillis() - l)
+ "ms";
log(message, messageBuffer);
return Response.ok().entity(messageBuffer.toString()).build();
} catch (Exception e) {
e.printStackTrace();
return Response.serverError()
.entity("Failed to initialize metrics: " + e.getMessage())
.build();
}
}
/** /**
* Diese Methode berechnet alle Metriken auf basis der existierenden Rechnungen * Diese Methode berechnet alle Metriken auf basis der existierenden Rechnungen
* neu * neu
@ -199,7 +108,7 @@ public class MetricDebitorRestService {
// Jetzt Rechnung addieren // Jetzt Rechnung addieren
metricDebitorService.addInvoice(metricData, invoice); metricDebitorService.addInvoice(metricData, invoice);
logger.info("│   │   │   ├──update metric " + InvoiceUtil.getBPId(invoice)); logger.info("│   │   │   ├──update metric " + InvoiceUtil.getBPId(invoice));
metricDebitorService.putMetric(metricData); metricDebitorService.updateMetric(metricData);
count++; count++;
} catch (PluginException e) { } catch (PluginException e) {
// invalid invoice - e.g. no dbtr. number // invalid invoice - e.g. no dbtr. number

View file

@ -16,15 +16,12 @@ import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.ProcessingEvent; import org.imixs.workflow.engine.ProcessingEvent;
import org.imixs.workflow.exceptions.PluginException; import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.QueryException;
import com.alexanderlogistics.InvoiceUtil; import com.alexanderlogistics.InvoiceUtil;
import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs; import jakarta.annotation.security.RunAs;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes; import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@ -56,7 +53,10 @@ public class MetricDebitorService {
private final Set<String> registeredGauges = ConcurrentHashMap.newKeySet(); private final Set<String> registeredGauges = ConcurrentHashMap.newKeySet();
public static final String TYPE_METRIC_DEBITOR = "metric.debitor"; public static final String TYPE_METRIC_DEBITOR = "metric.debitor";
public static final String ITEM_SALDO = "invoice.saldo"; public static final String ITEM_INVOICE_TOTAL = "invoice.total";
public static final String ITEM_INVOICE_SALDO = "invoice.saldo";
public static final String ITEM_METRIC_BALANCE = "invoice.balance";
public static final String ITEM_METRIC_SALES = "invoice.sales";
@Inject @Inject
@RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE) @RegistryScope(scope = MetricRegistry.APPLICATION_SCOPE)
@ -80,17 +80,6 @@ public class MetricDebitorService {
registeredGauges.clear(); registeredGauges.clear();
} }
/**
* Reset the internal metricCache and clears all registered Gauges for a list of
* metric keys.
*/
public void reset(List<String> metricKeys) {
for (String metricKey : metricKeys) {
metricCache.remove(metricKey);
metricRegistry.remove(metricKey);
}
}
/** /**
* Process Metric only if some data has changed.... * Process Metric only if some data has changed....
* *
@ -116,9 +105,10 @@ public class MetricDebitorService {
if (lastInvoice != null) { if (lastInvoice != null) {
try { try {
ItemCollection lastMetricData = getMetricByInvoice(lastInvoice); ItemCollection lastMetricData = getMetricByInvoice(lastInvoice);
subtractInvoice(lastMetricData, lastInvoice); if (!isNewMetric(lastMetricData)) {
putMetric(lastMetricData); subtractInvoice(lastMetricData, lastInvoice);
metricDataService.saveMetric(lastMetricData); updateMetric(lastMetricData);
}
} catch (PluginException e) { } catch (PluginException e) {
// invalid invoice - e.g. no cdtr. number // invalid invoice - e.g. no cdtr. number
} }
@ -129,11 +119,8 @@ public class MetricDebitorService {
ItemCollection metricData = getMetricByInvoice(invoice); ItemCollection metricData = getMetricByInvoice(invoice);
// Saldo-Berechnung // Saldo-Berechnung
addInvoice(metricData, invoice); addInvoice(metricData, invoice);
putMetric(metricData); updateMetric(metricData);
metricDataService.saveMetric(metricData);
// Update the Gauge
updateGauge(metricData);
logger.info("Metric dbtr update took " + (System.currentTimeMillis() - l) + "ms"); logger.info("Metric dbtr update took " + (System.currentTimeMillis() - l) + "ms");
} catch (PluginException e) { } catch (PluginException e) {
// invalid invoice - e.g. no dbtr. number // invalid invoice - e.g. no dbtr. number
@ -142,6 +129,17 @@ public class MetricDebitorService {
} }
/**
* Returns true if the metric is not yet registered. This means we do not have
* sales or balances for this metric
*
* @param metricData
* @return
*/
public boolean isNewMetric(ItemCollection metricData) {
return (!metricCache.containsKey(metricData.getItemValueString("name")));
}
/** /**
* Returns the corresponding metric data object for an invoice. The method uses * 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 * an internal cache. If the metric was not yet cached the method loads the
@ -161,9 +159,6 @@ public class MetricDebitorService {
} }
String metricKey = MetricDataService.buildKeyByInvoice(invoice); String metricKey = MetricDataService.buildKeyByInvoice(invoice);
ItemCollection metricData = metricCache.get(metricKey); ItemCollection metricData = metricCache.get(metricKey);
if (metricData == null) {
metricData = loadMetric(invoice);
}
// if metric still null we create a new metric data object. // if metric still null we create a new metric data object.
if (metricData == null) { if (metricData == null) {
metricData = createMetaData(invoice); metricData = createMetaData(invoice);
@ -181,15 +176,6 @@ public class MetricDebitorService {
return metricCache.get(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. * Returns a list with all cached metric keys.
* The method returns an unmodifiable list to prevent modifications. * The method returns an unmodifiable list to prevent modifications.
@ -200,34 +186,6 @@ public class MetricDebitorService {
return Collections.unmodifiableList(new ArrayList<>(metricCache.keySet())); 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 debitorMetric = null;
if (invoice == null) {
return null;
}
try {
String metricKey = MetricDataService.buildKeyByInvoice(invoice);
String query = "(type:" + TYPE_METRIC_DEBITOR + ") AND (name:" + metricKey + ")";
List<ItemCollection> result = documentService.find(query, 1, 0, "$modified", true);
if (result.size() > 0) {
debitorMetric = 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 debitorMetric;
}
/** /**
* Creates an empty Debitor Meta Data Object (ItemCollection) * Creates an empty Debitor Meta Data Object (ItemCollection)
* <p> * <p>
@ -260,17 +218,20 @@ public class MetricDebitorService {
* *
* @param metricData - the metricData ItemCollection * @param metricData - the metricData ItemCollection
*/ */
public void updateGauge(ItemCollection metricData) { public void updateMetric(ItemCollection metricData) {
String metricKey = metricData.getItemValueString("name"); String metricKey = metricData.getItemValueString("name");
String bpName = metricData.getItemValueString("bp.name");
String bpId = metricData.getItemValueString("bp.id"); // Cache aktualisieren
String country = metricData.getItemValueString("country"); metricCache.put(metricKey, metricData);
String department = metricData.getItemValueString("department");
String currency = metricData.getItemValueString("currency");
// Prüfen ob Gauge bereits registriert ist // Prüfen ob Gauge bereits registriert ist
if (registeredGauges.add(metricKey)) { // returns true newly added if (registeredGauges.add(metricKey)) { // returns true newly added
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");
List<Tag> tags = new ArrayList<>(); List<Tag> tags = new ArrayList<>();
tags.add(new Tag("type", "dbtr")); tags.add(new Tag("type", "dbtr"));
tags.add(new Tag("id", bpId)); tags.add(new Tag("id", bpId));
@ -279,19 +240,25 @@ public class MetricDebitorService {
tags.add(new Tag("currency", currency)); tags.add(new Tag("currency", currency));
tags.add(new Tag("department", department)); tags.add(new Tag("department", department));
logger.fine("register new metric for department: " + department + logger.fine("register new metric for department: " + department +
", " + metricData.getItemValueString(ITEM_SALDO) + ", " + metricData.getItemValueString(ITEM_METRIC_BALANCE) +
" " + currency); " " + currency);
Metadata metadata = Metadata.builder() // Saldo Gauge
Metadata balanceMetadata = Metadata.builder()
.withName("dbtr.balance") .withName("dbtr.balance")
.withDescription("Debitor Balance") .withDescription("Debitor Balance")
.build(); .build();
metricRegistry.gauge(balanceMetadata,
metricCache.get(metricKey); () -> metricCache.get(metricKey).getItemValueDouble(ITEM_METRIC_BALANCE),
metricRegistry.gauge(metadata, tags.toArray(new Tag[0]));
() -> metricCache.get(metricKey).getItemValueDouble(ITEM_SALDO),
// Umsatz Gauge
Metadata revenueMetadata = Metadata.builder()
.withName("dbtr.sales")
.withDescription("Debitor Sales")
.build();
metricRegistry.gauge(revenueMetadata,
() -> metricCache.get(metricKey).getItemValueDouble(ITEM_METRIC_SALES),
tags.toArray(new Tag[0])); tags.toArray(new Tag[0]));
} else {
logger.fine("Gauge already registered for department: " + department);
} }
} }
@ -303,64 +270,59 @@ public class MetricDebitorService {
* @param invoice * @param invoice
*/ */
public void addInvoice(ItemCollection metricData, ItemCollection invoice) { public void addInvoice(ItemCollection metricData, ItemCollection invoice) {
double invoiceTotal = invoice.getItemValueDouble(ITEM_SALDO); double invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_SALDO);
double invoiceTotal = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) { if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) {
// vorgang ist archiviert oder gelöscht worden => saldo = 0! // vorgang ist archiviert oder gelöscht worden => saldo = 0!
invoiceTotal = 0.0; invoiceSaldo = 0.0;
} }
logger.fine("│   │   │   │   ├── Invoice: " + invoice.getItemValueString("invoice.number") + " Saldo=" logger.fine("│   │   │   │   ├── Invoice: " + invoice.getItemValueString("invoice.number") + " Saldo="
+ invoiceTotal); + invoiceSaldo);
// is the metric new? // update saldo
if (!metricData.hasItem(ITEM_SALDO)) { double lastSaldo = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
// init metric with the total value! logger.fine("│   │   │   │   ├── last metric balance=" + lastSaldo);
metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(invoiceTotal)); metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastSaldo + invoiceSaldo));
} else {
double lastSaldo = metricData.getItemValueDouble(ITEM_SALDO); // Umsatz-Berechnung
logger.fine("│   │   │   │   ├──letzter Metric Saldo=" + lastSaldo); double lastTotal = metricData.getItemValueDouble(ITEM_METRIC_SALES);
metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(lastSaldo + invoiceTotal)); metricData.setItemValue(ITEM_METRIC_SALES, InvoiceUtil.round(lastTotal + invoiceTotal));
}
} }
public void subtractInvoice(ItemCollection metricData, ItemCollection invoice) { public void subtractInvoice(ItemCollection metricData, ItemCollection invoice) {
if (metricData == null || invoice == null) { if (metricData == null || invoice == null) {
return; return;
} }
double invoiceTotal = invoice.getItemValueDouble(ITEM_SALDO); double invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_SALDO);
double invoiceTotal = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
logger.fine("│   │   │   │   ├──Invoice: " + invoice.getItemValueString("invoice.number") + " Saldo=" logger.fine("│   │   │   │   ├──Invoice: " + invoice.getItemValueString("invoice.number") + " Saldo="
+ invoiceTotal); + invoiceSaldo);
if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) { if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) {
// vorgang ist archiviert oder gelöscht worden => saldo = 0! // vorgang ist archiviert oder gelöscht worden => saldo = 0!
invoiceTotal = 0.0; invoiceSaldo = 0.0;
} }
// subtract only if metric saldo exists // subtract only if metric saldo exists
if (metricData.hasItem(ITEM_SALDO)) { double lastSaldo = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
double lastSaldo = metricData.getItemValueDouble(ITEM_SALDO); logger.fine("│   │   │   │   ├── last Metric balance=" + lastSaldo);
logger.fine("│   │   │   │   ├──letzter Metric Saldo=" + lastSaldo); metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastSaldo - invoiceSaldo));
metricData.setItemValue(ITEM_SALDO, InvoiceUtil.round(lastSaldo - invoiceTotal));
} // update Umsatz
double lastTotal = metricData.getItemValueDouble(ITEM_METRIC_SALES);
metricData.setItemValue(ITEM_METRIC_SALES, InvoiceUtil.round(lastTotal - invoiceTotal));
} }
/** /**
* Helper Method that refreshes all gauges. The method is called by the * Helper Method that refreshes all gauges. The method is called by the
* RestService during a rebuild. * RestService during a rebuild.
*/ */
@TransactionAttribute(value = TransactionAttributeType.REQUIRES_NEW)
public void refreshGauges() { public void refreshGauges() {
List<String> keys = getMetricKeys(); List<String> 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<String> keys) {
for (String hashKey : keys) { for (String hashKey : keys) {
ItemCollection metricData = getMetric(hashKey); ItemCollection metricData = getMetric(hashKey);
documentService.save(metricData); updateMetric(metricData);
updateGauge(metricData);
} }
} }