/* * Copyright (C) 2008 Jive Software. 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. */ package org.jivesoftware.openfire.reporting.stats; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimerTask; import org.jivesoftware.openfire.archive.MonitoringConstants; import org.jivesoftware.openfire.cluster.ClusterManager; import org.jivesoftware.openfire.reporting.util.TaskEngine; import org.jivesoftware.openfire.stats.Statistic; import org.jivesoftware.openfire.stats.StatisticsManager; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.cache.CacheFactory; import org.jrobin.core.ConsolFuns; import org.jrobin.core.DsTypes; import org.jrobin.core.FetchData; import org.jrobin.core.RrdBackendFactory; import org.jrobin.core.RrdDb; import org.jrobin.core.RrdDef; import org.jrobin.core.RrdException; import org.jrobin.core.Sample; import org.picocontainer.Startable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The stats workhorse. Handles the job of sampling the different statistics existing in * the system and persiting them to the database. Also, it tracks through a <i>StatDefinition</i> * for each stat all the meta information related to a stat. * * @author Alexander Wenckus */ public class StatsEngine implements Startable { private static final Logger Log = LoggerFactory.getLogger(StatsEngine.class); private static final int STAT_RESOULUTION = 60; private final TaskEngine taskEngine; private final StatisticsManager statsManager; private final Map<String, StatDefinition> definitionMap = new HashMap<String, StatDefinition>(); private final Map<String, List<StatDefinition>> multiMap = new HashMap<String, List<StatDefinition>>(); private SampleTask samplingTask = new SampleTask(); /** * The default constructor used by the plugin container. * * @param taskEngine Used to execute tasks. */ public StatsEngine(TaskEngine taskEngine) { this.taskEngine = taskEngine; statsManager = StatisticsManager.getInstance(); } public void start() { try { // Set that RRD files will be stored in the database RrdBackendFactory.registerAndSetAsDefaultFactory(new RrdSqlBackendFactory()); // After 10 milliseconds begin sampling in 60 second intervals. Note: We need to start // asap so that the UI can access this info upon start up taskEngine.scheduleAtFixedRate(samplingTask, 10, STAT_RESOULUTION * 1000L); } catch (RrdException e) { Log.error("Error initializing RrdbPool.", e); } } public void stop() { // Clean-up sampling task samplingTask.cancel(); } private void checkDatabase(StatDefinition[] def) throws RrdException, IOException { File directory = new File(getStatsDirectroy()); if (directory.exists()) { // check if the rrd exists File rrdFile = new File(getRrdFilePath(def[0].getDbPath())); if (rrdFile.exists() && rrdFile.canRead()) { try { // Import existing RRD file into the DB RrdSqlBackend.importRRD(def[0].getDbPath(), rrdFile); // Delete the RRD file rrdFile.delete(); } catch (IOException e) { Log.error("Error importing rrd file: " + rrdFile, e); } } } // check if the rrd exists if (!RrdSqlBackend.exists(def[0].getDbPath())) { RrdDb db = null; try { RrdDef rrdDef = new RrdDef(def[0].getDbPath(), STAT_RESOULUTION); for (StatDefinition stat : def) { String dsType = determineDsType(stat.getStatistic().getStatType()); rrdDef.addDatasource(stat.getDatasourceName(), dsType, 5 * STAT_RESOULUTION, 0, Double.NaN); } // Every minute for 1 hour. rrdDef.addArchive(((DefaultStatDefinition) def[0]). consolidationFunction, 0.5, 1, 60); // Every half-hour for 1 day. rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 30, 48); // Every day for 5 years. rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 1440, 1825); // Every week for 5 years. rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 10080, 260); // Every month for 5 years. rrdDef.addArchive(ConsolFuns.CF_AVERAGE, 0.5, 43200, 60); db = new RrdDb(rrdDef); } finally { if(db != null) { db.close(); } } } } private String determineDsType(Statistic.Type statType) { return DsTypes.DT_GAUGE; } /** * Returns the path to the RRD file. * * @param datasourceName the name of the data source. * @return the path to the RRD file. */ private String getRrdFilePath(String datasourceName) { return getStatsDirectroy() + datasourceName + ".rrd"; } /** * Returns the directory in which all of the stat databases will be stored. * * @return Returns the directory in which all of the stat databases will be stored. */ private String getStatsDirectroy() { return JiveGlobals.getHomeDirectory() + File.separator + MonitoringConstants.NAME + File.separator + "stats" + File.separator; } private StatDefinition createDefintion(String key) { StatDefinition def = definitionMap.get(key); if (def == null) { Statistic statistic = statsManager.getStatistic(key); String statGroup = statsManager.getMultistatGroup(key); try { def = new DefaultStatDefinition(statGroup != null ? statGroup : key, key, statistic); // If the definition is a part of a group check to see all defiintions have been // made for that group StatDefinition[] definitions; if (statGroup != null) { definitions = checkAndCreateGroup(statGroup, def, true); } else { definitions = new StatDefinition[]{def}; multiMap.put(key, Arrays.asList(definitions)); } if (definitions != null) { checkDatabase(definitions); } definitionMap.put(key, def); } catch (RrdException e) { Log.error("Error creating database definition", e); } catch (IOException e) { Log.error("Error creating database definition", e); } } return def; } /** * Checks to see that all StatDefinitions for a stat group have been created. If they have * then an array of the StatDefinitions will be returned, if they haven't Null will be returned. * <p> * The purpose of this is to know when a database should be initialized, after all the StatDefinitions * have been created. * * @param statGroup The statGroup being checked * @param def The statdefinition that is being added to the statGroup * @return Null if the statgroup is completely defined and an array of statdefinitions if it is. */ private StatDefinition[] checkAndCreateGroup(String statGroup, StatDefinition def, boolean shouldCreate) { List<StatDefinition> statList = multiMap.get(statGroup); if (shouldCreate && statList == null) { statList = new ArrayList<StatDefinition>(); multiMap.put(statGroup, statList); } if (statList == null) { return null; } if (shouldCreate) { statList.add(def); } StatDefinition[] definitions; if (statsManager.getStatGroup(statGroup).size() == statList.size()) { definitions = statList.toArray(new StatDefinition[statList.size()]); } else { definitions = null; } return definitions; } /** * Returns the last minute that passed in seconds since the epoch. * * @return the last minute that passed in seconds since the epoch. */ private static long getLastMinute() { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); calendar.set(Calendar.SECOND, 0); return calendar.getTimeInMillis() / 1000; } /** * Returns the definition or definitions related to a statkey. There can be multiple * definitions if a stat is a multistat. * * @param statKey The key for which the definition is desired. * @return Returns the definition or definitions related to a statkey. There can be multiple * definitions if a stat is a multistat. */ StatDefinition[] getDefinition(String statKey) { List<StatDefinition> defs = multiMap.get(statKey); if (defs == null) { StatDefinition def = definitionMap.get(statKey); if (def != null) { return new StatDefinition[] {def}; } else { return null; } } else { return defs.toArray(new StatDefinition[defs.size()]); } } /** * Returns any multistat group names and any stats that are not part of a multistat. * * @return Returns any multistat group names and any stats that are not part of a multistat. */ String [] getAllHighLevelNames() { Set<String> keySet = multiMap.keySet(); return keySet.toArray(new String[keySet.size()]); } /** * The task which samples statistics and persits them to the database. * * @author Alexander Wenckus */ private class SampleTask extends TimerTask { private long lastSampleTime = 0; @Override public void run() { if (!ClusterManager.isSeniorClusterMember()) { // Create statistics definitions but do not sample them since we are not the senior cluster member for (Map.Entry<String, Statistic> statisticEntry : statsManager.getAllStatistics()) { String key = statisticEntry.getKey(); StatDefinition def = createDefintion(key); // Check to see if this stat belongs to a multi-stat and if that multi-stat group // has been completly defined String group = statsManager.getMultistatGroup(key); if (group != null) { checkAndCreateGroup(group, def, false); } } return; } long newTime = getLastMinute(); if (lastSampleTime != 0 && newTime <= lastSampleTime) { Log.warn("Sample task not run because less then a second has passed since last " + "sample."); return; } lastSampleTime = newTime; // Gather sample statistics from remote cluster nodes Collection<Object> remoteSamples = CacheFactory.doSynchronousClusterTask(new GetStatistics(), false); List<String> sampledStats = new ArrayList<String>(); for (Map.Entry<String, Statistic> statisticEntry : statsManager.getAllStatistics()) { String key = statisticEntry.getKey(); StatDefinition def = createDefintion(key); // Check to see if this stat belongs to a multi-stat and if that multi-stat group // has been completly defined String group = statsManager.getMultistatGroup(key); StatDefinition [] definitions; if (group != null) { definitions = checkAndCreateGroup(group, def, false); if (definitions == null || sampledStats.contains(def.getDatasourceName())) { continue; } } else { definitions = new StatDefinition[]{def}; } RrdDb db = null; try { newTime = getLastMinute(); if (def.lastSampleTime <= 0) { for(StatDefinition definition : definitions) { definition.lastSampleTime = newTime; // It is possible that this plugin and thus the StatsEngine didn't // start when Openfire started so we want to put the stats in a known // state for proper sampling. sampleStat(key, definition); } continue; } db = new RrdDb(def.getDbPath(), false); // We want to double check the last sample time recorded in the db so as to // prevent the log files from being inundated if more than one instance of // Openfire is updating the same database. Also, if there is a task taking a // long time to complete if(newTime <= db.getLastArchiveUpdateTime()) { Log.warn("Sample time of " + newTime + " for statistic " + key + " is " + "invalid."); } Sample sample = db.createSample(newTime); if (Log.isDebugEnabled()) { Log.debug("Stat: " + db.getPath() + ". Last sample: " + db.getLastUpdateTime() + ". New sample: " + sample.getTime()); } for (StatDefinition definition : definitions) { // Get a statistic sample of this JVM double statSample = sampleStat(key, definition); // Add up samples of remote cluster nodes for (Object nodeResult : remoteSamples) { Map<String, Double> nodeSamples = (Map<String, Double>) nodeResult; Double remoteSample = nodeSamples.get(key); if (remoteSample != null) { statSample += remoteSample; } } // Update sample with values sample.setValue(definition.getDatasourceName(), statSample); sampledStats.add(definition.getDatasourceName()); definition.lastSampleTime = newTime; definition.lastSample = statSample; } sample.update(); } catch (IOException e) { Log.error("Error sampling for statistic " + key, e); } catch (RrdException e) { Log.error("Error sampling for statistic " + key, e); } finally { if (db != null) { try { db.close(); } catch (IOException e) { Log.error("Error releasing db resource", e); } } } } } /** * Profiles the sampling to make sure that it does not take longer than half a second to * complete, if it does, a warning is logged. * * @param statKey the key related to the statistic. * @param definition the statistic definition for the stat to be sampled. * @return the sample. */ private double sampleStat(String statKey, StatDefinition definition) { long start = System.currentTimeMillis(); double sample = definition.getStatistic().sample(); if (System.currentTimeMillis() - start >= 500) { Log.warn("Stat " + statKey + " took longer than a second to sample."); } return sample; } } /** * Class to process all information retrieved from the stats databases. It also retains * any meta information related to these databases. * * @author Alexander Wenckus */ private class DefaultStatDefinition extends StatDefinition { private String consolidationFunction; DefaultStatDefinition(String dbPath, String datasourceName, Statistic stat) { super(dbPath, datasourceName, stat); this.consolidationFunction = determineConsolidationFun(stat.getStatType()); } private String determineConsolidationFun(Statistic.Type type) { switch (type) { case count: return ConsolFuns.CF_LAST; default: return ConsolFuns.CF_AVERAGE; } } @Override public double[][] getData(long startTime, long endTime) { return fetchData(consolidationFunction, startTime, endTime, -1); } @Override public double[][] getData(long startTime, long endTime, int dataPoints) { // Our greatest datapoints is 60 so if it is something less than that // then we want an average. return fetchData((dataPoints != 60 ? ConsolFuns.CF_AVERAGE : consolidationFunction), startTime, endTime, dataPoints); } @Override public long getLastSampleTime() { return lastSampleTime; } @Override public double getLastSample() { return lastSample; } @Override public double[] getMax(long startTime, long endTime) { return getMax(startTime, endTime, 1); } private double discoverMax(double[] doubles) { double max = 0; for (double d : doubles) { if (d > max) { max = d; } } return max; } private double[][] fetchData(String function, long startTime, long endTime, int dataPoints) { RrdDb db = null; try { db = new RrdDb(getDbPath(), true); FetchData data; if (dataPoints > 0) { data = db.createFetchRequest(function, startTime, endTime, getResolution(startTime, endTime, dataPoints)).fetchData(); } else { data = db.createFetchRequest(function, startTime, endTime).fetchData(); } return data.getValues(); } catch (IOException e) { Log.error("Error initializing Rrdb", e); } catch (RrdException e) { Log.error("Error initializing Rrdb", e); } finally { try { if (db != null) { db.close(); } } catch (IOException e) { Log.error("Unable to release Rrdb resources",e); } } return null; } private long getResolution(long startTime, long endTime, int dataPoints) { return (endTime - startTime) / (dataPoints * 60); } @Override public double[] getMin(long startTime, long endTime) { return getMin(startTime, endTime, 1); } @Override public double[] getMin(long startTime, long endTime, int dataPoints) { double[][] fetchedData = fetchData(consolidationFunction, startTime, endTime, dataPoints); if (fetchedData != null) { double[] toReturn = new double[fetchedData.length]; for (int i = 0; i < fetchedData.length; i++) { toReturn[i] = discoverMin(fetchedData[i]); } return toReturn; } return new double[] { 0 }; } @Override public double[] getMax(long startTime, long endTime, int dataPoints) { double[][] fetchedData = fetchData(consolidationFunction, startTime, endTime, dataPoints); if (fetchedData != null) { double[] toReturn = new double[fetchedData.length]; for (int i = 0; i < fetchedData.length; i++) { toReturn[i] = discoverMax(fetchedData[i]); } return toReturn; } return new double[] { 0 }; } private double discoverMin(double[] doubles) { double min = doubles[0]; for (double d : doubles) { if (d < min) { min = d; } } return min; } } }