/***********************************************************************************
*
* Copyright (c) 2014 Kamil Baczkowicz
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
*
* Kamil Baczkowicz - initial API and implementation and/or initial documentation
*
*/
package pl.baczkowicz.mqttspy.stats;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.xml.bind.JAXBElement;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.namespace.QName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.baczkowicz.mqttspy.configuration.ConfigurationManager;
import pl.baczkowicz.mqttspy.stats.generated.MqttSpyStats;
import pl.baczkowicz.spy.exceptions.XMLException;
import pl.baczkowicz.spy.utils.ThreadingUtils;
import pl.baczkowicz.spy.xml.XMLParser;
/**
* This class is responsible for loading, processing and saving processing statistics.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public class StatisticsManager implements Runnable
{
final static Logger logger = LoggerFactory.getLogger(StatisticsManager.class);
public static final String PACKAGE = "pl.baczkowicz.mqttspy.stats.generated";
public static final String SCHEMA = "/mqtt-spy-stats.xsd";
private static final String STATS_FILENAME = "mqtt-spy-stats.xml";
public final static List<Integer> periods = Arrays.asList(5, 30, 300);
private final static int removeAfter = 301;
private final XMLParser parser;
private File statsFile;
public static MqttSpyStats stats;
public static Map<String, ConnectionStats> runtimeMessagesPublished = new HashMap<>();
public static Map<String, ConnectionStats> runtimeMessagesReceived = new HashMap<>();
public StatisticsManager() throws XMLException
{
this.parser = new XMLParser(PACKAGE, SCHEMA);
statsFile = new File(ConfigurationManager.getDefaultHomeDirectory() + STATS_FILENAME);
new Thread(this).start();
}
public boolean loadStats()
{
try
{
stats = (MqttSpyStats) parser.loadFromFile(statsFile);
return true;
}
catch (XMLException e)
{
logger.error("Cannot process the statistics file at " + statsFile.getAbsolutePath(), e);
}
catch (FileNotFoundException e)
{
logger.error("Cannot read the statistics file from " + statsFile.getAbsolutePath(), e);
}
// If reading the stats failed...
final Random random = new Random();
stats = new MqttSpyStats();
stats.setID(random.nextLong());
final GregorianCalendar gc = new GregorianCalendar();
gc.setTime(new Date());
try
{
stats.setStartDate(DatatypeFactory.newInstance().newXMLGregorianCalendar(gc));
}
catch (DatatypeConfigurationException e)
{
logger.error("Cannot create date for stats", e);
}
return false;
}
public boolean saveStats()
{
try
{
// Create the mqtt-spy home directory if it doesn't exit
new File(ConfigurationManager.getDefaultHomeDirectory()).mkdirs();
if(!statsFile.exists())
{
statsFile.createNewFile();
}
// This is in case the previous version of mqtt-spy create a directory with the same name as the stats file
else if (statsFile.isDirectory())
{
statsFile.delete();
statsFile.createNewFile();
}
parser.saveToFile(statsFile,
new JAXBElement(new QName("http://baczkowicz.pl/mqtt-spy-stats", "MqttSpyStats"), MqttSpyStats.class, stats));
return true;
}
catch (XMLException | IOException e)
{
logger.error("Cannot save the statistics file - " + statsFile.getAbsolutePath(), e);
}
return false;
}
public static void newConnection()
{
stats.setConnections(stats.getConnections() + 1);
}
public static void newSubscription()
{
stats.setSubscriptions(stats.getSubscriptions() + 1);
}
public void messageReceived(final String connectionId, final List<String> subscriptions)
{
// Global stats (saved to XML)
stats.setMessagesReceived(stats.getMessagesReceived() + 1);
synchronized (runtimeMessagesReceived)
{
// Runtime stats
if (runtimeMessagesReceived.get(connectionId) == null)
{
resetConnection(runtimeMessagesReceived, connectionId);
}
runtimeMessagesReceived.get(connectionId).runtimeStats.get(0).add(subscriptions);
}
}
public void messagePublished(final String connectionId, final String topic)
{
// Global stats (saved to XML)
stats.setMessagesPublished(stats.getMessagesPublished() + 1);
synchronized (runtimeMessagesPublished)
{
// Runtime stats
if (runtimeMessagesPublished.get(connectionId) == null)
{
runtimeMessagesPublished.put(connectionId, new ConnectionStats(periods));
runtimeMessagesPublished.get(connectionId).runtimeStats.add(new ConnectionIntervalStats());
}
runtimeMessagesPublished.get(connectionId).runtimeStats.get(0).add(topic);
}
}
public static void nextInterval(final Map<String, ConnectionStats> runtimeMessages)
{
for (final String connectionId : runtimeMessages.keySet())
{
final Map<Integer, ConnectionIntervalStats> avgs = runtimeMessages.get(connectionId).avgPeriods;
final List<ConnectionIntervalStats> runtime = runtimeMessages.get(connectionId).runtimeStats;
final ConnectionIntervalStats lastComplete = runtime.size() > 0 ? runtime.get(0) : new ConnectionIntervalStats();
// Add the last complete and subtract over the interval
for (final int period : avgs.keySet())
{
avgs.get(period).plus(lastComplete);
if (runtime.size() > period)
{
avgs.get(period).minus(runtime.get(period));
}
}
runtime.add(0, new ConnectionIntervalStats());
if (runtime.size() > removeAfter)
{
runtime.remove(removeAfter);
}
}
}
public static void nextInterval()
{
synchronized (runtimeMessagesPublished)
{
nextInterval(runtimeMessagesPublished);
}
synchronized (runtimeMessagesReceived)
{
nextInterval(runtimeMessagesReceived);
}
}
public static ConnectionIntervalStats getMessagesPublished(final String connectionId, final int period)
{
if (runtimeMessagesPublished.get(connectionId) == null)
{
return new ConnectionIntervalStats();
}
return runtimeMessagesPublished.get(connectionId).avgPeriods.get(period).average(period);
}
public static long getMessagesPublished()
{
long total = 0;
for (final ConnectionStats stats : runtimeMessagesPublished.values())
{
if (stats.runtimeStats.size() > 1)
{
total = total + (int) stats.runtimeStats.get(1).overallCount;
}
}
return total;
}
public static long getMessagesReceived()
{
long total = 0;
for (final ConnectionStats stats : runtimeMessagesReceived.values())
{
if (stats.runtimeStats.size() > 1)
{
total = total + (int) stats.runtimeStats.get(1).overallCount;
}
}
return total;
}
public static ConnectionIntervalStats getMessagesReceived(final String connectionId, final int period)
{
if (runtimeMessagesReceived.get(connectionId) == null)
{
return new ConnectionIntervalStats();
}
return runtimeMessagesReceived.get(connectionId).avgPeriods.get(period).average(period);
}
public static void resetMessagesReceived(final String connectionId, final String topic)
{
if (runtimeMessagesReceived.get(connectionId) == null)
{
return;
}
runtimeMessagesReceived.get(connectionId).resetTopic(topic);
}
public static void resetMessagesReceived(final String connectionId)
{
resetConnection(runtimeMessagesReceived, connectionId);
}
private static void resetConnection(final Map<String, ConnectionStats> runtimeMessages, final String connectionId)
{
runtimeMessages.put(connectionId, new ConnectionStats(periods));
runtimeMessages.get(connectionId).runtimeStats.add(new ConnectionIntervalStats());
}
@Override
public void run()
{
ThreadingUtils.logThreadStarting("StatisticsManager");
while (true)
{
if (ThreadingUtils.sleep(1000))
{
break;
}
nextInterval();
}
ThreadingUtils.logThreadEnding();
}
/** Format of the stats label. */
public static final String STATS_FORMAT = "load: " + getPeriodValues();
/**
* Creates the list of all periods defined in the statistics manager.
*
* @return List of all periods
*/
public static String getPeriodList()
{
final StringBuffer sb = new StringBuffer();
final Iterator<Integer> iterator = StatisticsManager.periods.iterator();
while (iterator.hasNext())
{
final int period = (int) iterator.next();
if (period > 60)
{
sb.append((period / 60) + "m");
}
else
{
sb.append(period + "s");
}
if (iterator.hasNext())
{
sb.append(", ");
}
}
return sb.toString();
}
/**
* Creates the stats format for all periods defined in the statistics manager.
*
* @return Format for all periods
*/
public static String getPeriodValues()
{
final StringBuffer sb = new StringBuffer();
final Iterator<Integer> iterator = StatisticsManager.periods.iterator();
while (iterator.hasNext())
{
sb.append("%.1f");
iterator.next();
if (iterator.hasNext())
{
sb.append("/");
}
}
return sb.toString();
}
}