/**
* 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.catalog.jdbc.h2;
import java.net.InetSocketAddress;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import org.helios.apmrouter.catalog.DChannelEvent;
import org.helios.apmrouter.catalog.DChannelEventMBean;
import org.helios.apmrouter.catalog.DChannelEventType;
import org.helios.apmrouter.catalog.EntryStatus;
import org.helios.apmrouter.catalog.EntryStatus.EntryStatusChange;
import org.helios.apmrouter.catalog.MetricCatalogService;
import org.helios.apmrouter.collections.ConcurrentLongSlidingWindow;
import org.helios.apmrouter.collections.ConcurrentLongSortedSet;
import org.helios.apmrouter.collections.LongSlidingWindow;
import org.helios.apmrouter.destination.chronicletimeseries.ChronicleTSManager;
import org.helios.apmrouter.destination.chronicletimeseries.ChronicleTier;
import org.helios.apmrouter.metric.IMetric;
import org.helios.apmrouter.metric.MetricType;
import org.helios.apmrouter.metric.catalog.ICEMetricCatalog;
import org.helios.apmrouter.metric.catalog.IDelegateMetric;
import org.helios.apmrouter.server.ServerComponentBean;
import org.helios.apmrouter.server.services.session.DecoratedChannel;
import org.helios.apmrouter.util.SystemClock;
import org.helios.apmrouter.util.SystemClock.ElapsedTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
/**
* <p>Title: H2JDBCMetricCatalog</p>
* <p>Description: The H2 implementation of the {@link MetricCatalogService}. When realtime is set to true
* host, agent and metric timestamps are kept realtime with respect to their <i>last seen</i> timestamp.</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.catalog.jdbc.H2JDBCMetricCatalog</code></p>
*/
public class H2JDBCMetricCatalog extends ServerComponentBean implements MetricCatalogService {
/** The h2 datasource */
protected DataSource ds = null;
/** The Chronicle time-series manager */
protected ChronicleTSManager chronicleManager = null;
/** The Chronicle time-series live tier */
protected ChronicleTier liveTier= null;
/** Indicates if the metric catalog should be kept real time */
protected boolean realtime = false;
/** Sliding windows of catalog call elapsed times in ns. */
protected final LongSlidingWindow elapsedTimesNs = new ConcurrentLongSlidingWindow(50);
/**
* Creates a new H2JDBCMetricCatalog
*/
public H2JDBCMetricCatalog() {
super();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStart()
*/
@Override
public void doStart() throws Exception {
if(realtime) {
info("\n\t#############################\n\tMetric Catalog [", getClass().getSimpleName(), "] is REALTIME\n\t#############################\n");
}
if(chronicleManager!=null) liveTier = chronicleManager.getLiveTier();
chronicleManager.addStatusListener(this);
Connection conn = null;
PreparedStatement ps = null;
Statement st = null;
try {
conn = ds.getConnection();
//MERGE INTO TEST KEY(ID) VALUES(2, 'World')
ps = conn.prepareStatement("MERGE INTO TRACE_TYPE KEY(TYPE_ID) VALUES(?,?)");
for(MetricType mt: MetricType.values()) {
ps.setInt(1, mt.ordinal());
ps.setString(2, mt.name());
ps.addBatch();
}
ps.executeBatch();
ps.close();
st = conn.createStatement();
st.executeUpdate("UPDATE HOST SET CONNECTED = NULL, AGENTS = 0");
st.executeUpdate("UPDATE AGENT SET CONNECTED = NULL, URI = 'RESTART'");
st.close();
conn.close();
//
// UPDATE AGENT SET CONNECTED = NULL, URI = 'RESTART';
} catch (Exception e) {
throw new RuntimeException("Failed to add metric types", e);
} finally {
if(st!=null) try { st.close(); } catch (Exception e) {/* No Op */}
if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
//SharedChannelGroup.getInstance().addSessionListener(this);
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.catalog.EntryStatusChangeListener#onEntryStatusChange(java.util.Map)
*/
@Override
public void onEntryStatusChange(Map<EntryStatus, EntryStatusChange> changeMap) {
Connection conn = null;
PreparedStatement ps = null;
long start = System.currentTimeMillis();
try {
conn = ds.getConnection();
//ps = conn.prepareStatement("UPDATE METRIC SET STATE=?, LAST_SEEN=? WHERE METRIC_ID=?");
ps = conn.prepareStatement("UPDATE METRIC SET STATE=? WHERE METRIC_ID=?");
for(Map.Entry<EntryStatus, EntryStatusChange> entry: changeMap.entrySet()) {
byte status = entry.getKey().byteOrdinal();
ConcurrentLongSortedSet metricIds = entry.getValue().getMetricIds();
//Timestamp ts = new Timestamp(entry.getValue().getTimestamp());
for(int i = 0; i < metricIds.size(); i++) {
ps.setByte(1, status);
//ps.setTimestamp(2, ts);
ps.setLong(2, metricIds.get(i));
ps.addBatch();
}
ps.executeBatch();
}
ps.close(); ps = null;
conn.close(); conn = null;
long elapsed = System.currentTimeMillis()-start;
if("TRACE".equals(getLevel())) {
trace(new StringBuilder(EntryStatus.renderStatusCounts(changeMap)).append("\n\tUpdate Elapsed:").append(elapsed).append(" ms."));
}
} catch (Exception e) {
throw new RuntimeException("Exception processing entry status updates", e);
} finally {
if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
}
/** The SQL to fetch a delegate metric ID from a token */
public static final String GET_METRIC_SQL = "SELECT HOST.NAME, AGENT.NAME, TYPE_ID, NAMESPACE, METRIC.NAME "
+ "FROM HOST, AGENT, METRIC "
+ "WHERE AGENT.HOST_ID = HOST.HOST_ID AND METRIC.AGENT_ID = AGENT.AGENT_ID "
+ "AND METRIC_ID = ?";
/**
* Returns the delegate metric for the passed token
* @param token the token
* @return a delegate metric ID
*/
@Override
public IDelegateMetric getMetricID(long token) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rset = null;
try {
incr("TokenLookups");
conn = ds.getConnection();
ps = conn.prepareStatement(GET_METRIC_SQL);
ps.setLong(1, token);
rset = ps.executeQuery();
if(!rset.next()) {
return null;
}
// MetricLastTimeSeenService
CharSequence[] namespace = rset.getString(4).replaceFirst("/", "").split("/");
ICEMetricCatalog.getInstance().setToken(token, rset.getString(1), rset.getString(2), rset.getString(5), MetricType.valueOf(rset.getInt(3)), namespace);
return ICEMetricCatalog.getInstance().get(token);
} catch (SQLException sex) {
sex.printStackTrace(System.err);
return null;
} finally {
if(rset!=null) try { rset.close(); } catch (Exception e) {/* No Op */}
if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
}
/**
* Finds the assigned metric ID for the passed host/agent/name and namespace
* @param host The host name
* @param agent The agent name
* @param namespace The metric namespace
* @param name The metric name
* @return The metric ID or -1 if one was not found
*/
@Override
public long isAssigned(String host, String agent, String namespace, String name) {
Connection conn = null;
CallableStatement cs = null;
try {
conn = ds.getConnection();
cs = conn.prepareCall("? = CALL ASSIGNED(?,?,?,?)");
cs.registerOutParameter(1, Types.NUMERIC);
cs.setNull(1, Types.NULL);
cs.setString(2, host);
cs.setString(3, agent);
cs.setString(4, namespace);
cs.setString(5, name);
cs.execute();
return cs.getLong(1);
} catch (Exception ex) {
throw new RuntimeException("Assign exception", ex);
} finally {
if(cs!=null) try { cs.close(); } catch (Exception ex) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception ex) {/* No Op */}
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.catalog.MetricCatalogService#getID(long, java.lang.String, java.lang.String, int, java.lang.String, java.lang.String)
*/
@Override
public long getID(long tokenRef, String host, String agent, int typeId, String namespace, String name) {
if(tokenRef!=-1 && !realtime) return 0;
SystemClock.startTimer();
final long token = tokenRef!=-1 ? tokenRef : liveTier.createNewMetric();
incr("CallCount");
Connection conn = null;
CallableStatement cs = null;
//PreparedStatement ps = null;
try {
conn = ds.getConnection();
cs = realtime ? conn.prepareCall("? = CALL TOUCH(?,?,?,?,?,?)") : conn.prepareCall("? = CALL GET_ID(?,?,?,?,?,?)");
cs.registerOutParameter(1, Types.NUMERIC);
cs.setNull(1, Types.NULL);
cs.setLong(2, token);
cs.setString(3, host);
cs.setString(4, agent);
cs.setInt(5, typeId);
cs.setString(6, namespace);
cs.setString(7, name);
cs.execute();
long id = cs.getLong(1);
incr("AssignedMetricIDs");
ElapsedTime et = SystemClock.endTimer();
elapsedTimesNs.insert(et.elapsedNs);
return id;
} catch (Exception e) {
error("Failed to get ID for [" , String.format("%s/%s%s:%s", host, agent, namespace, name) , "]", e);
Throwable cause = e.getCause();
if(cause!=null) cause.printStackTrace(System.err);
//throw new RuntimeException("Failed to get ID", e);
return 0;
} finally {
//if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(cs!=null) try { cs.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.catalog.MetricCatalogService#touch(java.util.Collection)
*/
@Override
public int touch(Collection<IMetric> metrics) {
if(realtime && metrics!=null && !metrics.isEmpty()) {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = ds.getConnection();
ps = conn.prepareStatement("UPDATE METRIC SET LAST_SEEN = systimestamp WHERE METRIC_ID = ?");
for(IMetric metric: metrics) {
ps.setLong(1, metric.getMetricId().getToken());
ps.addBatch();
}
return ps.executeBatch().length;
} catch (Exception ex) {
error("Failed to touch timestamps on [" + metrics.size() + "] metrics", ex);
return 0;
} finally {
if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
}
return 0;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.catalog.MetricCatalogService#hostAgentState(boolean, java.lang.String, java.lang.String, java.lang.String, java.lang.String)
*/
@Override
public DChannelEvent hostAgentState(boolean connected, String host, String ip, String agent, String agentURI) {
SystemClock.startTimer();
incr("CallCount");
Connection conn = null;
PreparedStatement ps = null;
ResultSet rset = null;
try {
conn = ds.getConnection();
ps = conn.prepareStatement("CALL HOSTAGENTSTATE(?,?,?,?,?)");
ps.setBoolean(1, connected);
ps.setString(2, host);
ps.setString(3, ip);
ps.setString(4, agent);
ps.setString(5, agentURI);
rset = ps.executeQuery();
rset.next();
int agentCount = rset.getInt(1);
int hostId = rset.getInt(2);
int agentId = rset.getInt(3);
String[] domain = rset.getString(4).split("\\.");
ElapsedTime et = SystemClock.endTimer();
elapsedTimesNs.insert(et.elapsedNs);
return DChannelEvent.newEvent(connected ? DChannelEventType.IDENT : DChannelEventType.CLOSED,
domain, host, hostId, agent, agentId,
connected ? agentCount==1 : agentCount<1
);
} catch (Exception e) {
error("Failed to update host/agent state for [" , String.format("%s/%s:%s", connected, host, agent) , "]", e);
Throwable cause = e.getCause();
if(cause!=null) cause.printStackTrace(System.err);
return null;
} finally {
if(rset!=null) try { rset.close(); } catch (Exception e) {/* No Op */}
if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
}
/** The base host SQL */
public static final String LIST_HOSTS_SQL = "SELECT NAME, HOST_ID FROM HOST";
/** The online host SQL */
public static final String LIST_ONLINE_HOSTS_SQL = "SELECT NAME, HOST_ID FROM HOST WHERE CONNECTED IS NOT NULL";
/**
* Lists registered hosts
* @param onlineOnly If true, only lists online hosts
* @return A map of host names keyed by host ID
*/
@Override
public Map<Integer, String> listHosts(boolean onlineOnly) {
SystemClock.startTimer();
incr("CallCount");
Connection conn = null;
PreparedStatement ps = null;
ResultSet rset = null;
Map<Integer, String> map = new HashMap<Integer, String>();
try {
conn = ds.getConnection();
ps = conn.prepareStatement(onlineOnly ? LIST_ONLINE_HOSTS_SQL : LIST_HOSTS_SQL);
rset = ps.executeQuery();
while(rset.next()) {
map.put(rset.getInt(2), rset.getString(1));
}
ElapsedTime et = SystemClock.endTimer();
elapsedTimesNs.insert(et.elapsedNs);
return map;
} catch (Exception e) {
error("Failed to list hosts" , e);
Throwable cause = e.getCause();
if(cause!=null) cause.printStackTrace(System.err);
throw new RuntimeException("Failed to list hosts" , e);
} finally {
if(rset!=null) try { rset.close(); } catch (Exception e) {/* No Op */}
if(ps!=null) try { ps.close(); } catch (Exception e) {/* No Op */}
if(conn!=null) try { conn.close(); } catch (Exception e) {/* No Op */}
}
}
/**
* Sets the h2 datasource
* @param ds the h2 datasource
*/
@Autowired(required=true)
@Qualifier("H2DataSource")
public void setDs(DataSource ds) {
this.ds = ds;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponent#getSupportedMetricNames()
*/
@Override
public Set<String> getSupportedMetricNames() {
Set<String> metrics = new HashSet<String>(super.getSupportedMetricNames());
metrics.add("AssignedMetricIDs");
metrics.add("CallCount");
metrics.add("TokenLookups");
return metrics;
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponent#resetMetrics()
*/
@Override
public void resetMetrics() {
super.resetMetrics();
elapsedTimesNs.clear();
}
/**
* Returns the number of assigned metric IDs
* @return the number of assigned metric IDs
*/
@ManagedMetric(category="MetricCatalogService", metricType=org.springframework.jmx.support.MetricType.COUNTER, description="The number of assigned metric IDs")
public long getAssignedMetricIDs() {
return getMetricValue("AssignedMetricIDs");
}
/**
* Returns the number of token lookups
* @return the number of token lookups
*/
@ManagedMetric(category="MetricCatalogService", metricType=org.springframework.jmx.support.MetricType.COUNTER, description="The number of token lookups")
public long getTokenLookups() {
return getMetricValue("TokenLookups");
}
/**
* Returns the cumulative number of catalog calls
* @return the cumulative number of catalog calls
*/
@ManagedMetric(category="MetricCatalogService", metricType=org.springframework.jmx.support.MetricType.COUNTER, description="The cumulative number of catalog calls")
public long getCallCount() {
return getMetricValue("CallCount");
}
/**
* Returns the sliding average elapsed time in ns. of the last 50 catalog calls
* @return the sliding average elapsed time in ns. of the last 50 catalog calls
*/
@ManagedMetric(category="MetricCatalogService", displayName="AverageCallTimeNs", metricType=org.springframework.jmx.support.MetricType.GAUGE, description="The sliding average elapsed time in ns. of the last 50 catalog calls")
public long getAverageCallTimeNs() {
return elapsedTimesNs.avg();
}
/**
* Returns the sliding average elapsed time in ms. of the last 50 catalog calls
* @return the sliding average elapsed time in ms. of the last 50 catalog calls
*/
@ManagedMetric(category="MetricCatalogService", displayName="AverageCallTimeMs", metricType=org.springframework.jmx.support.MetricType.GAUGE, description="The sliding average elapsed time in ms. of the last 50 catalog calls")
public long getAverageCallTimeMs() {
return TimeUnit.MILLISECONDS.convert(getAverageCallTimeNs(), TimeUnit.NANOSECONDS);
}
/**
* Indicates if the metric catalog is real time
* @return true if the metric catalog is real time , false otherwise
*/
@Override
@ManagedAttribute(description="Indicates if the metric catalog is real time ")
public boolean isRealtime() {
return realtime;
}
/**
* Sets the realtime attribute of the metric catalog
* @param realtime true for a realtime metric catalog, false otherwise
*/
@Override
@ManagedAttribute(description="Indicates if the metric catalog is real time ")
public void setRealtime(boolean realtime) {
this.realtime = realtime;
}
/**
* <p>NoOp</p>
* {@inheritDoc}
* @see org.helios.apmrouter.server.services.session.ChannelSessionListener#onConnectedChannel(org.helios.apmrouter.server.services.session.DecoratedChannel)
*/
@Override
public void onConnectedChannel(DecoratedChannel channel) {
// No OP
}
/**
* <p>Updates the catalog service to mark the agent down if the agent name is not null or empty</p>
* {@inheritDoc}
* @see org.helios.apmrouter.server.services.session.ChannelSessionListener#onClosedChannel(org.helios.apmrouter.server.services.session.DecoratedChannel)
*/
@Override
public DChannelEventMBean onClosedChannel(DecoratedChannel channel) {
if(channel.getAgent()!=null && !channel.getAgent().trim().isEmpty()) {
DChannelEventMBean result = hostAgentState(false, channel.getHost(), ((InetSocketAddress)channel.getRemoteAddress()).getAddress().getHostAddress(), channel.getAgent(), channel.getType() + "/" + channel.getRemoteAddress().toString());
info("Marked [", channel.getHost(), "/", channel.getAgent(), "] DOWN. Context:" + result);
return result;
}
return null;
}
/**
* <p>Updates the catalog service to mark the agent up if the agent name is not null or empty</p>
* {@inheritDoc}
* @see org.helios.apmrouter.server.services.session.ChannelSessionListener#onIdentifiedChannel(org.helios.apmrouter.server.services.session.DecoratedChannel)
*/
@Override
public DChannelEventMBean onIdentifiedChannel(DecoratedChannel channel) {
if(channel.getAgent()!=null && !channel.getAgent().trim().isEmpty()) {
DChannelEventMBean result = hostAgentState(true, channel.getHost(), ((InetSocketAddress)channel.getRemoteAddress()).getAddress().getHostAddress(), channel.getAgent(), channel.getURI());
info("Marked [", channel.getHost(), "/", channel.getAgent(), "] UP. Context:" + result);
return result;
}
return null;
}
/**
* Sets the chronicle time-series manager
* @param chronicleManager the chronicleManager to set
*/
public void setChronicleManager(ChronicleTSManager chronicleManager) {
this.chronicleManager = chronicleManager;
}
}