/*
* Copyright (C) 2003-2010 eXo Platform SAS.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see<http://www.gnu.org/licenses/>.
*/
package org.exoplatform.services.jcr.statistics;
import org.exoplatform.commons.utils.PrivilegedFileHelper;
import org.exoplatform.commons.utils.PrivilegedSystemHelper;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.management.ManagementContext;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.annotations.ManagedName;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.management.rest.annotations.RESTEndpoint;
import org.exoplatform.services.jcr.storage.WorkspaceStorageConnection;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* This class manages all the statistics of eXo JCR. This will print all the metrics value into a file in csv format
* for all the registered statistics. It will provide metrics of type minimum, maximum, total, times and average
* for each method of {@link WorkspaceStorageConnection} and the global values. It will add data into the file
* every 5 seconds and add the last line at JVM exit. This class will also expose all the statistics through JMX.
*
* Created by The eXo Platform SAS
* Author : Nicolas Filotto
* nicolas.filotto@exoplatform.com
* 30 mars 2010
*/
@Managed
@ManagedDescription("JCR statistics manager")
@NameTemplate({@Property(key = "view", value = "jcr"), @Property(key = "service", value = "statistic")})
@RESTEndpoint(path = "jcrstatistics")
public class JCRStatisticsManager
{
/**
* The logger
*/
private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.JCRStatisticsManager");
/**
* The name of the global statistics
*/
private static final String GLOBAL_STATISTICS_NAME = "global";
/**
* The flag that indicates if the manager is launched or not.
*/
private static volatile boolean STARTED;
/**
* The list of all the contexts of statistics managed
*/
private static Map<String, StatisticsContext> CONTEXTS =
Collections.unmodifiableMap(new HashMap<String, StatisticsContext>());
/**
* Indicates if the persistence of the statistics has to be enabled.
*/
public static final boolean PERSISTENCE_ENABLED =
Boolean.valueOf(PrivilegedSystemHelper.getProperty("JCRStatisticsManager.persistence.enabled", "true"));
/**
* The length of time in milliseconds after which the snapshot of the statistics is persisted.
*/
public static final long PERSISTENCE_TIMEOUT =
Long.valueOf(PrivilegedSystemHelper.getProperty("JCRStatisticsManager.persistence.timeout", "15000"));
/**
* Default constructor.
*/
private JCRStatisticsManager()
{
}
/**
* Register a new category of statistics to manage.
* @param category the name of the category of statistics to register.
* @param global the global statistics.
* @param allStatistics the list of all statistics corresponding to the category.
*/
public static void registerStatistics(String category, Statistics global, Map<String, Statistics> allStatistics)
{
if (category == null || category.length() == 0)
{
throw new IllegalArgumentException("The category of the statistics cannot be empty");
}
if (allStatistics == null || allStatistics.isEmpty())
{
throw new IllegalArgumentException("The list of statistics " + category + " cannot be empty");
}
PrintWriter pw = null;
if (PERSISTENCE_ENABLED)
{
pw = initWriter(category);
if (pw == null)
{
LOG.warn("Cannot create the print writer for the statistics " + category);
}
}
startIfNeeded();
synchronized (JCRStatisticsManager.class)
{
Map<String, StatisticsContext> tmpContexts = new HashMap<String, StatisticsContext>(CONTEXTS);
StatisticsContext ctx = new StatisticsContext(pw, global, allStatistics);
tmpContexts.put(category, ctx);
if (pw != null)
{
// Define the file header
printHeader(ctx);
}
CONTEXTS = Collections.unmodifiableMap(tmpContexts);
}
}
/**
* Initialize the {@link PrintWriter}.
* It will first try to create the file in the user directory, if it cannot, it will try
* to create it in the temporary folder.
* @return the corresponding {@link PrintWriter}
*/
private static PrintWriter initWriter(String category)
{
PrintWriter pw = null;
File file = null;
try
{
file =
new File(PrivilegedSystemHelper.getProperty("user.dir"), "Statistics" + category + "-"
+ System.currentTimeMillis() + ".csv");
file.createNewFile();
pw = new PrintWriter(file);
}
catch (IOException e)
{
LOG.error("Cannot create the file for the statistics " + category
+ " in the user directory, we will try to create it in the temp directory", e);
try
{
file =
PrivilegedFileHelper.createTempFile("Statistics" + category, "-" + System.currentTimeMillis() + ".csv");
pw = new PrintWriter(file);
}
catch (IOException e1)
{
LOG.error("Cannot create the file for the statistics " + category, e1);
}
}
if (file != null)
{
LOG.info("The file for the statistics " + category + " is " + file.getPath());
}
return pw;
}
/**
* Starts the manager if needed
*/
private static void startIfNeeded()
{
if (!STARTED)
{
synchronized (JCRStatisticsManager.class)
{
if (!STARTED)
{
addTriggers();
ExoContainer container = ExoContainerContext.getTopContainer();
ManagementContext ctx = null;
if (container != null)
{
ctx = container.getManagementContext();
}
if (ctx == null)
{
LOG.warn("Cannot register the statistics");
}
else
{
ctx.register(new JCRStatisticsManager());
}
STARTED = true;
}
}
}
}
/**
* Add all the triggers that will keep the file up to date only if the persistence
* is enabled.
*/
private static void addTriggers()
{
if (!PERSISTENCE_ENABLED)
{
return;
}
Runtime.getRuntime().addShutdownHook(new Thread("JCRStatisticsManager-Hook")
{
@Override
public void run()
{
printData();
}
});
Thread t = new Thread("JCRStatisticsManager-Writer")
{
@Override
public void run()
{
while (true)
{
try
{
sleep(PERSISTENCE_TIMEOUT);
}
catch (InterruptedException e)
{
LOG.debug("InterruptedException", e);
}
printData();
}
}
};
t.setDaemon(true);
t.start();
}
/**
* Print the header of the csv file related to the given context.
*/
private static void printHeader(StatisticsContext context)
{
if (context.writer == null)
{
return;
}
boolean first = true;
if (context.global != null)
{
context.global.printHeader(context.writer);
first = false;
}
for (Statistics s : context.allStatistics.values())
{
if (first)
{
first = false;
}
else
{
context.writer.print(',');
}
s.printHeader(context.writer);
}
context.writer.println();
context.writer.flush();
}
/**
* Add one line of data to all the csv files.
*/
private static void printData()
{
Map<String, StatisticsContext> tmpContexts = CONTEXTS;
for (StatisticsContext context : tmpContexts.values())
{
printData(context);
}
}
/**
* Add one line of data to the csv file related to the given context.
*/
private static void printData(StatisticsContext context)
{
if (context.writer == null)
{
return;
}
boolean first = true;
if (context.global != null)
{
context.global.printData(context.writer);
first = false;
}
for (Statistics s : context.allStatistics.values())
{
if (first)
{
first = false;
}
else
{
context.writer.print(',');
}
s.printData(context.writer);
}
context.writer.println();
context.writer.flush();
}
/**
* Retrieve statistics context of the given category.
* @return the related {@link StatisticsContext}, <code>null</code> otherwise.
*/
private static StatisticsContext getContext(String category)
{
if (category == null)
{
return null;
}
return CONTEXTS.get(category);
}
/**
* Format the name of the statistics in the target format
* @param name the name of the statistics requested
* @return the formated statistics name
*/
static String formatName(String name)
{
return name == null ? null : name.replaceAll(" ", "").replaceAll("[,;]", ", ");
}
/**
* Retrieve statistics of the given category and name.
* @return the related {@link Statistics}, <code>null</code> otherwise.
*/
private static Statistics getStatistics(String category, String name)
{
StatisticsContext context = getContext(category);
if (context == null)
{
return null;
}
// Format the name
name = formatName(name);
if (name == null)
{
return null;
}
Statistics statistics;
if (GLOBAL_STATISTICS_NAME.equalsIgnoreCase(name))
{
statistics = context.global;
}
else
{
statistics = context.allStatistics.get(name);
}
return statistics;
}
/**
* @return the <code>min</code> value for the statistics corresponding to the given
* category and name.
*/
@Managed
@ManagedDescription("The minimum value of the time spent for one call.")
public static long getMin(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category,
@ManagedDescription("The name of the expected method or global for the global value")
@ManagedName("statisticsName") String name)
{
Statistics statistics = getStatistics(category, name);
return statistics == null ? 0l : statistics.getMin();
}
/**
* @return the <code>max</code> value for the statistics corresponding to the given
* category and name.
*/
@Managed
@ManagedDescription("The maximum value of the time spent for one call.")
public static long getMax(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category,
@ManagedDescription("The name of the expected method or global for the global value")
@ManagedName("statisticsName") String name)
{
Statistics statistics = getStatistics(category, name);
return statistics == null ? 0l : statistics.getMax();
}
/**
* @return the <code>total</code> value for the statistics corresponding to the given
* category and name.
*/
@Managed
@ManagedDescription("The total time spent for all the calls.")
public static long getTotal(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category,
@ManagedDescription("The name of the expected method or global for the global value")
@ManagedName("statisticsName") String name)
{
Statistics statistics = getStatistics(category, name);
return statistics == null ? 0l : statistics.getTotal();
}
/**
* @return the <code>times</code> value for the statistics corresponding to the given
* category and name.
*/
@Managed
@ManagedDescription("The total amount of calls.")
public static long getTimes(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category,
@ManagedDescription("The name of the expected method or global for the global value")
@ManagedName("statisticsName") String name)
{
Statistics statistics = getStatistics(category, name);
return statistics == null ? 0l : statistics.getTimes();
}
/**
* @return the <code>avg</code> value for the statistics corresponding to the given
* category and name.
*/
@Managed
@ManagedDescription("The average value of the time spent for one call.")
public static float getAvg(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category,
@ManagedDescription("The name of the expected method or global for the global value")
@ManagedName("statisticsName") String name)
{
Statistics statistics = getStatistics(category, name);
return statistics == null ? 0l : statistics.getAvg();
}
/**
* Allows to reset the statistics corresponding to the given category and name.
* @param category
* @param name
*/
@Managed
@ManagedDescription("Reset the statistics.")
public static void reset(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category,
@ManagedDescription("The name of the expected method or global for the global value")
@ManagedName("statisticsName") String name)
{
Statistics statistics = getStatistics(category, name);
if (statistics != null)
{
statistics.reset();
}
}
/**
* Allows to reset all the statistics corresponding to the given category.
* @param category
*/
@Managed
@ManagedDescription("Reset all the statistics.")
public static void resetAll(
@ManagedDescription("The name of the category of the statistics")
@ManagedName("categoryName") String category)
{
StatisticsContext context = getContext(category);
if (context != null)
{
context.reset();
}
}
/**
* Define the context of a given category of statistics
*/
private static class StatisticsContext
{
/**
* The printer used to print the statistics in csv format
*/
private final PrintWriter writer;
/**
* The list of all the statistics
*/
private final Map<String, Statistics> allStatistics;
/**
* The global statistics
*/
private final Statistics global;
/**
* The default constructor.
*/
public StatisticsContext(PrintWriter writer, Statistics global, Map<String, Statistics> allStatistics)
{
this.writer = writer;
this.global = global;
this.allStatistics = allStatistics;
}
/**
* Reset all the statistics related to the given context.
*/
public void reset()
{
if (global != null)
{
global.reset();
}
for (Statistics statistics : allStatistics.values())
{
statistics.reset();
}
}
}
}