org.fusesource.bai.backend.mongo.MongoDBBackend.java Source code

Java tutorial

Introduction

Here is the source code for org.fusesource.bai.backend.mongo.MongoDBBackend.java

Source

/*
 * Copyright (C) FuseSource, Inc.
 * http://fusesource.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.fusesource.bai.backend.mongo;

import com.mongodb.*;
import com.mongodb.util.JSON;
import com.mongodb.util.JSONParseException;
import org.apache.camel.*;
import org.apache.camel.dataformat.xmljson.XmlJsonDataFormat;
import org.apache.camel.management.event.*;
import org.apache.camel.util.ServiceHelper;
import org.fusesource.bai.AuditConstants;
import org.fusesource.bai.AuditEvent;
import org.fusesource.bai.backend.BAIAuditBackend;
import org.fusesource.bai.backend.BAIAuditBackendSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Properties;

/**
 * MongoDB Business Activity Insight backend
 * Manages three types of collections: per-route collection, x-ray collection and debug
 * The per-route collection is the heart of BAI, and contains records following this structure:
 * { breadcrumbId: ,
 *   input: { },
 *   exchanges: { },
 *   endpointFailures: { },
 *   processorFailures: { },
 *   endpointRedeliveries: { },
 *   processorRedeliveries: { }
 * }
 * @author Raul Kripalani
 *
 */
public class MongoDBBackend extends BAIAuditBackendSupport implements BAIAuditBackend {

    private final static Logger LOG = LoggerFactory.getLogger(MongoDBBackend.class);

    private Mongo mongo;
    private String dbname;
    private DB db;
    private CamelContext context;
    private TypeConverter typeConverter;
    private Properties typeHints;
    private XmlJsonDataFormat xmlJson = new XmlJsonDataFormat();
    private boolean debug = true;

    @Override
    public void audit(AuditEvent ev) {
        String endpointId = ev.getEndpointURI();
        String srcContextId = ev.getExchange().getContext().getName();
        String srcRouteId = ev.getExchange().getFromRouteId();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Received AuditEvent: " + ev + " breadCrumbID: " + ev.getBreadCrumbId()
                    + " | Extracted data: " + endpointId + ", " + srcContextId + ", " + srcRouteId);
        }

        boolean handled = false;
        // a message is being sent
        if (ev.getEvent() instanceof ExchangeSendingEvent || ev.getEvent() instanceof ExchangeSentEvent
                || ev.getEvent() instanceof ExchangeCreatedEvent
                || ev.getEvent() instanceof ExchangeCompletedEvent) {
            digestExchangeEvent(ev);
            handled = true;
        }
        // a message has failed
        else if (ev.getEvent() instanceof ExchangeFailedEvent) {
            digestEndpointFailureEvent(ev);
            handled = true;
        }
        // a message is being redelivered
        else if (ev.getEvent() instanceof ExchangeRedeliveryEvent) {
            if (ev.getEndpointURI() != null) {
                digestEndpointRedeliveryEvent(ev);
            } else {
                digestProcessorRedeliveryEvent(ev);
            }
            handled = true;
        }

        // add the entry to the meta collection - which tells us which routes a breadcrumbId has passed through
        if (handled) {
            addToMetaCollection(ev);
        }

        // if debug is enabled, insert a record in the debug collection
        if (debug) {
            createDebugRecord(ev);
        }

    }

    private void createDebugRecord(AuditEvent ev) {
        BasicDBObject object = new BasicDBObject();
        object.append("breadCrumbId", ev.getBreadCrumbId());
        object.append("eventtype", (ev.getEvent()).getClass().getName());
        object.append("endpointURI", ev.getEndpointURI());
        object.append("exchangeId", ev.getExchange().getExchangeId());
        object.append("exception", ev.getException() == null ? null : ev.getException().toString());
        object.append("redelivered", ev.getRedelivered());
        object.append("timestamp", ev.getTimestamp());
        object.append("sourceContextId", ev.getSourceContextId());
        object.append("sourceRouteId", ev.getSourceRouteId());
        object.append("inBody", ev.getEvent().getExchange().getIn().getBody(String.class));
        object.append("outBody",
                ev.getEvent().getExchange().hasOut() ? ev.getEvent().getExchange().getOut().getBody(String.class)
                        : null);

        db.getCollection("baievents").insert(object);
    }

    /*
     * Assumes that events are processed in order, i.e. the first ExchangeCreatedEvent received is the one from the route's consumer, ExchangeSending events are not received before
     * the ExchangeCreated, etc.
     */

    private void digestExchangeEvent(AuditEvent ev) {
        AbstractExchangeEvent event = ev.getEvent();
        if (event instanceof ExchangeCreatedEvent) {
            digestExchangeCreatedEvent(ev);
        }

        // if the Exchange that has just completed is the same that started the route (i.e. the one from the ExchangeCreated event we accepted)
        if (event instanceof ExchangeCompletedEvent) {
            digestExchangeCompletedEvent(ev);
        }

        if (event instanceof ExchangeSendingEvent) {
            digestExchangeSendingEvent(ev);
        }

        if (event instanceof ExchangeSentEvent) {
            digestExchangeSentEvent(ev);
        }

    }

    private void digestExchangeCreatedEvent(AuditEvent ev) {
        // filter: { _id : <breadcrumbId> }
        // updateObj: { $push : { exchanges: { in: <inmessage>, inTimestamp: <inTimestamp> } } }
        // an exchange has been created (by a consumer or by an EIP - we probably don't want to track the latter, so we need to find a way)
        // perhaps: if an exchange already exists with the same unit of work, of if an exchange with created='yes' already exists for that route, discard this message

        // ExchangeCreated will create a record in the per-route collection, *ONLY IF* a record doesn't already exist
        // if a record exists, it means that an EIP or processor has created a new Exchange, which will be sent later on, so we'll intercept it at that event
        Object inMessage = null;
        try {
            inMessage = convertPayload(ev.getExchange().getIn().getBody(), ev.getExchange());
        } catch (Exception e) {
            // nothing
        }

        DBObject exchObj = BasicDBObjectBuilder.start().append("endpointUri", ev.getEndpointURI())
                .append("startTimestamp", ev.getTimestamp()).append("status", "in_progress")
                .append("exchangeId", ev.getEvent().getExchange().getExchangeId())
                .append("exchangePattern", ev.getEvent().getExchange().getPattern().toString())
                .append("in", inMessage).append("dispatchId",
                        ev.getEvent().getExchange().getProperty(AuditConstants.DISPATCH_ID, String.class))
                .get();

        DBObject toInsert = BasicDBObjectBuilder.start().append("_id", ev.getBreadCrumbId())
                .append("input", Arrays.asList(exchObj)).get();

        addCurrentRouteIdIfNeeded(ev, exchObj);
        // insert the record => if it already exists, Mongo will ignore the insert
        collectionFor(ev).insert(toInsert);
    }

    private void digestExchangeCompletedEvent(AuditEvent ev) {
        DBObject filter = BasicDBObjectBuilder.start().append("_id", ev.getBreadCrumbId())
                .append("input.endpointUri", ev.getEndpointURI())
                .append("input.exchangeId", ev.getExchange().getExchangeId()).append("exchanges.dispatchId",
                        ev.getEvent().getExchange().getProperty(AuditConstants.DISPATCH_ID, String.class))
                .get();

        DBObject toApply = new BasicDBObject();
        toApply.put("$set", new BasicDBObject());
        DBObject toSet = (BasicDBObject) toApply.get("$set");
        toSet.put("input.$.endTimestamp", ev.getTimestamp());
        toSet.put("input.$.status", "finished");

        // TODO: what if the exchange pattern changed while routing?
        // if the exchange pattern is InOut, first check if there's an out message, if not, dump the in message as the out (since this is what Camel's PipelineProcessor
        // will do internally anyway)
        if (ev.getEvent().getExchange().getPattern() == ExchangePattern.InOut) {
            Object outBody = null;
            try {
                outBody = ev.getExchange().hasOut() ? ev.getExchange().getOut().getBody()
                        : ev.getExchange().getIn().getBody();
                // it's okay to insert a null value if the he pattern was InOut
                toSet.put("input.$.out", convertPayload(outBody, ev.getExchange()));
                toSet.put("input.$.originalOut", typeConverter.convertTo(String.class, outBody));
            } catch (Exception e) {
                // nothing
            }
        }
        // update the record, only if the filter criteria is met
        collectionFor(ev).update(filter, toApply);
    }

    private void digestExchangeSendingEvent(AuditEvent ev) {
        DBObject filter = new BasicDBObject();
        filter.put("_id", ev.getBreadCrumbId());
        DBObject toApply = new BasicDBObject();
        toApply.put("$push", new BasicDBObject("exchanges", new BasicDBObject()));
        DBObject exchangeToPush = (BasicDBObject) ((BasicDBObject) toApply.get("$push")).get("exchanges");
        exchangeToPush.put("endpointUri", ev.getEndpointURI());
        exchangeToPush.put("startTimestamp", ev.getTimestamp());
        exchangeToPush.put("status", "in_progress");
        exchangeToPush.put("exchangeId", ev.getEvent().getExchange().getExchangeId());
        exchangeToPush.put("exchangePattern", ev.getEvent().getExchange().getPattern().toString());
        exchangeToPush.put("dispatchId",
                ev.getEvent().getExchange().getProperty(AuditConstants.DISPATCH_ID, String.class));
        addCurrentRouteIdIfNeeded(ev, exchangeToPush);
        try {
            exchangeToPush.put("in", convertPayload(ev.getExchange().getIn().getBody(), ev.getExchange()));
        } catch (Exception e) {
            // nothing
        }

        // update the record
        collectionFor(ev).update(filter, toApply);
    }

    private void digestExchangeSentEvent(AuditEvent ev) {
        DBObject filter = BasicDBObjectBuilder.start().append("_id", ev.getBreadCrumbId())
                .append("exchanges.endpointUri", ev.getEndpointURI())
                .append("exchanges.exchangeId", ev.getExchange().getExchangeId()).append("exchanges.dispatchId",
                        ev.getEvent().getExchange().getProperty(AuditConstants.DISPATCH_ID, String.class))
                .get();

        DBObject toApply = BasicDBObjectBuilder.start().push("$set")
                .append("exchanges.$.endTimestamp", ev.getTimestamp()).append("exchanges.$.status", "finished")
                .get();

        // if the exchange pattern is InOut, first check if there's an out message, if not, dump the in message as the out (since this is what Camel's PipelineProcessor
        // will do internally anyway)
        if (ev.getEvent().getExchange().getPattern() == ExchangePattern.InOut) {
            Object outBody = null;
            try {
                outBody = ev.getExchange().hasOut() ? ev.getExchange().getOut().getBody()
                        : ev.getExchange().getIn().getBody();
                // it's okay to insert a null value if the he pattern was InOut
                ((BasicDBObject) toApply.get("$set")).put("exchanges.$.out",
                        convertPayload(outBody, ev.getExchange()));
            } catch (Exception e) {
                // nothing
            }
        }
        // update the record, only if the filter criteria is met
        collectionFor(ev).update(filter, toApply);
    }

    private void digestEndpointFailureEvent(AuditEvent ev) {
        DBObject filter = new BasicDBObject("_id", ev.getBreadCrumbId());
        // 1. push the failure into endpointFailures
        DBObject toUpdate = BasicDBObjectBuilder.start().push("$push").push("endpointFailures")
                .append("endpointUri", ev.getEndpointURI()).append("exception", ev.getException().toString())
                .append("timestamp", ev.getTimestamp()).get();

        collectionFor(ev).update(filter, toUpdate);
        addCurrentRouteIdIfNeeded(ev, (DBObject) ((DBObject) toUpdate.get("$push")).get("endpointFailures"));

        // 2. Then set the status of the exchange to failed - if it was an exchange sent from this route
        filter.put("exchanges.endpointUri", ev.getEndpointURI());
        filter.put("exchanges.exchangeId", ev.getEvent().getExchange().getExchangeId());
        filter.put("exchanges.dispatchId",
                ev.getEvent().getExchange().getProperty(AuditConstants.DISPATCH_ID, String.class));
        toUpdate = BasicDBObjectBuilder.start().push("$set").append("exchanges.$.status", "failed")
                .append("exchanges.$.failTimestamp", ev.getTimestamp()).get();

        // if an exception is informed, we add it too
        if (ev.getException() != null) {
            ((BasicDBObject) toUpdate.get("$set")).put("exchanges.$.exception", ev.getException().toString());
        }

        collectionFor(ev).update(filter, toUpdate);

        // 3. Then set the status of the exchange to failed - if it was the incoming exchange into the route
        filter.put("in.endpointUri", ev.getEndpointURI());
        filter.put("in.exchangeId", ev.getEvent().getExchange().getExchangeId());
        filter.put("in.dispatchId",
                ev.getEvent().getExchange().getProperty(AuditConstants.DISPATCH_ID, String.class));
        toUpdate = BasicDBObjectBuilder.start().push("$set").append("in.$.status", "failed")
                .append("in.$.failTimestamp", ev.getTimestamp()).get();

        // if an exception is informed, we add it too
        if (ev.getException() != null) {
            ((BasicDBObject) toUpdate.get("$set")).put("in.$.exception", ev.getException().toString());
        }

        collectionFor(ev).update(filter, toUpdate);

    }

    private void digestEndpointRedeliveryEvent(AuditEvent ev) {
        DBObject filter = new BasicDBObject("_id", ev.getBreadCrumbId());
        // we don't know what processor caused it, because this info is not on the event, so just push an element into the processorRedeliveries array for the time being
        DBObject toPush = BasicDBObjectBuilder.start().push("$push").push("endpointRedeliveries")
                .append("exchangeId", ev.getExchange().getExchangeId()).append("endpointURI", ev.getEndpointURI())
                .append("timestamp", ev.getTimestamp()).append("exception", ev.getException().toString())
                .append("attempt", ev.getExchange().getProperty(Exchange.REDELIVERY_COUNTER)).get();
        collectionFor(ev).update(filter, toPush);
    }

    private void digestProcessorRedeliveryEvent(AuditEvent ev) {
        DBObject filter = new BasicDBObject("_id", ev.getBreadCrumbId());
        // we don't know what processor caused it, because this info is not on the event, so just push an element into the processorRedeliveries array for the time being
        DBObject toPush = BasicDBObjectBuilder.start().push("$push").push("processorRedeliveries")
                .append("exchangeId", ev.getExchange().getExchangeId()).append("timestamp", ev.getTimestamp())
                .append("exception", ev.getException().toString())
                .append("attempt", ev.getExchange().getProperty(Exchange.REDELIVERY_COUNTER)).get();

        collectionFor(ev).update(filter, toPush);

    }

    private void addCurrentRouteIdIfNeeded(AuditEvent ev, DBObject dbo) {
        if (ev.getCurrentRouteId() != null && !ev.getCurrentRouteId().equals(ev.getSourceRouteId())) {
            dbo.put("currentRouteId", ev.getCurrentRouteId());
        }
    }

    private void addToMetaCollection(AuditEvent ev) {
        DBObject filter = new BasicDBObject("_id", ev.getBreadCrumbId());
        DBObject dbo = new BasicDBObject("$addToSet",
                new BasicDBObject("routes", ev.getSourceContextId() + "." + ev.getSourceRouteId()));
        // possible collection names: eagleView, hawkView
        DBCollection collection = db.getCollection("exchangeXray");
        collection.update(filter, dbo, true, false);
    }

    private DBCollection collectionFor(AuditEvent ev) {
        return db.getCollection(ev.getSourceContextId() + "." + ev.getSourceRouteId());
    }

    /**
     * Converts the payload into something that we know for sure can go into Mongo (DBObject), primitive value, etc.
     * @param payload
     * @param exchange
     * @return
     * @throws Exception
     */
    private Object convertPayload(Object payload, Exchange exchange) throws Exception {
        Expression expression = getStoreBodyExpression();
        if (expression != null) {
            payload = expression.evaluate(exchange, Object.class);
        }
        if (payload == null) {
            return null;
        }
        // TODO should we try convert to a DBObject first?

        // have a String representation handy
        String s = typeConverter.convertTo(String.class, payload);
        // 1. JSON if it starts with {
        if (s.startsWith("{")) {
            Object answer = null;
            try {
                answer = JSON.parse(s);
            } catch (JSONParseException ex) {
                LOG.warn("Attempt to convert " + payload + " to JSON failed: " + ex, ex);
            }

            if (answer != null) {
                return answer;
            }
        }

        // 2. XML if it starts with <
        if (s.startsWith("<")) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                xmlJson.marshal(exchange, payload, baos);
                if (baos != null && baos.size() > 0) {
                    // first convert to String, then to DBObject
                    String json = typeConverter.convertTo(String.class, baos);
                    return JSON.parse(json);
                }
            } catch (Exception e) {
                System.out.println(e);
            }
        }

        // 3. String, if it was originally a String
        if (payload instanceof String) {
            return payload;
        }
        // 4. Object
        // MongoDbBasicConverters has a converter that uses Jackson to convert any object to JSON, by first turning it into a Map
        return typeConverter.convertTo(DBObject.class, payload);
    }

    public void init() throws Exception {
        db = mongo.getDB(dbname);
        typeConverter = context.getTypeConverter();
        // load the type hints
        typeHints = new Properties();
        try {
            typeHints.load(this.getClass().getClassLoader().getResourceAsStream("typeHints.properties"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        xmlJson.setForceTopLevelObject(true);
        ServiceHelper.startService(xmlJson);
    }

    public Mongo getMongo() {
        return mongo;
    }

    public void setMongo(Mongo mongo) {
        this.mongo = mongo;
    }

    public String getDbname() {
        return dbname;
    }

    public void setDbname(String dbname) {
        this.dbname = dbname;
    }

    public CamelContext getContext() {
        return context;
    }

    public void setContext(CamelContext context) {
        this.context = context;
    }

}