package org.sharegov.cirm.stats; import java.util.Date; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import org.sharegov.cirm.utils.GenUtils; import mjson.Json; /** * CiRMStatistics class, keeping track of success, failure, lastSuccess, lastFailure and SR-Numbers for each interface and action including CIRMCLIENT. * Allows for aggregated sorted views. * * Example Usage: * To collect stats: * 1. StatsEntry se = Refs.stats().getEntry(CIRMCLIENT, RX_NEW_SR, type | UNKNOWN) * 2. se.addSuccess(Case_NUmber) | se.addFailure(...) * ---- * To retrieve stats: * A) getEntry for specific stats * B) getStatistics for all stats * C1) getAggregatedStatisticsFor(ALL, ALL, ALL) = all failures/succeess + the very last success, failure time, SR and kind. * C2) getAggregatedStatisticsFor(PWS, ALL, ALL) = all failures/succeess for PWS * D1) getAggregatedStatisticsFor(EACH, EACH, EACH) is equivalent to getStatistics() (but slower) * D2) getAggregatedStatisticsFor(EACH, ALL, ALL) = aggregated failures/succeess by each component (e.g. interface) + the last success, failure time, SR and kind for each component. * D3) getAggregatedStatisticsFor(ALL, ALL, EACH) = aggregated failures/succeess by each type (e.g. sr type) + the last success, failure time, SR and kind for each type. * * @author Thomas Hilpold * */ public class CirmStatistics { /* * Example interface names * CIRMCLIENT, PWS, WCS, COM, CMS, UNKNOWN, ALL * CIRMCLIENT = createNew or update and originator user */ /* * Exemple interface actions: * TX_NEW_SR, //Cirm sends a new SR to an interface TX_UPDATE_SR, TX_NEW_ACTIVITY, TX_RESPONSE_NEW_SR, //Cirm sends the response after receiving a new SR from an interface to the interface TX_RESPONSE_UPDATE_SR, TX_RESPONSE_NEW_ACTIVITY, RX_NEW_SR, //Web also RX_UPDATE_SR, //Web also RX_NEW_ACTIVITY, //never Web RX_RESPONSE_NEW_SR, RX_RESPONSE_UPDATE_SR, RX_RESPONSE_NEW_ACTIVITY, //12 interface actions total UNKNOWN, ALL */ public final static String UNKNOWN = "UNKNOWN"; public final static String ALL = "ALL"; public final static String EACH = "EACH"; private volatile ConcurrentHashMap<StatsKey, StatsValue> statsEntryMap = new ConcurrentHashMap<StatsKey, CirmStatistics.StatsValue>(1001); /** * Clears all results collected so far. * Thread safe. */ public void clear() { statsEntryMap.clear(); } /** * Retrieve a stats entry for adding success or failure now or later. * Stats entry access is blocking. * * THIS IS THREAD SAFE and non blocking. * @param name null safe, but should be avoided -> unknown is set * @param action null safe, but should be avoided -> unknown is set * @param type null safe, but should be avoided -> unknown is set * @return */ StatsValue getEntry(String component, String action, String type) { if (component == null) component = UNKNOWN; if (action == null) action = UNKNOWN; if (type == null) type = UNKNOWN; StatsKey findKey = new StatsKey(component, action, type); StatsValue entry = statsEntryMap.get(findKey); if (entry == null) { entry = new StatsValue(); statsEntryMap.put(findKey, entry); } return entry; } /** * Returns the a copy of the statistic entry map as sorted treemap - Expensive!. * Entries may change while iterating over the map. * Statistic Entry map may change after this method returns. * The Treemap will not be modified after this methos returns. * Sort order is defined in Key class compareTo method. * @return */ public TreeMap<StatsKey, StatsValue> getStatistics() { TreeMap<StatsKey, StatsValue> treeMap = new TreeMap<StatsKey, CirmStatistics.StatsValue>(); //weakly consistent but thread and concurrent modification safe traversal treeMap.putAll(statsEntryMap); return treeMap; } /** * Returns the a copy of the statistic entry map as sorted treemap, applying aggregation as defined by the parameters - Expensive!. * For example: if the comparator defines all as 0, the resulting treemap will contain only one key/entry pair, which aggregates all statsEntries. * (In this case the Key will be (UNKNOWN, UNKNOWN, UNKNOWN) * If the comparator defines all types as 0, the tree will contain as many keys as different Modules/Actions are contained, but all type specific entries will be aggregated. * A typical key would read (WEB, RX_UPDATE, UNKNOWN). * * This method is NOT thread safe, use external synchronisation. * * @param component null NOT allowed, use constants ALL or -UNKNOWN * @param action null NOT allowed, use constants ALL or -UNKNOWN * @param type null NOT allowed, use constants SR_TYPE_ALL or -UNKNOWN * @return */ public TreeMap<StatsKey, StatsValue> getAggregatedStatisticsFor(String component, String action, String type) { if (component == null) throw new NullPointerException("name"); if (action == null) throw new NullPointerException("action"); if (type == null) throw new NullPointerException("type"); StatsKey filterKey = new StatsKey(component, action, type); TreeMap<StatsKey, StatsValue> aggregateStatsTreeMap = new TreeMap<StatsKey, CirmStatistics.StatsValue>(); //weakly consistent but thread and concurrent modification safe traversal (see ConcurrentHashMap) //Iterate over statsEntryMap to find matches to filterKey and aggregate the values StatsKey curAggregateKey = filterKey; for (Map.Entry<StatsKey, StatsValue> statsKeyValue : statsEntryMap.entrySet()) { //Determine, if Entry shall be considered for aggregation StatsKey curStatsKey = statsKeyValue.getKey(); if (isKeyMatched(filterKey, curStatsKey)) { curAggregateKey = getAggregateKeyFor(filterKey, curAggregateKey, curStatsKey); StatsValue aggregateEntry = aggregateStatsTreeMap.get(curAggregateKey); if (aggregateEntry == null) { aggregateEntry = new StatsValue(); //insert first new value as basis for aggregation aggregateStatsTreeMap.put(curAggregateKey, aggregateEntry); } //once it's there just add to it aggregateEntry.aggregate(statsKeyValue.getValue()); } // else skip entry } return aggregateStatsTreeMap; } /** * Returns an aggregate key necessary during EACH queries or either component, action or type. * If no parameter in the filterKey has value EACH, the given curKey is returned. * * @param filterKey * @param curAggregateKey * @param statsKey * @return */ private StatsKey getAggregateKeyFor(StatsKey filterKey, StatsKey curAggregateKey, StatsKey statsKey) { StatsKey result = curAggregateKey; boolean componentEach = EACH.equals(filterKey.getComponent()); boolean actionEach = EACH.equals(filterKey.getAction()); boolean typeEach = EACH.equals(filterKey.gettype()); boolean newComponentEach = (componentEach && !curAggregateKey.getComponent().equals(statsKey.getComponent())); boolean newActionEach = (actionEach && !curAggregateKey.getAction().equals(statsKey.getAction())); boolean newTypeEach = (typeEach && !curAggregateKey.gettype().equals(statsKey.gettype())); if (newComponentEach || newActionEach || newTypeEach) { //new key object necessary String component = componentEach? statsKey.getComponent() : curAggregateKey.getComponent(); String action = actionEach? statsKey.getAction(): curAggregateKey.getAction(); String type = typeEach? statsKey.gettype(): curAggregateKey.gettype(); result = new StatsKey(component, action, type); } return result; } /** * Determines if an existing key matches a filter given as key. * @param filterKey * @param existingKey * @return */ private boolean isKeyMatched(StatsKey filterKey, StatsKey existingKey) { boolean result = false; boolean nameMatch = ALL.equals(filterKey.getComponent()) || EACH.equals(filterKey.getComponent()) || filterKey.getComponent().equals(existingKey.getComponent()); if (nameMatch) { boolean actionMatch = ALL.equals(filterKey.getAction()) || EACH.equals(filterKey.getAction()) || filterKey.getAction().equals(existingKey.getAction()); if (actionMatch) { result = ALL.equals(filterKey.gettype()) || EACH.equals(filterKey.gettype()) || filterKey.gettype().equals(existingKey.gettype()); } } return result; } /** * Sortable Key for interface entries ordered by interfaceName, -action and type. * Not hashable, no equals overwrite. * * Thread safe. * * @author Thomas Hilpold * */ public static class StatsKey implements Comparable<StatsKey> { private final String component; private final String action; private final String type; private final int hashCode; /** * Creates a sortable key for stats entries. * @param component if null, UNKNOWN will be set * @param action if null, UNKNOWN will be set * @param type, if null, UNKNOWN will be set */ StatsKey(String component, String action, String type) { //defensive creation to prevent exception during stats operations //users can find trouble spots in the code later by reading the results which should never //contain any UNKNOW category. if (component == null) component = UNKNOWN; if (action == null) action = UNKNOWN; if (type == null) type = UNKNOWN; //assert no null to be set to avoid compareTo having to deal with null order this.component = component; this.action = action; this.type = type; //all final, determine constant hashcode this.hashCode = 5 * component.hashCode() + 3 * action.hashCode() + type.hashCode(); } /** * @return a string json "/" delimited */ public Json toJson() { String result = getComponent().toString() + "/" + getAction().toString() + "/" + type; return Json.make(result); } public final String getComponent() { return component; } public final String getAction() { return action; } public final String gettype() { return type; } public boolean equals(Object other) { if (other == null || !(other instanceof StatsKey)) return false; else { StatsKey otherKey = (StatsKey)other; return getComponent().equals(otherKey.getComponent()) && getAction().equals(otherKey.getAction()) && gettype().equals(otherKey.gettype()); } } @Override public int hashCode() { return this.hashCode; } @Override public int compareTo(StatsKey o) { int result; if (o == null) { //nulls come first, if any result = -1; } else { int interfaceNameOrder = this.getComponent().compareTo(o.getComponent()); if (interfaceNameOrder != 0) { result = interfaceNameOrder; } else { int interfaceActionOrder = this.getAction().compareTo(o.getAction()); if (interfaceActionOrder != 0) { result = interfaceActionOrder; } else { int typeKey = this.gettype().compareTo(o.gettype()); result = typeKey; } } } return result; } } /** * Statistics entry for success or failure of interface operations by type. * * THIS CLASS IS THREAD SAFE AND BLOCKING. * * It is safe for any thread to call any method on an object of this class. * * @author Thomas Hilpold * */ public static class StatsValue { // private Date firstEntryTime; //time at which first success or failure is determined private long successCount; private Date lastSuccessTime; private String lastSuccessId; private long failureCount; private Date lastFailureTime; private String lastFailureId; private String lastFailureException; private String lastFailureMessage; /** * Reports success for this id and increases successCount. * @param id */ public synchronized void addSuccess(String id) { Date now = new Date(); if (getFirstEntryTime() == null) setFirstEntryTime(now); setLastSuccessId(id); setLastSuccessTime(now); setSuccessCount(getSuccessCount() + 1); } /** * Reports failure for this id and increases failureCount. * @param id * @param exception * @param failureMessage */ public synchronized void addFailure(String id, String exception, String failureMessage) { Date now = new Date(); if (getFirstEntryTime() == null) setFirstEntryTime(now); setLastFailureId(id); setLastFailureTime(now); setLastFailureException(exception); setLastFailureMessage(failureMessage); setFailureCount(getFailureCount() + 1); } /** * For building grouped stats by various criteria. * Time durations get expanded by setting earlier first and later last time. * @param other */ public synchronized void aggregate(StatsValue other) { setSuccessCount(getSuccessCount() + other.getSuccessCount()); setFailureCount(getFailureCount() + other.getFailureCount()); if (other.getFirstEntryTime() != null) { if (getFirstEntryTime() == null || other.getFirstEntryTime().before(getFirstEntryTime())) { setFirstEntryTime(other.getFirstEntryTime()); } } if (other.getLastSuccessTime() != null) { if (getLastSuccessTime() == null || other.getLastSuccessTime().after(getLastSuccessTime())) { setLastSuccessTime(other.getLastSuccessTime()); setLastSuccessId(other.getLastSuccessId()); } } if (other.getLastFailureTime() != null) { if (getLastFailureTime() == null || other.getLastFailureTime().after(getLastFailureTime())) { setLastFailureTime(other.getLastFailureTime()); setLastFailureId(other.getLastFailureId()); setLastFailureException(other.getLastFailureException()); setLastFailureMessage(other.getLastFailureMessage()); } } } /** * Returns a json representation of this StatsValue as a json object. * @return */ public synchronized Json toJson() { Json result = Json.object(); result.set("firstEntryTime", getFirstEntryTime() != null ? GenUtils.formatDate(getFirstEntryTime()) : null); result.set("successCount", getSuccessCount()); result.set("lastSuccessTime", getLastSuccessTime() != null ? GenUtils.formatDate(getLastSuccessTime()) : null); result.set("lastSuccessId", getLastSuccessId()); result.set("failureCount", getFailureCount()); result.set("lastFailureTime", getLastFailureTime() != null ? GenUtils.formatDate(getLastFailureTime()) : null); result.set("lastFailureId", getLastFailureId()); result.set("lastFailureException", getLastFailureException()); result.set("lastFailureMessage", getLastFailureMessage()); return result; } public synchronized final Date getFirstEntryTime() { return firstEntryTime; } public synchronized final void setFirstEntryTime(Date firstEntryTime) { this.firstEntryTime = firstEntryTime; } public synchronized final long getSuccessCount() { return successCount; } public synchronized final void setSuccessCount(long successCount) { this.successCount = successCount; } public synchronized final Date getLastSuccessTime() { return lastSuccessTime; } public synchronized final void setLastSuccessTime(Date lastSuccessTime) { this.lastSuccessTime = lastSuccessTime; } public synchronized final String getLastSuccessId() { return lastSuccessId; } public synchronized final void setLastSuccessId(String lastSuccessId) { this.lastSuccessId = lastSuccessId; } public synchronized final long getFailureCount() { return failureCount; } public synchronized final void setFailureCount(long failureCount) { this.failureCount = failureCount; } public synchronized final Date getLastFailureTime() { return lastFailureTime; } public synchronized final void setLastFailureTime(Date lastFailureTime) { this.lastFailureTime = lastFailureTime; } public synchronized final String getLastFailureId() { return lastFailureId; } public synchronized final void setLastFailureId(String lastFailureId) { this.lastFailureId = lastFailureId; } public synchronized final String getLastFailureException() { return lastFailureException; } public synchronized final void setLastFailureException(String lastFailureException) { this.lastFailureException = lastFailureException; } public synchronized final String getLastFailureMessage() { return lastFailureMessage; } public synchronized final void setLastFailureMessage(String lastFailureMessage) { this.lastFailureMessage = lastFailureMessage; } } }