/* * * 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()); } }