382 lines
No EOL
15 KiB
Java
382 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 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 Debitoren Metric Entity (type=metric.debitor).
|
|
* <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 MetricDebitorService {
|
|
|
|
private static Logger logger = Logger.getLogger(MetricDebitorService.class.getName());
|
|
private final ConcurrentHashMap<String, ItemCollection> metricCache = new ConcurrentHashMap<>();
|
|
private final Set<String> registeredGauges = ConcurrentHashMap.newKeySet();
|
|
|
|
public static final String TYPE_METRIC_DEBITOR = "metric.debitor";
|
|
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
|
|
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 debitor metrics from database...");
|
|
String query = "(type:" + TYPE_METRIC_DEBITOR + ")";
|
|
|
|
// 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 internal metricCache and clear 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 (!invoice.getModelVersion().startsWith("rechnungsausgang-")) {
|
|
// 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);
|
|
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 dbtr update took " + (System.currentTimeMillis() - l) + "ms");
|
|
} catch (PluginException e) {
|
|
// invalid invoice - e.g. no dbtr. 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 dbtr.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 = createMetaData(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 Debitor Meta Data Object (ItemCollection)
|
|
* <p>
|
|
* 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_DEBITOR);
|
|
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", "dbtr"));
|
|
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_METRIC_BALANCE) +
|
|
" " + currency);
|
|
|
|
// Saldo Gauge
|
|
Metadata balanceMetadata = Metadata.builder()
|
|
.withName("dbtr.balance")
|
|
.withDescription("Debitor Balance")
|
|
.build();
|
|
metricRegistry.gauge(balanceMetadata,
|
|
() -> metricCache.get(metricKey).getItemValueDouble(ITEM_METRIC_BALANCE),
|
|
tags.toArray(new Tag[0]));
|
|
|
|
// 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]));
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Addiert den saldo einer Invoice zu einem metricData object
|
|
*
|
|
* @param metricData
|
|
* @param invoice
|
|
*/
|
|
public void addInvoice(ItemCollection metricData, ItemCollection invoice) {
|
|
double invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_SALDO);
|
|
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;
|
|
}
|
|
|
|
logger.fine("│ │ │ │ ├── Invoice: " + invoice.getItemValueString("invoice.number") + " Saldo="
|
|
+ invoiceSaldo);
|
|
// update saldo
|
|
double lastSaldo = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
|
|
logger.fine("│ │ │ │ ├── last metric balance=" + lastSaldo);
|
|
metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastSaldo + invoiceSaldo));
|
|
|
|
// Umsatz-Berechnung
|
|
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 invoiceSaldo = invoice.getItemValueDouble(ITEM_INVOICE_SALDO);
|
|
double invoiceTotal = invoice.getItemValueDouble(ITEM_INVOICE_TOTAL);
|
|
logger.fine("│ │ │ │ ├──Invoice: " + invoice.getItemValueString("invoice.number") + " Saldo="
|
|
+ invoiceSaldo);
|
|
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 lastSaldo = metricData.getItemValueDouble(ITEM_METRIC_BALANCE);
|
|
logger.fine("│ │ │ │ ├── last Metric balance=" + lastSaldo);
|
|
metricData.setItemValue(ITEM_METRIC_BALANCE, InvoiceUtil.round(lastSaldo - invoiceSaldo));
|
|
|
|
// update Umsatz
|
|
double lastTotal = metricData.getItemValueDouble(ITEM_METRIC_SALES);
|
|
metricData.setItemValue(ITEM_METRIC_SALES, InvoiceUtil.round(lastTotal - invoiceTotal));
|
|
|
|
}
|
|
|
|
} |