package org.araqne.logdb.query.command; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.araqne.logdb.QueryCommand; import org.araqne.logdb.QueryStopReason; import org.araqne.logdb.Row; import org.araqne.logdb.TimeSpan; import org.slf4j.LoggerFactory; public class Transaction extends QueryCommand { private final org.slf4j.Logger logger = LoggerFactory.getLogger(Transaction.class); private static class XactContext { private Date minTime; private Date maxTime; private List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>(4); } private ConcurrentMap<List<Object>, XactContext> transactions = new ConcurrentHashMap<List<Object>, XactContext>(); public static class TransactionOptions { public TimeSpan maxSpan; public TimeSpan maxPause; public int maxEvents; // TODO: use expression public String startsWith; public String endsWith; } private TransactionOptions txOptions; private List<String> fields; public Transaction(TransactionOptions txOptions, List<String> fields) { this.txOptions = txOptions; this.fields = fields; } @Override public String getName() { return "transaction"; } /** * assume that onPush() is called in descending order */ @Override public void onPush(Row row) { Object o = row.get("line"); if (o == null) return; // generate tx key List<Object> keys = new ArrayList<Object>(fields.size()); for (String field : fields) { Object value = row.get(field); if (value == null) return; keys.add(value); } String line = o.toString(); XactContext ctx = transactions.get(keys); // open tx if (txOptions.endsWith != null && line.contains(txOptions.endsWith)) { if (ctx != null) { // start tx not found if transactions contains key // e.g. start end (missing!) end Row txRow = new Row(); txRow.put("_time", ctx.maxTime); txRow.put("_tx_keys", keys); txRow.put("_tx_orphan", true); txRow.put("_tx_rows", ctx.rows); pushPipe(txRow); if (logger.isDebugEnabled()) logger.debug("araqne logdb: missing transaction begin [{}]", line); } else { // open transaction ctx = new XactContext(); ctx.maxTime = (Date) row.get("_time"); ctx.rows.add(row.map()); transactions.put(keys, ctx); } } // close tx if (txOptions.startsWith != null && line.contains(txOptions.startsWith)) { if (ctx != null) { // found normal transaction start, pop transaction transactions.remove(keys); ctx.minTime = (Date) row.get("_time"); Row txRow = new Row(); txRow.put("_time", ctx.minTime); txRow.put("_tx_keys", keys); txRow.put("_tx_rows", ctx.rows); txRow.put("_tx_duration", ctx.maxTime.getTime() - ctx.minTime.getTime()); pushPipe(txRow); } else { Row txRow = new Row(); txRow.put("_time", row.get("_time")); txRow.put("_tx_keys", keys); txRow.put("_tx_orphan", true); txRow.put("_tx_rows", Arrays.asList(row.map())); pushPipe(txRow); // no matching transaction end (not yet completed orphan) if (logger.isDebugEnabled()) logger.debug("araqne logdb: running transaction [{}]", line); } } } @Override public void onClose(QueryStopReason reason) { if (reason == QueryStopReason.End) { // flush all orphans for (List<Object> keys : transactions.keySet()) { XactContext ctx = transactions.get(keys); Row txRow = new Row(); txRow.put("_time", ctx.maxTime); txRow.put("_tx_keys", keys); txRow.put("_tx_orphan", true); txRow.put("_tx_rows", ctx.rows); pushPipe(txRow); } transactions.clear(); } } }