/**
* 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.cube;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Collection;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.helios.apmrouter.destination.BaseDestination;
import org.helios.apmrouter.metric.IMetric;
import org.helios.apmrouter.util.RepeatingEventHandler;
import org.helios.apmrouter.wsclient.WebSocketClient;
import org.helios.apmrouter.wsclient.WebSocketEventListener;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.support.MetricType;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* <p>Title: CubeDestination</p>
* <p>Description: Sends metrics to <a href="https://github.com/square/cube">Cube</a></p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.destination.cube.CubeDestination</code></p>
*/
public class CubeDestination extends BaseDestination implements WebSocketEventListener, Runnable {
/** The Cube URI, e.g. <code>ws://localhost:1080/1.0/event/put</code> */
protected URI cubeUri = null;
/** The WebSocketClient */
protected WebSocketClient webSockClient;
/** The connected state */
protected final AtomicBoolean connected = new AtomicBoolean(false);
/** The expect close flag */
protected final AtomicBoolean expectClose = new AtomicBoolean(false);
/** The reconnect loop scheduled task */
protected final AtomicReference<ScheduledFuture<?>> reconnectSchedule = new AtomicReference<ScheduledFuture<?>>(null);
/** The task scheduler */
@Autowired(required=false)
protected ThreadPoolTaskScheduler scheduler = null;
/** Repeating connection exception handler */
protected final RepeatingEventHandler<String> connectExceptionCounter = new RepeatingEventHandler<String>();
/** The error handler for repeating websocket connection failures */
public static final String WEB_SOCK_CONNECT_ERR = "WebSocketConnectException";
/**
* Starts this listener
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStart()
*/
@Override
protected void doStart() throws Exception {
super.doStart();
doConnect();
}
/**
* Stops this listener
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStop()
*/
@Override
protected void doStop() {
expectClose.set(true);
webSockClient.close();
super.doStop();
}
/**
* Initiates an asynch connect to the graphite server
*/
protected void doConnect() {
try {
webSockClient = WebSocketClient.getNewInstance(cubeUri, this);
connected.set(true);
} catch (Exception ex) {
if(connectExceptionCounter.report(WEB_SOCK_CONNECT_ERR)) {
error(connectExceptionCounter.getRemainingMessage(WEB_SOCK_CONNECT_ERR).replace("##cubeUri##", cubeUri.toString()), ex );
}
}
}
/**
* Accept Route additive for BaseDestination extensions
* @param routable The metric to route
*/
@Override
protected void doAcceptRoute(IMetric routable) {
if(connected.get()) {
writeToCube(routable.getUnmapped());
incr("MetricsForwarded");
} else {
incr("MetricsDropped");
}
}
/** The format of a Cube message */
public static final String CUBE_MSG_FORMAT = "{\"type\":\"metric\", \"data\":{" +
"\"host\":\"%s\"," +
"\"agent\":\"%s\"," +
"\"fqn\":\"%s\"," +
"\"type\":\"%s\"," +
"\"ts\":%s," +
"\"val\":%s" +
"}}";
/**
* Writes the metric to cube
* @param metric The metric to write
*/
protected void writeToCube(IMetric metric) {
String message = String.format(CUBE_MSG_FORMAT,
metric.getHost(), metric.getAgent(),
metric.getFQN(), metric.getType().name(),
metric.getTime(), metric.getLongValue()
);
webSockClient.sendRequest(message);
}
/**
* Creates a new CubeDestination
* @param patterns The {@link IMetric} pattern this destination accepts
*/
public CubeDestination(String... patterns) {
super(patterns);
connectExceptionCounter.register(WEB_SOCK_CONNECT_ERR, 3, 20, 120000, "Failed to connect to [##cubeUri##]. This message will log %s more times and then periodically\n");
}
/**
* Creates a new CubeDestination
* @param patterns The {@link IMetric} pattern this destination accepts
*/
public CubeDestination(Collection<String> patterns) {
super(patterns);
connectExceptionCounter.register(WEB_SOCK_CONNECT_ERR, 3, 20, 120000, "Failed to connect to [##cubeUri##]. This message will log %s more times and then periodically\n");
}
/**
* Creates a new CubeDestination
*/
public CubeDestination() {
connectExceptionCounter.register(WEB_SOCK_CONNECT_ERR, 3, 20, 120000, "Failed to connect to [##cubeUri##]. This message will log %s more times and then periodically\n");
}
/**
* Returns the cube websocket URI
* @return the cube websocket URI
*/
@ManagedAttribute(description="The cube websocket URI")
public URI getCubeUri() {
return cubeUri;
}
/**
* Sets the cube websocket URI
* @param cubeUri the cube websocket URI
*/
public void setCubeUri(URI cubeUri) {
this.cubeUri = cubeUri;
}
/**
* Indicates if the cube channel is connected
* @return true if the cube channel is connected, false otherwise
*/
@ManagedAttribute(description="Indicates if this destination is connected to the cube endpoint")
public boolean isConnected() {
return connected.get();
}
/**
* Returns the number of metrics forwarded to Cube
* @return the number of metrics forwarded to Cube
*/
@ManagedMetric(category="Cube", metricType=MetricType.COUNTER, description="the number of metrics forwarded to Cube")
public long getMetricsForwarded() {
return getMetricValue("MetricsForwarded");
}
/**
* Returns the number of metrics that failed on sending to Cube
* @return the number of metrics that failed on sending to Cube
*/
@ManagedMetric(category="Cube", metricType=MetricType.COUNTER, description="the number of metrics that failed on sending to Cube")
public long getMetricsForwardFailures() {
return getMetricValue("MetricsForwardFailures");
}
/**
* Returns the number of metrics that were dropped because Cube was down
* @return the number of metrics that were dropped because Cube was down
*/
@ManagedMetric(category="Cube", metricType=MetricType.COUNTER, description="the number of metrics that were dropped because Cube was down")
public long getMetricsDropped() {
return getMetricValue("MetricsDropped");
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.wsclient.WebSocketEventListener#onConnect(java.net.SocketAddress)
*/
@Override
public void onConnect(SocketAddress remoteAddress) {
info("Cube WebSocketClient Connected to [", cubeUri, "]");
connected.set(true);
expectClose.set(false);
connectExceptionCounter.reset(WEB_SOCK_CONNECT_ERR);
ScheduledFuture<?> sf = reconnectSchedule.getAndSet(null);
if(sf!=null) {
sf.cancel(true);
info("Cancelled Cube [" , cubeUri , "] Reconnect Task");
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.wsclient.WebSocketEventListener#onClose(java.net.SocketAddress)
*/
@Override
public void onClose(SocketAddress remoteAddress) {
connected.set(false);
if(!expectClose.get() && this.reconnectSchedule.get()== null) {
startReconnectLoop();
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.wsclient.WebSocketEventListener#onError(java.net.SocketAddress, java.lang.Throwable)
*/
@Override
public void onError(SocketAddress remoteAddress, Throwable t) {
warn("WebSocket error on cubes destination [", cubeUri, "] ", t);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.wsclient.WebSocketEventListener#onMessage(java.net.SocketAddress, org.json.JSONObject)
*/
@Override
public void onMessage(SocketAddress remoteAddress, JSONObject message) {
System.out.println(message);
}
/**
* <p>The reconnect task</p>
* {@inheritDoc}
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
doConnect();
}
/**
* Starts a periodic reconnect attempt loop
*/
protected void startReconnectLoop() {
if(scheduler==null) {
warn("Cube destination to [", cubeUri, "] was null so destination will not execute re-connect loop");
return;
}
synchronized(reconnectSchedule) {
ScheduledFuture<?> sf = reconnectSchedule.get();
if(sf!=null && !sf.isCancelled()) {
//warn("cube reconnect loop already running");
return;
}
reconnectSchedule.set(scheduler.getScheduledExecutor().scheduleWithFixedDelay(this, 15, 15, TimeUnit.SECONDS));
info("Started cube reconnect loop for [", cubeUri, "]");
}
}
// public void onConnect(WebSocketClient webSock) {
// info("Cube WebSocketClient Connected");
// connected.set(true);
// }
// public void onDisconnect(WebSocketClient webSock) {
// this.webSockClient = null;
// connected.set(false);
// }
// public void onError(Throwable t) {
// error("WebSock Error", t);
// }
// public void onMessage(WebSocketClient webSock, WebSocketFrame frame) {
// if(frame.isText()) {
// info("Unexpected Cube Response [", frame.getTextData(), "]");
// } else {
// info("Unexpected Cube Response Type:", frame.getType());
// }
// }
}