/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.destination.mongodb;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.management.MXBean;
import javax.management.Notification;
import org.helios.apmrouter.catalog.domain.Agent;
import org.helios.apmrouter.catalog.domain.Host;
import org.helios.apmrouter.catalog.domain.Metric;
//import org.helios.apmrouter.catalog.jdbc.h2.AbstractTrigger;
//import org.helios.apmrouter.catalog.jdbc.h2.NewElementTriggers;
import org.helios.apmrouter.collections.ConcurrentLongSlidingWindow;
import org.helios.apmrouter.collections.ILongSlidingWindow;
import org.helios.apmrouter.destination.BaseDestination;
import org.helios.apmrouter.destination.accumulator.FlushQueueReceiver;
import org.helios.apmrouter.destination.accumulator.TimeSizeFlushQueue;
import org.helios.apmrouter.metric.IMetric;
import org.helios.apmrouter.tsmodel.Tier;
import org.helios.apmrouter.tsmodel.TimeSeriesModel;
import org.helios.apmrouter.util.SystemClock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.data.mongodb.core.CollectionOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.support.MetricType;
import com.mongodb.Mongo;
/**
* <p>Title: MongoDbDestination</p>
* <p>Description: Destination for a MongoDb database. Also handles secondary catalog updates.</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.destination.mongodb.MongoDbDestination</code></p>
*/
@MXBean(true)
public class MongoDbDestination extends BaseDestination implements Runnable, FlushQueueReceiver<IMetric>, MongoDbDestinationMXBean {
/** The mongo DB template */
protected MongoTemplate mongoTemplate = null;
/** The raw mongo connection */
protected Mongo mongo = null;
/** The catalog queue processor thread */
protected Thread catalogProcessorThread = null;
/** The maximum batch size for catalog queue reads */
protected int catalogMaxBatchSize = 30;
/** Thread run indicator */
protected final AtomicBoolean keepRunning = new AtomicBoolean(false);
/** The last catalog elapsed write time in ms */
protected final ILongSlidingWindow lastCatalogElapsedNs = new ConcurrentLongSlidingWindow(60);
/** The last metric-data elapsed write time in ms */
protected final ILongSlidingWindow lastMetricElapsedNs = new ConcurrentLongSlidingWindow(60);
/** The last metric-data batch size */
protected final ILongSlidingWindow lastBatchSize = new ConcurrentLongSlidingWindow(60);
/** The time based flush trigger in ms. */
protected long timeTrigger = 15000;
/** The size based flush trigger in number of metrics accumulated */
protected int sizeTrigger = 30;
/** The time/size triggered flush queue */
protected TimeSizeFlushQueue<IMetric> flushQueue = null;
/** The time-series model for the MongoDb time-series collections */
protected TimeSeriesModel tsModel = null;
/** The time-series model expression */
protected String tsDefinition = null;
/** The maximum size of a collection as a factor of the tier specification */
protected long maxCollectionSizePerPeriod = 650000;
/** The live step size */
protected long step = 15000;
// BasicDBObject doc = new BasicDBObject();
// doc.put("$set", new BasicDBObject("word", word));
// doc.put("$inc", new BasicDBObject("c", 1));
/**
* {@inheritDoc}
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
final List<Host> hosts = new ArrayList<Host>();
final List<Agent> agents = new ArrayList<Agent>();
final List<Metric> metrics = new ArrayList<Metric>();
long loopTime = SystemClock.time() + 5000;
while(true) {
try {
// Notification notif = notificationQueue.poll(1000, TimeUnit.MILLISECONDS);
// try {
// if(notif!=null) {
// String type = notif.getType();
// if(AbstractTrigger.NEW_HOST.equals(type)) {
// hosts.add(new Host(notif));
// incr("HostsQueued");
// } else if(AbstractTrigger.NEW_AGENT.equals(type)) {
// agents.add(new Agent(notif));
// incr("AgentsQueued");
// } else if(AbstractTrigger.NEW_METRIC.equals(type)) {
// metrics.add(new Metric(notif));
// incr("MetricsQueued");
// }
// }
// } catch (Exception ex) {
// error("Failed to create domain object for MongoDb insert.\n\tNotification:", notif, ex);
//
// }
boolean inserts = false;
int batchSize = 0;
try {
if(!hosts.isEmpty() || !agents.isEmpty() || !metrics.isEmpty()) {
inserts = true;
SystemClock.startTimer();
} else {
continue;
}
try {
if(!hosts.isEmpty()) {
mongoTemplate.insert(hosts, Host.class);
incr("HostsInserted", hosts.size());
batchSize += hosts.size();
}
} catch (Exception ex) {
error("Failed to save host to MongoDb\n\tHosts:", hosts, ex);
}
try {
if(!agents.isEmpty()) {
mongoTemplate.insert(agents, Agent.class);
incr("AgentsInserted", agents.size());
batchSize += agents.size();
}
} catch (Exception ex) {
error("Failed to save agent to MongoDb\n\tAgents:", agents, ex);
}
try {
if(!metrics.isEmpty()) {
mongoTemplate.insert(metrics, Metric.class);
incr("MetricsInserted", metrics.size());
batchSize += metrics.size();
}
} catch (Exception ex) {
error("Failed to save metrics to MongoDb\n\tMetrics:", metrics, ex);
}
} finally {
if(inserts) {
lastCatalogElapsedNs.insert(SystemClock.endTimer().elapsedNs);
lastBatchSize.insert(batchSize);
}
loopTime = SystemClock.time() + 5000;
}
} catch (Exception iex) {
if(keepRunning.get()) {
Thread.interrupted();
} else {
break;
}
} finally {
hosts.clear();
agents.clear();
metrics.clear();
}
}
}
/**
* Accept Route additive for BaseDestination extensions
* @param routable The metric to route
*/
@Override
protected void doAcceptRoute(IMetric routable) {
try {
routable.getLongValue();
flushQueue.put(routable);
incr("MetricsForwarded");
} catch (Exception e) {
incr("InvalidMetricDrops");
//error("Invalid Metric Type [", routable, "]");
//e.printStackTrace(System.err);
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.destination.accumulator.FlushQueueReceiver#flushTo(java.util.Collection)
*/
@Override
public void flushTo(Collection<IMetric> flushedItems) {
//info("Flushed [", flushedItems.size(), "] Items");
for(IMetric metric: flushedItems) {
final long period = getPeriod(metric.getTime());
Query query = query(where("period").is(period));
Update update = new Update().inc("cnt", 1)
.set("min", metric.getLongValue())
.set("max", metric.getLongValue());
mongoTemplate.upsert(query, update, "live");
}
}
/**
* Returns the current period
* @param timestamp The timestamp to get the period for
* @return the period
*/
public long getPeriod(long timestamp) {
return (timestamp - (timestamp%step));
}
/**
* {@inheritDoc}
*/
@Override
public void resetMetrics() {
this.lastBatchSize.clear();
this.lastCatalogElapsedNs.clear();
this.lastMetricElapsedNs.clear();
super.resetMetrics();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.destination.BaseDestination#doStart()
*/
@Override
protected void doStart() throws Exception {
super.doStart();
tsModel = TimeSeriesModel.create(tsDefinition);
flushQueue = new TimeSizeFlushQueue<IMetric>(getClass().getSimpleName(), sizeTrigger, timeTrigger, this);
catalogProcessorThread = new Thread(this, "MongoCatalogProcessorThread");
catalogProcessorThread.setDaemon(true);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.destination.BaseDestination#doStop()
*/
@Override
protected void doStop() {
keepRunning.set(false);
catalogProcessorThread.interrupt();
}
/**
* On start, registers this instance as a notification listener on notifications from the sub service
* @param event The app context refresh event
*/
@Override
public void onApplicationContextRefresh(ContextRefreshedEvent event) {
catalogProcessorThread.start();
info("Started MongoDb Catalog Processor Thread");
for(Tier tier: getTimeSeriesTiers()) {
if(!mongoTemplate.collectionExists(tier.getName())) {
long maxCollectionSize = tier.getPeriodCount() * maxCollectionSizePerPeriod;
mongoTemplate.createCollection(tier.getName(), new CollectionOptions((int)maxCollectionSize, (int) tier.getPeriodCount(), true));
info("Created tier [", tier.getName(), "]\n\tSize:" , maxCollectionSize, "\n\tDocs:", tier.getPeriodCount());
}
}
step = TimeUnit.MILLISECONDS.convert(tsModel.getModelTiers().get(0).getPeriodDuration().seconds, TimeUnit.SECONDS);
}
/**
* {@inheritDoc}
*/
@Override
public Set<String> getSupportedMetricNames() {
Set<String> _metrics = new HashSet<String>(super.getSupportedMetricNames());
_metrics.add("MetricsForwarded");
_metrics.add("InvalidMetricDrops");
_metrics.add("AgentsQueued");
_metrics.add("AgentsInserted");
_metrics.add("HostsQueued");
_metrics.add("HostsInserted");
_metrics.add("MetricsQueued");
_metrics.add("MetricsInserted");
return _metrics;
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.COUNTER, description="the number of hosts queued for insert")
public long getHostsQueuedForInsert() {
return getMetricValue("HostsQueued");
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.COUNTER, description="the number of hosts inserted")
public long getHostsInserted() {
return getMetricValue("HostsInserted");
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.COUNTER, description="the number of agents queued for insert")
public long getAgentsQueuedForInsert() {
return getMetricValue("AgentsQueued");
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.COUNTER, description="the number of agents inserted")
@ManagedOperation
public long getAgentsInserted() {
return getMetricValue("AgentsInserted");
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.COUNTER, description="the number of metrics queued for insert")
public long getMetricsQueuedForInsert() {
return getMetricValue("MetricsQueued");
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.COUNTER, description="the number of metrics inserted")
public long getMetricsInserted() {
return getMetricValue("MetricsInserted");
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.GAUGE, description="the last elapsed write time in ms")
public long getLastElapsedWriteTimeMs() {
return TimeUnit.MILLISECONDS.convert(getLastElapsedWriteTimeNs(), TimeUnit.NANOSECONDS);
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.GAUGE, description="the rolling average of elapsed write times in ms")
public long getRollingElapsedWriteTimeMs() {
return TimeUnit.MILLISECONDS.convert(getRollingElapsedWriteTimeNs(), TimeUnit.NANOSECONDS);
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.GAUGE, description="the last elapsed write time in ns")
public long getLastElapsedWriteTimeNs() {
return lastCatalogElapsedNs.isEmpty() ? 0 : lastCatalogElapsedNs.get(0);
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbCatalog", metricType=MetricType.GAUGE, description="the rolling average of elapsed write times in ns")
public long getRollingElapsedWriteTimeNs() {
return lastCatalogElapsedNs.avg();
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbMetrics", metricType=MetricType.GAUGE, description="the last written batch size")
public long getLastBatchSize() {
return lastBatchSize.isEmpty() ? 0 : lastBatchSize.get(0);
}
/**
* {@inheritDoc}
*/
@Override
@ManagedMetric(category="MongoDbMetrics", metricType=MetricType.GAUGE, description="the rolling average of the written batch sizes")
public long getRollingBatchSizes() {
return lastBatchSize.avg();
}
/**
* Creates a new MongoDbDestination
* @param patterns The metric patterns accepted by this destination
*/
public MongoDbDestination(String... patterns) {
super(patterns);
}
/**
* Creates a new MongoDbDestination
* @param patterns The metric patterns accepted by this destination
*/
public MongoDbDestination(Collection<String> patterns) {
super(patterns);
}
/**
* {@inheritDoc}
*/
@Override
@ManagedAttribute(description="The elapsed time after which accumulated time-series writes are flushed")
public long getTimeTrigger() {
return timeTrigger;
}
/**
* Sets the time based flush trigger
* @param timeTrigger the frequency that the buffer is flushed in ms.
*/
@ManagedAttribute(description="The elapsed time after which accumulated time-series writes are flushed")
public void setTimeTrigger(long timeTrigger) {
this.timeTrigger = timeTrigger;
}
/**
* {@inheritDoc}
*/
@Override
@ManagedAttribute(description="The number of accumulated time-series writes that triggers a flush")
public int getSizeTrigger() {
return sizeTrigger;
}
/**
* Sets the size based flush trigger
* @param sizeTrigger the number of metrics to accumulate before they are flushed
*/
@ManagedAttribute(description="The number of accumulated time-series writes that triggers a flush")
public void setSizeTrigger(int sizeTrigger) {
this.sizeTrigger = sizeTrigger;
}
/**
* Creates a new MongoDbDestination
*/
public MongoDbDestination() {
}
/**
* Sets the mongo DB template
* @param mongoTemplate the mongo DB template
*/
@Autowired(required=true)
public void setMongoTemplate(MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
/**
* Sets the raw mongo connection
* @param mongo the raw mongo connection
*/
@Autowired(required=true)
public void setMongo(Mongo mongo) {
this.mongo = mongo;
}
/**
* {@inheritDoc}
*/
@Override
@ManagedAttribute(description="The time-series model definition")
public String getTsDefinition() {
return tsDefinition;
}
/**
* Returns the time-series model definition
* @param tsDefinition the time-series model definition
*/
public void setTsDefinition(String tsDefinition) {
this.tsDefinition = tsDefinition;
}
/**
* {@inheritDoc}
*/
@Override
@ManagedAttribute(description="The time-series model matrix")
public long[][] getModelMatrix() {
if(tsModel==null) return null;
return tsModel.getModelMatrix();
}
/**
* {@inheritDoc}
*/
@Override
@ManagedAttribute(description="The time-series model matrix as a string [periodDuration.seconds, tier.tierDuration.seconds, tier.periodCount]")
public String getModelMatrixString() {
if(tsModel==null) return null;
return Arrays.deepToString(tsModel.getModelMatrix());
}
/**
* {@inheritDoc}
*/
@Override
@ManagedAttribute(description="The time-series tiers")
public Tier[] getTimeSeriesTiers() {
return tsModel.getModelTiers().toArray(new Tier[0]);
}
}