office-alexander-logistics/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/metrics/MetricCreditorService.java
2025-02-06 15:44:33 +01:00

394 lines
No EOL
15 KiB
Java

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.archive.core.SnapshotService;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.ProcessingEvent;
import org.imixs.workflow.engine.SetupEvent;
import org.imixs.workflow.exceptions.PluginException;
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.Singleton;
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).
* <p>
* Beim Rechnungseingang gilt eine Rechnung als Bezahlt wenn diese einen finalen
* Status erreicht hat (>=5800)
* <p>
* 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")
@Singleton
@ApplicationScoped
public class MetricCreditorService {
private static Logger logger = Logger.getLogger(MetricCreditorService.class.getName());
private final ConcurrentHashMap<String, ItemCollection> metricCache = new ConcurrentHashMap<>();
private final Set<String> registeredGauges = ConcurrentHashMap.newKeySet();
public static final String TYPE_METRIC_CREDITOR = "metric.creditor";
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
@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;
/**
* Init all metrics during setup. Called by the Imixs SetupService
*/
public void initializeMetrics(@Observes SetupEvent setupEvent) {
if (!metricsEnabled) {
return;
}
long l = System.currentTimeMillis();
int batchSize = 500;
int totalMetrics = 0;
try {
logger.info("├── Initializing creditor metrics from database...");
String query = "(type:" + TYPE_METRIC_CREDITOR + ")";
// Gesamtanzahl ermitteln
int totalCount = documentService.count(query);
// Berechne Anzahl der benötigten Pages
int totalPages = (int) Math.ceil((double) totalCount / batchSize);
// Verarbeite Page für Page
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
List<ItemCollection> metrics = documentService.find(query, batchSize, pageIndex);
for (ItemCollection metric : metrics) {
updateMetric(metric, false);
totalMetrics++;
}
// Fortschritt loggen
logger.info("│ ├── Processed page " + (pageIndex + 1) + " of " + totalPages +
" (" + totalMetrics + " of " + totalCount + " metrics)");
// Optional: Kurze Pause nach jedem Batch
Thread.sleep(100);
}
long duration = System.currentTimeMillis() - l;
double metricsPerSecond = totalMetrics / (duration / 1000.0);
logger.info("├── Successfully initialized " + totalMetrics + " metrics in " +
duration + "ms (" + String.format("%.1f", metricsPerSecond) + " metrics/sec)");
} catch (Exception e) {
logger.warning("Failed to initialize metrics: " + e.getMessage());
}
}
/**
* Reset the internal metricCache and clears all registered Gauges.
*/
public void reset() {
metricCache.clear();
registeredGauges.clear();
}
public long getMetricCount() {
return metricCache.size();
}
/**
* 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);
// update last metric only if exists...
if (!isNewMetric(lastMetricData)) {
subtractInvoice(lastMetricData, lastInvoice);
updateMetric(lastMetricData, true);
}
} 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);
updateMetric(metricData, true);
logger.info("Metric cdtr update took " + (System.currentTimeMillis() - l) + "ms");
} catch (PluginException e) {
// invalid invoice - e.g. no cdtr. number
}
}
}
/**
* 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
* 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 metric still null we create a new metric data object.
if (metricData == null) {
metricData = createMetricData(invoice);
}
return metricData;
}
/**
* Returns a metric data object by key
*
* @param key
* @return
*/
public ItemCollection getMetric(String key) {
return metricCache.get(key);
}
/**
* Returns a list with all cached metric keys.
* The method returns an unmodifiable list to prevent modifications.
*
* @return
*/
public List<String> getMetricKeys() {
return Collections.unmodifiableList(new ArrayList<>(metricCache.keySet()));
}
/**
* Creates an empty Creditor Metric Data Object (ItemCollection)
* <p>
* The ItemCollection stores the name and number and all categories.
* A new metric object does not yet have the items 'invoice.saldo' and
* 'invoice.total'
*
* @param invoice - invoice ItemCollection
* @return
* @throws PluginException
*/
private ItemCollection createMetricData(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(SnapshotService.NOSNAPSHOT, true);
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;
}
/**
* This method registers and updates the metric meta data objects based on a
* given metricData object. Optional the metric data object is persisted.
*
* @param metricData - the metricData ItemCollection
* @param persist - if true the metricData entity will be persisted
*/
public void updateMetric(ItemCollection metricData, boolean persist) {
String metricKey = metricData.getItemValueString("name");
// Cache aktualisieren
metricCache.put(metricKey, metricData);
// In Datenbank persistieren
if (persist) {
metricData.setItemValue(SnapshotService.NOSNAPSHOT, true);
documentService.save(metricData);
}
// Prüfen ob Gauge bereits registriert ist
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<>();
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));
// Saldo Gauge
Metadata balanceMetadata = Metadata.builder()
.withName("cdtr.balance")
.withDescription("Creditor Balance")
.build();
metricRegistry.gauge(balanceMetadata,
() -> metricCache.get(metricKey).getItemValueDouble(ITEM_METRIC_BALANCE),
tags.toArray(new Tag[0]));
// 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]));
}
}
// /**
// * Helper Method that refreshes all gauges. The method is called by the
// * RestService during a rebuild.
// */
// public void updateAllMetrics() {
// List<String> keys = getMetricKeys();
// for (String hashKey : keys) {
// ItemCollection metricData = getMetric(hashKey);
// updateMetric(metricData, false);
// }
// }
/**
* Addiert den saldo einer Invoice zu einem metricData object
*
* @param metricData
* @param invoice
*/
public void addInvoice(ItemCollection metricData, ItemCollection invoice) {
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) {
// vorgang ist archiviert oder gelöscht worden => saldo = 0!
invoiceSaldo = 0.0;
}
// 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) {
if (metricData == null || invoice == null) {
return;
}
double invoiceTotal = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
double invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
if (!"workitem".equals(invoice.getType()) || invoice.getTaskID() >= 5800) {
// vorgang ist archiviert oder gelöscht worden => saldo = 0!
invoiceSaldo = 0.0;
}
// subtract only if metric saldo exists
double lastBalance = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastBalance - invoiceSaldo));
// Neue Umsatz Logik
double lastSales = metricData.getItemValueDouble(ITEM_METRIC_SALES);
metricData.setItemValue(ITEM_METRIC_SALES, InvoiceUtil.round(lastSales - invoiceTotal));
}
}