com.ca.apm.mongo.Collector.java Source code

Java tutorial

Introduction

Here is the source code for com.ca.apm.mongo.Collector.java

Source

/*
 *
 * Copyright (c) 2014 CA. All rights reserved.
 *
 * 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.
    
 * IN NO EVENT WILL CA BE LIABLE TO THE END USER OR ANY THIRD PARTY FOR ANY LOSS
 * OR DAMAGE, DIRECT OR INDIRECT, FROM THE USE OF THIS MATERIAL,
 * INCLUDING WITHOUT LIMITATION, LOST PROFITS, BUSINESS INTERRUPTION, GOODWILL,
 * OR LOST DATA, EVEN IF CA IS EXPRESSLY ADVISED OF SUCH LOSS OR DAMAGE.
 *
 */

package com.ca.apm.mongo;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.Properties;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import javax.net.ssl.SSLSocketFactory;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.CommandResult;
import com.mongodb.DB;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;

import com.ca.apm.mongo.Topology.ClusterType;

/**
 *
 *
 * @author
 * @version $Revision: 1.1.1.1 $
 */
public class Collector implements Runnable {
    public static final String DB_HOST_PROP = "mongo.hostname";
    public static final String DB_PORT_PROP = "mongo.port";
    public static final String DB_USER_PROP = "mongo.user";
    public static final String DB_PASSWD_PROP = "mongo.pw";
    public static final String DB_AUTH_PROP = "mongo.auth";

    public static final String COLLECTION_INTERVAL_PROP = "mongo.interval.seconds";
    public static final String USE_SSL_PROP = "mongo.usessl";
    public static final String USE_KERB_PROP = "mongo.usekerberos";
    public static final String SSL_CLIENT_TRUST_STORE_FILE_PROP = "javax.net.ssl.trustStore";
    public static final String SSL_CLIENT_TRUST_STORE_PASSWD_PROP = "javax.net.ssl.trustStorePassword";
    public static final String APM_HOST_PROP = "apm.apihost";
    public static final String APM_PORT_PROP = "apm.apiport";

    public static final String AUTH_NONE = "none";
    public static final String AUTH_CR = "basic";
    public static final String AUTH_X509 = "x.509";
    public static final String AUTH_KERBEROS = "kerberos";
    public static final String AUTH_SASL = "plainsasl";

    private static Logger logger;

    public static void main(final String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: Collector your-propfile");
            System.exit(1);
        }

        try {
            setupLogger(new FileInputStream(args[0]));
            Properties p = new Properties();
            p.load(new FileReader(args[0]));
            logger.log(Level.INFO, "APM MongoDB Collector version: {0}",
                    Collector.class.getPackage().getImplementationVersion());
            collect(p);

            for (Handler h : logger.getHandlers()) {
                h.close();
            }
        } catch (Exception ex) {
            if (logger != null) {
                logger.log(Level.WARNING, "Exception: {0}", ex);
            } else {
                System.err.println("Exception: " + ex);
            }
            System.exit(1);
        }
    }

    public static void setupLogger(FileInputStream propFile) {
        LogManager manager = LogManager.getLogManager();

        try {
            if (propFile != null) {
                manager.readConfiguration(propFile);
            }
        } catch (IOException e) {
            System.err.println("Error in setting up logger: " + e);
        }

        logger = Logger.getLogger(Collector.class.getName());
        logger.setUseParentHandlers(false);
        logger.addHandler(new ConsoleHandler());

        try {
            logger.addHandler(new FileHandler());
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Error adding FileHandler");
        }
    }

    public static void collect(final Properties p) {
        try {
            Collector c = new Collector(p);
            if (c.collectionInterval > 0) {
                ScheduledExecutorService ses = new ScheduledThreadPoolExecutor(1);
                ses.scheduleAtFixedRate(c, 0, c.collectionInterval, TimeUnit.SECONDS);
                // run forever
                synchronized (ses) {
                    try {
                        ses.wait();
                    } catch (InterruptedException ie) {
                    }
                }
            } else {
                c.run();
            }
        } catch (Exception ex) {
            logger.log(Level.SEVERE, "Exception: ", ex);
        }
    }

    private Properties props;
    private int collectionInterval;
    private boolean keepRunning;
    private URL apiUrl;
    private List<MongoCredential> mongoCreds = new ArrayList<MongoCredential>();
    private Topology topology;

    public Collector(final Properties inProps) {
        props = inProps;
        processProperties();
        try {
            topology = discoverTopology();
        } catch (Exception e) {
            logger.log(Level.WARNING, "Exception discovering topology: ", e);
            try {
                // just assume standalone
                final String host = getStringProp(DB_HOST_PROP);
                final int port = getIntProp(DB_PORT_PROP);
                topology = new StandaloneMongod(props, host, port, logger);
                topology.discoverServers("Standalone");
            } catch (Exception e2) {
                // for standalone server discovery, there's not really
                // anything to fail...
            }
        }
        keepRunning = true;
    }

    public void run() {
        logger.log(Level.INFO, "harvesting metrics...");
        for (String mongoSrv : topology.getDiscoveredServers()) {
            MongoServer ms = null;
            try {
                ms = new MongoServer(mongoSrv);
                CommandResult mcr = getMongoData(ms.getHost(), ms.getPort());
                if (isValidData(mcr)) {
                    MetricFeedBundle mfb = makeMetrics(mcr);
                    deliverMetrics(mfb);
                }
            } catch (Exception e) {
                logger.log(Level.SEVERE, "Exception: ", e);
            }
        }
    }

    public CommandResult getMongoData(final String host, final int port) throws Exception {
        return dbAdminCmd(host, port, "serverStatus");
    }

    private CommandResult dbAdminCmd(final String host, final int port, final String cmd) throws Exception {
        return runDBCmd(host, port, "admin", cmd);
    }

    private CommandResult runDBCmd(final String host, final int port, final String database, final String cmd)
            throws Exception {
        MongoClient dbClient = null;
        try {
            dbClient = setupDbClient(host, port);
            DB db = dbClient.getDB(database);
            return db.command(cmd);
        } finally {
            if (dbClient != null) {
                dbClient.close();
            }
        }
    }

    public MetricFeedBundle makeMetrics(final CommandResult mcr) throws Exception {
        MetricFeedBundle mfb = new MetricFeedBundle();
        ServerAddress sa = mcr.getServerUsed();
        // Add a "mongo segment" to the metric path to insure that
        // mongo metrics are grouped/segregated in the metric browser.
        // Note that we can't use ":" in that segment though
        final String basePath = String.format("MongoDB@%s;%d", sa.getHost(), sa.getPort());
        makeMetrics(mfb, basePath, mcr);
        return mfb;
    }

    private boolean isValidData(BasicDBObject bdo) {
        Object ok = bdo.get("ok");
        Object error = bdo.get("errmsg");
        if (ok != null && ((Number) ok).intValue() == 1 && error == null) {
            bdo.removeField("ok");
            return true;
        }
        if (error != null) {
            logger.log(Level.WARNING, "Error from mongo command: {0}", error);
        } else {
            logger.log(Level.WARNING, "Invalid/unexpected mongo command output: {0}", bdo);
        }
        return false;
    }

    private void makeMetrics(final MetricFeedBundle mfb, final String basePath, final BasicDBObject bdo)
            throws Exception {
        for (String s : bdo.keySet()) {
            final MetricPath metricPath = new MetricPath(basePath);
            final Object o = bdo.get(s);
            if (o instanceof BasicDBObject) {
                metricPath.addElement(s);
                makeMetrics(mfb, metricPath.toString(), (BasicDBObject) o);
            } else if (o instanceof BasicDBList) {
                metricPath.addElement(s);
                processBasicDBList(mfb, metricPath.toString(), (BasicDBList) o);
            } else if (isKnownDataType(o)) {
                metricPath.addMetric(s);
                makeMetric(metricPath.toString(), o, mfb);
            } else {
                logger.log(Level.WARNING, "Unknown type in mongo output for key {0}: {1}",
                        new Object[] { s, o.getClass().getName() });
            }
        }
    }

    private void processBasicDBList(final MetricFeedBundle mfb, final String basePath, final BasicDBList bdl)
            throws Exception {
        int i = 0;
        for (Object o : bdl) {
            MetricPath metricPath = new MetricPath(basePath);
            if (o instanceof BasicDBObject) {
                metricPath.addElement(String.format("%d", i++));
                makeMetrics(mfb, metricPath.toString(), (BasicDBObject) o);
            } else if (o instanceof BasicDBList) {
                metricPath.addElement(String.format("%d", i++));
                processBasicDBList(mfb, metricPath.toString(), (BasicDBList) o);
            } else if (isKnownDataType(o)) {
                metricPath.addMetric(String.format("%d", i++));
                makeMetric(metricPath.toString(), o, mfb);
            } else {
                logger.log(Level.WARNING, "Unknown type in mongo output for DBList {0}: {1}",
                        new Object[] { bdl, o.getClass().getName() });
            }
        }
    }

    private void makeMetric(final String metricPath, final Object dataObj, final MetricFeedBundle mfb) {
        if (dataObj instanceof String) {
            mfb.addMetric("StringEvent", metricPath, (String) dataObj);
        } else if (dataObj instanceof Number) {
            String type;
            if (dataObj instanceof Double) {
                // API doesn't support floating-point metric values
                // so we round the value to a long, and also create a string
                // metric to display the value (just for debugging etc.)
                type = "LongCounter";
                long val = Math.round((Double) dataObj);
                mfb.addMetric("LongCounter", metricPath + " (rounded)", String.valueOf(val));
                mfb.addMetric("StringEvent", metricPath + " (string)", dataObj.toString());
            } else {
                if (dataObj instanceof Long) {
                    type = "LongCounter";
                } else {
                    // treat as Integer
                    type = "IntCounter";
                }
                mfb.addMetric(type, metricPath, dataObj.toString());
            }
        } else if (dataObj instanceof Date) {
            mfb.addMetric("TimeStamp", metricPath, String.valueOf(((Date) dataObj).getTime()));
        } else if (dataObj instanceof Boolean) {
            mfb.addMetric("StringEvent", metricPath, dataObj.toString());
        }
    }

    private boolean isKnownDataType(final Object dataObj) {
        return (dataObj instanceof String || dataObj instanceof Number || dataObj instanceof Date
                || dataObj instanceof Boolean);
    }

    public void deliverMetrics(final MetricFeedBundle mfb) throws Exception {
        final String json = mfb.toString();
        final HttpURLConnection conn = (HttpURLConnection) apiUrl.openConnection();
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Type", "application/json");
        conn.getOutputStream().write(json.getBytes());
        final int rc = conn.getResponseCode();
        if (rc != 200) {
            logger.log(Level.SEVERE, "Error code: {0}, payload: {1}",
                    new Object[] { rc, getPayload(conn.getErrorStream()) });
        } else {
            logger.log(Level.INFO, "Successful metric delivery");
        }
    }

    private String getPayload(final InputStream is) throws Exception {
        BufferedReader rdr = new BufferedReader(new InputStreamReader(is));
        String line;
        StringBuilder sb = new StringBuilder();
        while ((line = rdr.readLine()) != null) {
            sb.append(String.format("%s%n", line));
        }
        rdr.close();
        return sb.toString();
    }

    private void processProperties() {
        // Don't validate mongo connection since the DB may not
        // be running when we start.  But, host and port properties must
        // be set at least
        getStringProp(DB_HOST_PROP);
        getIntProp(DB_PORT_PROP);
        setupCreds(mongoCreds, props);
        setInterval();
        setApiUrl();
    }

    public static void setupCreds(final List<MongoCredential> mc, final Properties iprops) {
        final String type = getStringProp(DB_AUTH_PROP, iprops);
        String user;
        String pw;
        if (AUTH_NONE.equalsIgnoreCase(type)) {
            // nothing to do
        } else if (AUTH_CR.equalsIgnoreCase(type)) {
            user = getStringProp(DB_USER_PROP, iprops);
            pw = getStringProp(DB_PASSWD_PROP, iprops);
            mc.add(MongoCredential.createMongoCRCredential(user, "admin", pw.toCharArray()));
        } else if (AUTH_X509.equalsIgnoreCase(type)) {
            user = getStringProp(DB_USER_PROP, iprops);
            System.out.printf("X509 cred user(%s)%n", user);
            mc.add(MongoCredential.createMongoX509Credential(user));
        } else if (AUTH_KERBEROS.equalsIgnoreCase(type)) {
            user = getStringProp(DB_USER_PROP, iprops);
            mc.add(MongoCredential.createGSSAPICredential(user));
        } else if (AUTH_SASL.equalsIgnoreCase(type)) {
            user = getStringProp(DB_USER_PROP, iprops);
            pw = getStringProp(DB_PASSWD_PROP, iprops);
            mc.add(MongoCredential.createPlainCredential(user, "$external", pw.toCharArray()));
        } else {
            throw new IllegalArgumentException(String.format("Invalid %s property", DB_AUTH_PROP));
        }
    }

    private MongoClient setupDbClient(final String dbHost, final int dbPort) {
        final boolean useSSL = getBooleanProp(USE_SSL_PROP);
        final String clientTrustStore = getOptionalStringProp(SSL_CLIENT_TRUST_STORE_FILE_PROP);
        final String clientPasswd = getOptionalStringProp(SSL_CLIENT_TRUST_STORE_PASSWD_PROP);

        try {
            MongoClientOptions.Builder builder = new MongoClientOptions.Builder();

            if (useSSL) {
                System.setProperty(SSL_CLIENT_TRUST_STORE_FILE_PROP, clientTrustStore);
                System.setProperty(SSL_CLIENT_TRUST_STORE_PASSWD_PROP, clientPasswd);
                builder = builder.socketFactory(SSLSocketFactory.getDefault());
            }

            final MongoClientOptions options = builder.build();
            return new MongoClient(new ServerAddress(dbHost, dbPort), mongoCreds, options);
        } catch (Exception ex) {
            throw new RuntimeException("Can't initialize mongo client", ex);
        }
    }

    private void setInterval() {
        collectionInterval = getIntProp(COLLECTION_INTERVAL_PROP);
    }

    private void setApiUrl() {
        final String apiHost = getStringProp(APM_HOST_PROP);
        final int apiPort = getIntProp(APM_PORT_PROP);
        try {
            apiUrl = new URL(String.format("http://%s:%d/apm/metricFeed", apiHost, apiPort));
        } catch (Exception ex) {
            throw new RuntimeException("Can't initialize APM API URL", ex);
        }
    }

    private String getStringProp(final String pname) {
        return getStringProp(pname, props);
    }

    public static String getStringProp(final String pname, final Properties p) {
        final String ret = p.getProperty(pname);
        if (isEmpty(ret)) {
            throw new IllegalArgumentException(String.format("missing or invalid property: %s%n", pname));
        }
        return ret;
    }

    private String getOptionalStringProp(final String pname) {
        return getOptionalStringProp(pname, props);
    }

    public static String getOptionalStringProp(final String pname, final Properties p) {
        final String ret = p.getProperty(pname);
        if (isEmpty(ret)) {
            return "";
        }
        return ret;
    }

    private int getIntProp(final String pname) {
        int ret = 0;
        try {
            ret = Integer.parseInt(getStringProp(pname));
        } catch (NumberFormatException nfe) {
            throw new IllegalArgumentException(String.format("missing or invalid integer property: %s%n", pname));
        }
        return ret;
    }

    private boolean getBooleanProp(final String pname) {
        return getBooleanProp(pname, props);
    }

    public static boolean getBooleanProp(final String pname, final Properties p) {
        return Boolean.valueOf(p.getProperty(pname));
    }

    private static boolean isEmpty(final String s) {
        return (s == null || "".equals(s.trim()));
    }

    public Topology discoverTopology() throws Exception {

        final String host = getStringProp(DB_HOST_PROP);
        final int port = getIntProp(DB_PORT_PROP);

        logger.log(Level.INFO, "Discovering Topology for host: {0}:{1}", new Object[] { host, port });

        final CommandResult master = dbAdminCmd(host, port, "isMaster");

        // ismaster returns true for a standalone mongod instance, a mongos
        // instance, a mongod shard node, or a primary in a replica set
        if (master.getBoolean("ismaster") || master.containsField("primary")) {

            boolean isReplicaSet = false;
            if (master.containsField("primary")) {
                isReplicaSet = true;
            }

            if (isInShardCluster(master)) {
                topology = new ShardCluster(props, host, port, logger);
                topology.discoverServers(getClusterNodeType());
            } else if (isReplicaSet(master)) {
                topology = new ReplicaSet(props, host, port, logger);
                topology.discoverServers("doesn't matter");
            } else {
                topology = new StandaloneMongod(props, host, port, logger);
                topology.discoverServers("doesn't matter");
            }
        }
        logger.log(Level.INFO, "Topology: {0}", topology);
        return topology;
    }

    private boolean isInShardCluster(final CommandResult cr) throws Exception {

        MongoServer ms = new MongoServer(getMyself(cr));
        final String host = ms.getHost();
        final int port = ms.getPort();

        boolean sharded = false;

        final String msg = cr.getString("msg");
        if ((msg != null) && msg.contains("isdbgrid")) {
            sharded = true;
        } else if (cr.getBoolean("ismaster")) {
            final CommandResult shardState = dbAdminCmd(host, port, "shardingState");
            // shardingState command only returns OK when server is in a sharded
            // cluster
            if (shardState.ok()) {
                if (shardState.getBoolean("enabled")) {
                    sharded = true;
                }
            }
        } else if (cr.containsField("primary")) {
            // we are in a replica set but not the primary,
            // check the primary to see if it is a shard member
            final String primary = cr.getString("primary");
            ms = new MongoServer(primary);
            final CommandResult priIsMaster = dbAdminCmd(ms.getHost(), ms.getPort(), "isMaster");
            sharded = isInShardCluster(priIsMaster);
        }
        return sharded;
    }

    private String getClusterNodeType() throws Exception {

        final String host = getStringProp(DB_HOST_PROP);
        final int port = getIntProp(DB_PORT_PROP);

        String nodeType = null;

        final CommandResult isMaster = dbAdminCmd(host, port, "isMaster");
        if (isMaster.getBoolean("ismaster")) {
            final String msg = isMaster.getString("msg");
            if (msg != null && msg.contains("isdbgrid")) {
                nodeType = "shardRouter";
            } else {
                final CommandResult shardState = dbAdminCmd(host, port, "shardingState");
                if (shardState.ok() && shardState.getBoolean("enabled")) {
                    if (isConfigServer(host, port)) {
                        nodeType = "shardConfigServer";
                    } else {
                        nodeType = "shardMember";
                    }
                }
            }
        } else if (isReplicaMember(isMaster)) {
            nodeType = "shardMember";
        }

        return nodeType;
    }

    final boolean isConfigServer(final String host, final int port) {

        boolean isConfigServer = false;

        MongoClient dbClient = null;

        try {
            dbClient = setupDbClient(host, port);
            final DB configDB = dbClient.getDB("config");
            if (configDB.getCollectionFromString("mongos").find().hasNext()) {
                isConfigServer = true;
            }
        } finally {
            if (dbClient != null) {
                dbClient.close();
            }
        }
        return isConfigServer;
    }

    private boolean isReplicaSet(final CommandResult cr) {
        return cr.containsField("primary");
    }

    /**
     * This method is to check to see if a node is in a replica set despite
     * not being the primary member.
     */
    private boolean isReplicaMember(final CommandResult cr) {
        boolean isReplMember = false;
        if (cr.containsField("primary")) {
            if (cr.getBoolean("secondary") || cr.getBoolean("passive") || cr.getBoolean("arbiterOnly")) {
                isReplMember = true;
            }
        }
        return isReplMember;
    }

    private String getMyself(final CommandResult cr) {
        return String.format("%s:%d", cr.getServerUsed().getHost(), cr.getServerUsed().getPort());
    }
}