diff --git a/examples/pom.xml b/examples/pom.xml index 92abe46b..e5a848f3 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,15 +1,10 @@ - 4.0.0 - - io.springside - springside-parent - 5.0.0-SNAPSHOT - ../modules/parent/ - + 4.0.0 io.springside.examples springside-examples + 5.0.0-SNAPSHOT Springside :: Examples pom diff --git a/modules/metrics/pom.xml b/modules/metrics/pom.xml new file mode 100755 index 00000000..7da0f0b4 --- /dev/null +++ b/modules/metrics/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + io.springside + springside-modules + 5.0.0-SNAPSHOT + ../ + + springside-metrics + jar + Springside :: Module :: Metrics + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + junit + junit + ${junit.version} + + + + org.assertj + assertj-core + ${assertj.version} + + + + org.mockito + mockito-core + ${mockito.version} + + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + + \ No newline at end of file diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/Exporter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/Exporter.java new file mode 100644 index 00000000..050162bc --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/Exporter.java @@ -0,0 +1,14 @@ +/******************************************************************************* + * Copyright (c) 2005, 2017 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +/** + * 不同于定时计算并汇报的Reporter, Exporter以JMX/RESTFul等方式,暴露所有提供Metrics的实时查询接口. + * + * 但Exporter不会触发Metric计算,只会读取由ReporterScheduler产生的快照值 + */ +public interface Exporter { +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/MetricRegistry.java b/modules/metrics/src/main/java/org/springside/modules/metrics/MetricRegistry.java new file mode 100755 index 00000000..bbe8e745 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/MetricRegistry.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (c) 2005, 2017 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.Timer; + +/** + * 注册中心, 用户创建Metrics的入口. + * 支持多线程并发的取得或创建metrics. + */ +public class MetricRegistry { + + public static final MetricRegistry INSTANCE = new MetricRegistry(); + + private Double[] defaultPcts = new Double[] {}; + + //从get的性能考虑,没有使用ConcurrentSkipListMap而是仍然使用ConcurrentHashMap. + private ConcurrentMap gauges = new ConcurrentHashMap(); + private ConcurrentMap counters = new ConcurrentHashMap(); + private ConcurrentMap histograms = new ConcurrentHashMap(); + private ConcurrentMap timers = new ConcurrentHashMap(); + + private List listeners = new ArrayList(); + + /** + * 格式化以"."分割的Metrics Name的辅助函数. + */ + public static String name(String name, String... subNames) { + final StringBuilder builder = new StringBuilder(name); + if (subNames != null) { + for (String s : subNames) { + if ((s != null) && !s.isEmpty()) { + builder.append('.').append(s); + } + } + } + return builder.toString(); + } + + /** + * 在注册中心注册Gauge. + */ + public void registerGauge(String name, Gauge gauge) { + gauges.put(name, gauge); + } + + /** + * 在注册中心获取或创建Counter. + */ + public Counter counter(String name) { + if (counters.containsKey(name)) { + return counters.get(name); + } else { + Counter counter = new Counter(); + return register(counters, name, counter); + } + } + + /** + * 在注册中心获取或创建Histogram, 并设置所需的百分比计算. + */ + public Histogram histogram(String name, Double... pcts) { + if (histograms.containsKey(name)) { + return histograms.get(name); + } else { + Histogram histogram = new Histogram(((pcts != null) && (pcts.length > 0)) ? pcts : defaultPcts); + return register(histograms, name, histogram); + } + } + + /** + * 在注册中心获取或创建Timer, 并设置所需的百分比计算. + */ + public Timer timer(String name, Double... pcts) { + if (timers.containsKey(name)) { + return timers.get(name); + } else { + Timer timer = new Timer(((pcts != null) && (pcts.length > 0) ? pcts : defaultPcts)); + return register(timers, name, timer); + } + } + + /** + * 快速清理注册表中全部Metrics. + */ + public void clearAll() { + for (MetricRegistryListener listener : listeners) { + for (String key : gauges.keySet()) { + listener.onGaugeRemoved(key); + } + + for (String key : counters.keySet()) { + listener.onCounterRemoved(key); + } + + for (String key : histograms.keySet()) { + listener.onHistogramRemoved(key); + } + + for (String key : timers.keySet()) { + listener.onTimerRemoved(key); + } + } + + gauges.clear(); + counters.clear(); + histograms.clear(); + timers.clear(); + } + + private T register(ConcurrentMap metrics, String name, T newMetric) { + T existingMetric = metrics.putIfAbsent(name, newMetric); + if (existingMetric != null) { + return existingMetric; + } else { + notifyNewMetric(name, newMetric); + return newMetric; + } + } + + /** + * 通知Listener有新Metrics加入 + */ + private void notifyNewMetric(String name, Object newMetric) { + for (MetricRegistryListener listener : listeners) { + if (newMetric instanceof Gauge) { + listener.onGaugeAdded(name, (Gauge) newMetric); + } + if (newMetric instanceof Counter) { + listener.onCounterAdded(name, (Counter) newMetric); + } + if (newMetric instanceof Histogram) { + listener.onHistogramAdded(name, (Histogram) newMetric); + } + if (newMetric instanceof Timer) { + listener.onTimerAdded(name, (Timer) newMetric); + } + } + } + + /** + * 返回所有Gauge, 无序. + */ + public Map getGauges() { + return gauges; + } + + /** + * 返回所有Counter, 无序. + */ + public Map getCounters() { + return counters; + } + + /** + * 返回所有Histogram, 无序. + */ + public Map getHistograms() { + return histograms; + } + + /** + * 返回所有Timer, 无序. + */ + public Map getTimers() { + return timers; + } + + /** + * 重新设置默认的百分比设置. + */ + public void setDefaultPcts(Double[] defaultPcts) { + this.defaultPcts = defaultPcts; + } + + /** + * Exporter需要将自己加为Listener. + */ + public void addListener(MetricRegistryListener listener) { + listeners.add(listener); + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/MetricRegistryListener.java b/modules/metrics/src/main/java/org/springside/modules/metrics/MetricRegistryListener.java new file mode 100755 index 00000000..c8ff6426 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/MetricRegistryListener.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2005, 2017 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.Timer; +/** + * Registry中Metrics有注册或注销变化时的通知接口 + */ +public interface MetricRegistryListener { + + void onGaugeAdded(String name, Gauge gauge); + + void onCounterAdded(String name, Counter counter); + + void onHistogramAdded(String name, Histogram histogram); + + void onTimerAdded(String name, Timer timer); + + void onGaugeRemoved(String name); + + void onCounterRemoved(String name); + + void onHistogramRemoved(String name); + + void onTimerRemoved(String name); +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/ReportScheduler.java b/modules/metrics/src/main/java/org/springside/modules/metrics/ReportScheduler.java new file mode 100755 index 00000000..0640c8e4 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/ReportScheduler.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2005, 2017 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.Timer; + +/** + * Reporter定时任务, 定期计算所有Metrics, 并调用所有Reporter进行汇报. + * + * 由用户负责reporter的初始化及定时管理任务的启停. + */ +public class ReportScheduler { + private static final String SCHEDULER_THREAD_NAME = "metrics-reporter"; + + private static Logger logger = LoggerFactory.getLogger(ReportScheduler.class); + + private MetricRegistry metricRegistry; + private List reporters; + + private ScheduledExecutorService executor; + private volatile boolean started; + + public ReportScheduler(MetricRegistry metricRegistry, Reporter... reporters) { + this(metricRegistry, Arrays.asList(reporters)); + } + + public ReportScheduler(MetricRegistry metricRegistry, List reporters) { + this.metricRegistry = metricRegistry; + this.reporters = reporters; + this.executor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory(SCHEDULER_THREAD_NAME)); + } + + public void addReporter(Reporter reporter) { + reporters.add(reporter); + } + + public void removeReporter(Reporter reporter) { + reporters.remove(reporter); + } + + /** + * 启动定时任务. + */ + public void start(long period, TimeUnit unit) { + if (started) { + throw new IllegalStateException("Scheduler had been started before"); + } + executor.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + report(); + } catch (Throwable e) { + logger.error(e.getMessage(), e); + } + } + }, period, period, unit); + started = true; + logger.info("metric reporters started."); + } + + /** + * 启动停止任务. + */ + public void stop() { + if (!started) { + throw new IllegalStateException("Scheduler hadn't been started before"); + } + executor.shutdownNow(); + try { + if (executor.awaitTermination(5, TimeUnit.SECONDS)) { + logger.info("metric reporters stopped."); + } else { + logger.info("metric reporters can't stop in 5 seconds, force stopped."); + } + started = false; + } catch (InterruptedException ignored) { + // do nothing + } + } + + /** + * 定期计算所有Metrics, 并调用所有Reporter进行汇报. + */ + public void report() { + + // 取出所有Metrics, 未按名称排序. + Map gaugeMap = metricRegistry.getGauges(); + Map counterMap = metricRegistry.getCounters(); + Map histogramMap = metricRegistry.getHistograms(); + Map timerMap = metricRegistry.getTimers(); + + // 调度每个Metrics的caculateMetrics()方法 + // 计算单位时间内的metrics值, 存入该Metrics的Snapshot中,并清零原始数据 + for (Gauge gauge : gaugeMap.values()) { + gauge.calculateMetric(); + } + + for (Counter counter : counterMap.values()) { + counter.calculateMetric(); + } + + for (Histogram histogram : histogramMap.values()) { + histogram.calculateMetric(); + } + + for (Timer timer : timerMap.values()) { + timer.calculateMetric(); + } + + // 调度所有Reporters 输出 metrics值 + for (Reporter reporter : reporters) { + reporter.report(gaugeMap, counterMap, histogramMap, timerMap); + } + } + + /** + * 给线程命名的线程工厂类,为了减少项目依赖,没有直接使用Guava里的实现。 + */ + private static class NamedThreadFactory implements ThreadFactory { + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + private NamedThreadFactory(String name) { + final SecurityManager s = System.getSecurityManager(); + this.group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + this.namePrefix = name; + } + + @Override + public Thread newThread(Runnable r) { + final Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + t.setDaemon(true); + return t; + } + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/Reporter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/Reporter.java new file mode 100755 index 00000000..7d4df425 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/Reporter.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2005, 2017 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import java.util.Map; + +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.Timer; + +/** + * Reporter的公共接口, 被ReportScheduler定时调用输出报告. + */ +public interface Reporter { + + void report(Map gauges, Map counters, Map histograms, + Map timers); +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/exporter/JmxExporter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/exporter/JmxExporter.java new file mode 100755 index 00000000..070cf66b --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/exporter/JmxExporter.java @@ -0,0 +1,362 @@ +package org.springside.modules.metrics.exporter; + +import java.lang.management.ManagementFactory; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.InstanceNotFoundException; +import javax.management.JMException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectInstance; +import javax.management.ObjectName; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springside.modules.metrics.Exporter; +import org.springside.modules.metrics.MetricRegistry; +import org.springside.modules.metrics.MetricRegistryListener; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.Timer; + +/** + * 以JMX形式,暴露所有Metrics. + * + * 为每一个Metrics注册一个MBean, 实现MetricRegistryListener接口感知Metrics的变化. + * + * TODO: 实现一个将所有Metrics暴露成一个总的JSON字符串的选项. + */ +public class JmxExporter implements Exporter, MetricRegistryListener { + + private static Logger logger = LoggerFactory.getLogger(JmxExporter.class); + private MBeanServer mBeanServer; + private MetricRegistry registry; + private String domain; + private final Map registered; + + public JmxExporter(String domain, MetricRegistry registry) { + this.mBeanServer = ManagementFactory.getPlatformMBeanServer(); + this.registry = registry; + this.domain = domain; + this.registered = new ConcurrentHashMap(); + + registry.addListener(this); + } + + public void initMBeans() { + + Map gauges = registry.getGauges(); + for (Entry entry : gauges.entrySet()) { + onGaugeAdded(entry.getKey(), entry.getValue()); + } + + Map counters = registry.getCounters(); + for (Entry entry : counters.entrySet()) { + onCounterAdded(entry.getKey(), entry.getValue()); + } + + Map histograms = registry.getHistograms(); + for (Entry entry : histograms.entrySet()) { + onHistogramAdded(entry.getKey(), entry.getValue()); + } + + Map timers = registry.getTimers(); + for (Entry entry : timers.entrySet()) { + onTimerAdded(entry.getKey(), entry.getValue()); + } + } + + public void destroyMBeans() { + Map gauges = registry.getGauges(); + for (String key : gauges.keySet()) { + onGaugeRemoved(key); + } + + Map counters = registry.getCounters(); + for (String key : counters.keySet()) { + onCounterRemoved(key); + } + + Map histograms = registry.getHistograms(); + for (String key : histograms.keySet()) { + onHistogramRemoved(key); + } + + Map timers = registry.getTimers(); + for (String key : timers.keySet()) { + onTimerRemoved(key); + } + } + + private void registerMBean(Object mBean, ObjectName objectName) { + try { + ObjectInstance objectInstance = mBeanServer.registerMBean(mBean, objectName); + if (objectInstance != null) { + // the websphere mbeanserver rewrites the objectname to include cell, node & server info + // make sure we capture the new objectName for unregistration + registered.put(objectName, objectInstance.getObjectName()); + } else { + registered.put(objectName, objectName); + } + } catch (InstanceAlreadyExistsException e) { + logger.debug("Unable to register:" + objectName, e); + } catch (JMException e) { + logger.warn("Unable to register:" + objectName, e); + } + } + + private void unregisterMBean(ObjectName originalObjectName) { + ObjectName storedObjectName = registered.remove(originalObjectName); + try { + if (storedObjectName != null) { + mBeanServer.unregisterMBean(storedObjectName); + + } else { + mBeanServer.unregisterMBean(originalObjectName); + } + } catch (InstanceNotFoundException e) { + logger.debug("Unable to unregister:" + originalObjectName, e); + } catch (MBeanRegistrationException e) { + logger.warn("Unable to unregister:" + originalObjectName, e); + } + } + + private ObjectName createName(String type, String name) { + try { + return new ObjectName(this.domain, "name", name); + } catch (MalformedObjectNameException e) { + try { + return new ObjectName(this.domain, "name", ObjectName.quote(name)); + } catch (MalformedObjectNameException e1) { + logger.warn("Unable to register {} {}", type, name, e1); + throw new RuntimeException(e1); + } + } + } + + @Override + public void onGaugeAdded(String name, Gauge gauge) { + final ObjectName objectName = createName("gauges", name); + registerMBean(new JmxGauge(gauge, objectName), objectName); + } + + @Override + public void onCounterAdded(String name, Counter counter) { + final ObjectName objectName = createName("counters", name); + registerMBean(new JmxCounter(counter, objectName), objectName); + } + + @Override + public void onHistogramAdded(String name, Histogram histogram) { + final ObjectName objectName = createName("histograms", name); + registerMBean(new JmxHistogram(histogram, objectName), objectName); + } + + @Override + public void onTimerAdded(String name, Timer timer) { + final ObjectName objectName = createName("timers", name); + registerMBean(new JmxTimer(timer, objectName), objectName); + } + + @Override + public void onGaugeRemoved(String name) { + final ObjectName objectName = createName("guages", name); + unregisterMBean(objectName); + } + + @Override + public void onCounterRemoved(String name) { + final ObjectName objectName = createName("counters", name); + unregisterMBean(objectName); + } + + @Override + public void onHistogramRemoved(String name) { + final ObjectName objectName = createName("histograms", name); + unregisterMBean(objectName); + } + + @Override + public void onTimerRemoved(String name) { + final ObjectName objectName = createName("timers", name); + unregisterMBean(objectName); + } + + public interface MetricMBean { + ObjectName objectName(); + } + + public interface JmxGaugeMBean extends MetricMBean { + Number getValue(); + } + + public interface JmxCounterMBean extends MetricMBean { + long getLatestCount(); + + long getLatestRate(); + + long getTotalCount(); + + long getMeanRate(); + } + + public interface JmxHistogramMBean extends MetricMBean { + + long getMin(); + + long getMax(); + + double getMean(); + + // TODO: add pcts + } + + public interface JmxTimerMBean extends MetricMBean { + long getLatestCount(); + + long getLatestRate(); + + long getTotalCount(); + + long getMeanRate(); + + long getMinLatency(); + + long getMaxLatency(); + + double getMeanLatency(); + + // TODO: add pcts + } + + private abstract static class AbstractBean implements MetricMBean { + private final ObjectName objectName; + + AbstractBean(ObjectName objectName) { + this.objectName = objectName; + } + + @Override + public ObjectName objectName() { + return objectName; + } + } + + private static class JmxGauge extends AbstractBean implements JmxGaugeMBean { + + private final Gauge metric; + + public JmxGauge(Gauge gauge, ObjectName objectName) { + super(objectName); + this.metric = gauge; + } + + @Override + public Number getValue() { + return metric.latestMetric; + } + } + + private static class JmxCounter extends AbstractBean implements JmxCounterMBean { + private final Counter metric; + + private JmxCounter(Counter metric, ObjectName objectName) { + super(objectName); + this.metric = metric; + } + + @Override + public long getLatestCount() { + return metric.latestMetric.latestCount; + } + + @Override + public long getLatestRate() { + return metric.latestMetric.latestRate; + } + + @Override + public long getTotalCount() { + return metric.latestMetric.totalCount; + } + + @Override + public long getMeanRate() { + return metric.latestMetric.meanRate; + } + + } + + private static class JmxHistogram extends AbstractBean implements JmxHistogramMBean { + private final Histogram metric; + + private JmxHistogram(Histogram metric, ObjectName objectName) { + super(objectName); + this.metric = metric; + } + + @Override + public long getMin() { + return metric.latestMetric.min; + } + + @Override + public long getMax() { + return metric.latestMetric.max; + } + + @Override + public double getMean() { + return metric.latestMetric.mean; + } + } + + private static class JmxTimer extends AbstractBean implements JmxTimerMBean { + private final Timer metric; + + private JmxTimer(Timer metric, ObjectName objectName) { + super(objectName); + this.metric = metric; + } + + @Override + public long getLatestCount() { + return metric.latestMetric.counterMetric.latestCount; + } + + @Override + public long getLatestRate() { + return metric.latestMetric.counterMetric.latestRate; + } + + @Override + public long getTotalCount() { + return metric.latestMetric.counterMetric.totalCount; + } + + @Override + public long getMeanRate() { + return metric.latestMetric.counterMetric.meanRate; + } + + @Override + public long getMinLatency() { + return metric.latestMetric.histogramMetric.min; + } + + @Override + public long getMaxLatency() { + return metric.latestMetric.histogramMetric.max; + } + + @Override + public double getMeanLatency() { + return metric.latestMetric.histogramMetric.mean; + } + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Counter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Counter.java new file mode 100755 index 00000000..20bcae79 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Counter.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.metric; + +import java.util.concurrent.atomic.AtomicLong; + +import org.springside.modules.metrics.utils.Clock; + +/** + * Counter类型, 主要用于计算QPS. + * + * 报告Report间隔时间内的Counter变化值及平均QPS,以及服务从启动到现在的Counter总值和平均QPS. + */ +public class Counter { + public static Clock clock = Clock.DEFAULT; + + public CounterMetric latestMetric; // Snapshot值 + + private AtomicLong counter; // 统计周期内的计数器 + + private long startTime; // 启动时间 + private long lastReportTime; // 上一次报告的时间 + private long totalCount; // 从启动到上一次报告周期的总调用次数 + + public Counter() { + reset(); + } + + public void inc() { + counter.incrementAndGet(); + } + + public void inc(long n) { + counter.addAndGet(n); + } + + public void dec() { + counter.decrementAndGet(); + } + + public void dec(long n) { + counter.addAndGet(-n); + } + + /** + * 计算单位时间内的metrics值, 存入该Metrics的Snapshot中,并清零原始数据. + */ + public CounterMetric calculateMetric() { + long latestCount = counter.getAndSet(0); + long currentTime = clock.getCurrentTime(); + + CounterMetric metric = new CounterMetric(); + + totalCount += latestCount; + long totalElapsed = currentTime - startTime; + metric.meanRate = (totalCount * 1000) / totalElapsed; + + metric.latestCount = latestCount; + metric.totalCount = totalCount; + + long elapsed = currentTime - lastReportTime; + if (elapsed > 0) { + metric.latestRate = (latestCount * 1000) / elapsed; + } + + lastReportTime = currentTime; + + latestMetric = metric; + + return metric; + } + + public void reset() { + latestMetric = new CounterMetric(); + counter = new AtomicLong(0); + totalCount = 0L; + startTime = clock.getCurrentTime(); + lastReportTime = startTime; + } + + @Override + public String toString() { + return "Counter [latestMetric=" + latestMetric + ", counter=" + counter + ", totalCount=" + totalCount + + ", startTime=" + startTime + ", lastReportTime=" + lastReportTime + "]"; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/CounterMetric.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/CounterMetric.java new file mode 100755 index 00000000..0703f559 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/CounterMetric.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.metric; + +/** + * Counter的统计值 + */ +public class CounterMetric { + public long totalCount; //从启动到目前到调用次数 + public long meanRate; //从启动到目前到平均QPS + public long latestCount;//最后一个报告周期的调用次数 + public long latestRate; //最后一个报告周期的QPS + + @Override + public String toString() { + return "CounterMetric [totalCount=" + totalCount + ", meanRate=" + meanRate + ", latestCount=" + latestCount + + ", latestRate=" + latestRate + "]"; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Gauge.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Gauge.java new file mode 100755 index 00000000..d15d81ab --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Gauge.java @@ -0,0 +1,26 @@ +/*------------------------------------------------------------------------------ + * COPYRIGHT Ericsson 2014 + * + * The copyright to the computer program(s) herein is the property of + * Ericsson Inc. The programs may be used and/or copied only with written + * permission from Ericsson Inc. or in accordance with the terms and + * conditions stipulated in the agreement/contract under which the + * program(s) have been supplied. + *----------------------------------------------------------------------------*/ +package org.springside.modules.metrics.metric; + +public abstract class Gauge { + + public Number latestMetric; + + public void calculateMetric() { + latestMetric = getValue(); + } + + protected abstract Number getValue(); + + @Override + public String toString() { + return "Gauge [latestMetric=" + latestMetric + "]"; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Histogram.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Histogram.java new file mode 100755 index 00000000..1226cc46 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Histogram.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.metric; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * Histogram数据类型, 主要用于计算Latency. + * + * 报告Report间隔时间内数值的最小/最大,平均值,以及某个百分比的请求数都小于的值. + */ +public class Histogram { + + public HistogramMetric latestMetric;// Snapshot值 + + private Double[] pcts; //配置需要计算的百分位 + + private LinkedList measurements; //统计周期内的原始数据 + + public Histogram(Double... pcts) { + this.pcts = pcts; + measurements = new LinkedList(); + latestMetric = createEmptyMetric(); + } + + public void update(long value) { + synchronized (measurements) { + measurements.add(value); + } + } + + /** + * 计算单位时间内的metrics值, 存入该Metrics的Snapshot中,并清零原始数据. + */ + public HistogramMetric calculateMetric() { + // 快照当前的数据,在计算时不阻塞新的metrics update. + List snapshotList = measurements; + measurements = new LinkedList(); + + if (snapshotList.isEmpty()) { + return createEmptyMetric(); + } + + HistogramMetric metric = new HistogramMetric(); + int count = snapshotList.size(); + double sum = 0; + + if ((pcts != null) && (pcts.length > 0)) { + // 按数值大小排序,以快速支持百分比过滤 + Collections.sort(snapshotList); + + metric.min = snapshotList.get(0); + metric.max = snapshotList.get(count - 1); + + for (long value : snapshotList) { + sum += value; + } + + for (Double pct : pcts) { + metric.pcts.put(pct, getPercent(snapshotList, count, pct)); + } + } else { + // 不排序的算法,因为不需要支持百分比过滤 + metric.min = snapshotList.get(0); + metric.max = snapshotList.get(0); + + for (long value : snapshotList) { + if (value < metric.min) { + metric.min = value; + } + if (value > metric.max) { + metric.max = value; + } + sum += value; + } + } + + metric.mean = sum / count; + + latestMetric = metric; + return metric; + } + + private HistogramMetric createEmptyMetric() { + HistogramMetric metric = new HistogramMetric(); + for (Double pct : pcts) { + metric.pcts.put(pct, 0L); + } + return metric; + } + + private Long getPercent(List snapshotList, int count, double pct) { + + final double pos = (pct * (count + 1)) / 100; + + if (pos < 1) { + return snapshotList.get(0); + } + + if (pos >= count) { + return snapshotList.get(count - 1); + } + + return snapshotList.get((int) pos - 1); + } + + @Override + public String toString() { + return "Histogram [latestMetric=" + latestMetric + ", measurements=" + measurements + ", pcts=" + + Arrays.toString(pcts) + "]"; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/HistogramMetric.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/HistogramMetric.java new file mode 100755 index 00000000..bc3eddb9 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/HistogramMetric.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.metric; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Histogram的统计值 + */ +public class HistogramMetric { + public long min; //最小 + public long max; //最大 + public double mean; //平均 + public Map pcts = new TreeMap(); //百分位数 + + @Override + public String toString() { + return "HistogramMetric [min=" + min + ", max=" + max + ", mean=" + mean + ", pcts=" + pcts + "]"; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Timer.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Timer.java new file mode 100755 index 00000000..51455db8 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/Timer.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.metric; + +import org.springside.modules.metrics.utils.Clock; + +/** + * Timer类型,兼具Counter和Histogram的简便写法. + * + * 有两种用法: + * 1. 使用timer + * + *
+ * TimerContext timerContext = timer.start();
+ * ...
+ * timerContext.stop();
+ * 
+ * + * 2. 自行计算,更吝啬一点 + * + *
+ * long start = System.currentTimeMillis();
+ * ....
+ * timer.update(start);
+ * 
+ */ +public class Timer { + + public static Clock clock = Clock.DEFAULT; + + public TimerMetric latestMetric; //snapshot值 + + private Counter counter; + private Histogram histogram; + + public Timer(Double... pcts) { + counter = new Counter(); + histogram = new Histogram(pcts); + latestMetric = new TimerMetric(); + } + + public void update(long start) { + histogram.update(clock.getCurrentTime() - start); + counter.inc(); + } + + public TimerContext start() { + return new TimerContext(this, clock.getCurrentTime()); + } + + private void stopTimer(long startTime) { + long elapsed = clock.getCurrentTime() - startTime; + histogram.update(elapsed); + counter.inc(); + } + + /** + * 计算单位时间内的metrics值, 存入该Metrics的Snapshot中,并清零原始数据. + */ + public TimerMetric calculateMetric() { + TimerMetric metric = new TimerMetric(); + metric.counterMetric = counter.calculateMetric(); + metric.histogramMetric = histogram.calculateMetric(); + latestMetric = metric; + return metric; + } + + @Override + public String toString() { + return "Timer [latestMetric=" + latestMetric + ", counter=" + counter + ", histogram=" + histogram + "]"; + } + + /** + * 保存某一次请求的初始时间与Timer实例. + */ + public static class TimerContext { + private final Timer timer; + private final long startTime; + + private TimerContext(Timer timer, long startTime) { + this.timer = timer; + this.startTime = startTime; + } + + public void stop() { + timer.stopTimer(startTime); + } + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/metric/TimerMetric.java b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/TimerMetric.java new file mode 100755 index 00000000..668cc533 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/metric/TimerMetric.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.metric; + +/** + * Timer的统计值 + */ +public class TimerMetric { + public CounterMetric counterMetric; + public HistogramMetric histogramMetric; + + @Override + public String toString() { + return "TimerMetric [counterMetric=" + counterMetric + ", histogramMetric=" + histogramMetric + "]"; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/ConsoleReporter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/ConsoleReporter.java new file mode 100755 index 00000000..60763997 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/ConsoleReporter.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.reporter; + +import java.io.PrintStream; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.springside.modules.metrics.Reporter; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.CounterMetric; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.HistogramMetric; +import org.springside.modules.metrics.metric.Timer; +import org.springside.modules.metrics.metric.TimerMetric; + +/** + * 将Metrics打印到Console. + */ +public class ConsoleReporter implements Reporter { + + private static final int CONSOLE_WIDTH = 80; + private PrintStream output = System.out; + + @Override + public void report(Map gauges, Map counters, Map histograms, + Map timers) { + + printWithBanner(new Date().toString(), '='); + output.println(); + + if (!gauges.isEmpty()) { + printWithBanner("-- Gaugues", '-'); + for (Map.Entry entry : getSortedMetrics(gauges).entrySet()) { + output.println(entry.getKey()); + printGauge(entry.getValue()); + } + output.println(); + } + + if (!counters.isEmpty()) { + printWithBanner("-- Counters", '-'); + for (Map.Entry entry : getSortedMetrics(counters).entrySet()) { + output.println(entry.getKey()); + printCounter(entry.getValue().latestMetric); + } + output.println(); + } + + if (!histograms.isEmpty()) { + printWithBanner("-- Histograms", '-'); + for (Map.Entry entry : getSortedMetrics(histograms).entrySet()) { + output.println(entry.getKey()); + printHistogram(entry.getValue().latestMetric); + } + output.println(); + } + + if (!timers.isEmpty()) { + printWithBanner("-- Timers", '-'); + for (Map.Entry entry : getSortedMetrics(timers).entrySet()) { + output.println(entry.getKey()); + printTimer(entry.getValue().latestMetric); + } + output.println(); + } + } + + private void printWithBanner(String s, char c) { + output.print(s); + output.print(' '); + for (int i = 0; i < (CONSOLE_WIDTH - s.length() - 1); i++) { + output.print(c); + } + output.println(); + } + + private void printGauge(Gauge gauge) { + output.printf(" value = %s%n", gauge.latestMetric); + } + + private void printCounter(CounterMetric counter) { + output.printf(" latest count = %d%n", counter.latestCount); + output.printf(" total count = %d%n", counter.totalCount); + output.printf(" latest rate = %d%n", counter.latestRate); + output.printf(" mean rate = %d%n", counter.meanRate); + } + + private void printHistogram(HistogramMetric histogram) { + output.printf(" min = %d%n", histogram.min); + output.printf(" max = %d%n", histogram.max); + output.printf(" mean = %2.2f%n", histogram.mean); + for (Entry pct : histogram.pcts.entrySet()) { + output.printf(" %2.2f%% <= %d %n", pct.getKey(), pct.getValue()); + } + } + + private void printTimer(TimerMetric timer) { + output.printf(" latest count = %d%n", timer.counterMetric.latestCount); + output.printf(" total count = %d%n", timer.counterMetric.totalCount); + output.printf(" latest rate = %d%n", timer.counterMetric.latestRate); + output.printf(" mean rate = %d%n", timer.counterMetric.meanRate); + output.printf(" min latency = %d ms%n", timer.histogramMetric.min); + output.printf(" max latency = %d ms%n", timer.histogramMetric.max); + output.printf(" mean latency = %2.2f ms%n", timer.histogramMetric.mean); + for (Entry pct : timer.histogramMetric.pcts.entrySet()) { + output.printf(" %2.2f%% latency <= %d ms%n", pct.getKey(), pct.getValue()); + } + } + + /** + * 返回按metrics name排序的Map. + * + * 从get的性能考虑,没有使用ConcurrentSkipListMap而是仍然使用ConcurrentHashMap,因此每次报告时需要用TreeMap重新排序. + */ + private SortedMap getSortedMetrics(Map metrics) { + return new TreeMap(metrics); + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/GraphiteReporter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/GraphiteReporter.java new file mode 100755 index 00000000..9a339bee --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/GraphiteReporter.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.reporter; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +import javax.net.SocketFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springside.modules.metrics.MetricRegistry; +import org.springside.modules.metrics.Reporter; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.CounterMetric; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.HistogramMetric; +import org.springside.modules.metrics.metric.Timer; +import org.springside.modules.metrics.metric.TimerMetric; + +/** + * 输出到Graphite. + */ +public class GraphiteReporter implements Reporter { + + public static final String DEFAULT_PREFIX = "metrics"; + + private static final Pattern WHITESPACE = Pattern.compile("[\\s]+"); + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static Logger logger = LoggerFactory.getLogger(GraphiteReporter.class); + + private String prefix; + + private InetSocketAddress address; + private SocketFactory socketFactory; + private Socket socket; + private Writer writer; + + // use to only print connection error message once. + private GraphiteConnStatus graphiteConnStatus = GraphiteConnStatus.CONN_OK; + + public GraphiteReporter(InetSocketAddress address) { + this(address, DEFAULT_PREFIX); + } + + public GraphiteReporter(InetSocketAddress address, String prefix) { + this.prefix = prefix; + this.address = address; + this.socketFactory = SocketFactory.getDefault(); + } + + @Override + public void report(Map gauges, Map counters, Map histograms, + Map timers) { + try { + connect(); + long timestamp = System.currentTimeMillis() / 1000; + + for (Map.Entry entry : gauges.entrySet()) { + reportGauge(entry.getKey(), entry.getValue().latestMetric, timestamp); + } + + for (Map.Entry entry : counters.entrySet()) { + reportCounter(entry.getKey(), entry.getValue().latestMetric, timestamp); + } + + for (Map.Entry entry : histograms.entrySet()) { + reportHistogram(entry.getKey(), entry.getValue().latestMetric, timestamp); + } + + for (Map.Entry entry : timers.entrySet()) { + reportTimer(entry.getKey(), entry.getValue().latestMetric, timestamp); + } + + flush(); + + onConnSuccess(); + } catch (IOException e) { + onConnFail(e); + } finally { + try { + close(); + } catch (IOException e) { + logger.warn("Error disconnecting from Graphite", e); + } + } + } + + private void reportGauge(String name, Number gauge, long timestamp) throws IOException { + send(MetricRegistry.name(prefix, name, "gauge"), format(gauge), timestamp); + } + + private void reportCounter(String name, CounterMetric counter, long timestamp) throws IOException { + send(MetricRegistry.name(prefix, name, "count"), format(counter.latestCount), timestamp); + } + + private void reportHistogram(String name, HistogramMetric histogram, long timestamp) throws IOException { + send(MetricRegistry.name(prefix, name, "min"), format(histogram.min), timestamp); + send(MetricRegistry.name(prefix, name, "max"), format(histogram.max), timestamp); + send(MetricRegistry.name(prefix, name, "mean"), format(histogram.mean), timestamp); + for (Entry pct : histogram.pcts.entrySet()) { + send(MetricRegistry.name(prefix, name, format(pct.getKey()).replace('.', '_')), format(pct.getValue()), + timestamp); + } + } + + private void reportTimer(String name, TimerMetric timer, long timestamp) throws IOException { + send(MetricRegistry.name(prefix, name, "count"), format(timer.counterMetric.latestCount), timestamp); + + send(MetricRegistry.name(prefix, name, "min"), format(timer.histogramMetric.min), timestamp); + send(MetricRegistry.name(prefix, name, "max"), format(timer.histogramMetric.max), timestamp); + send(MetricRegistry.name(prefix, name, "mean"), format(timer.histogramMetric.mean), timestamp); + for (Entry pct : timer.histogramMetric.pcts.entrySet()) { + send(MetricRegistry.name(prefix, name, format(pct.getKey()).replace('.', '_')), format(pct.getValue()), + timestamp); + } + } + + private void connect() throws IllegalStateException, IOException { + if (socket != null) { + throw new IllegalStateException("Already connected"); + } + + this.socket = socketFactory.createSocket(address.getAddress(), address.getPort()); + this.writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), UTF_8)); + } + + private void send(String name, String value, long timestamp) throws IOException { + try { + writer.write(sanitize(name)); + writer.write(' '); + writer.write(sanitize(value)); + writer.write(' '); + writer.write(format(timestamp)); + writer.write('\n'); + } catch (IOException e) { + throw e; + } + } + + private void flush() throws IOException { + writer.flush(); + } + + private void close() throws IOException { + if (writer != null) { + writer.flush(); + } + + if (socket != null) { + socket.close(); + } + this.socket = null; + this.writer = null; + } + + private String format(long n) { + return Long.toString(n); + } + + private String format(double v) { + return String.format(Locale.US, "%2.2f", v); + } + + private String format(Number o) { + if (o instanceof Float) { + return format(((Float) o).doubleValue()); + } else if (o instanceof Double) { + return format(((Double) o).doubleValue()); + } else if (o instanceof Short) { + return format(((Short) o).longValue()); + } else if (o instanceof Integer) { + return format(((Integer) o).longValue()); + } else if (o instanceof Long) { + return format(((Long) o).longValue()); + } + return null; + } + + private String sanitize(String s) { + return WHITESPACE.matcher(s).replaceAll("-"); + } + + private void onConnFail(Exception exception) { + if (graphiteConnStatus != GraphiteConnStatus.CONN_NOK) { + logger.warn("Unable to report to Graphite", exception); + graphiteConnStatus = GraphiteConnStatus.CONN_NOK; + } + } + + private void onConnSuccess() { + if (graphiteConnStatus != GraphiteConnStatus.CONN_OK) { + logger.info("Graphite connection is recovered."); + graphiteConnStatus = GraphiteConnStatus.CONN_OK; + } + } + + private enum GraphiteConnStatus { + CONN_OK, CONN_NOK + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/Slf4jReporter.java b/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/Slf4jReporter.java new file mode 100755 index 00000000..0c3ccb03 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/reporter/Slf4jReporter.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.reporter; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springside.modules.metrics.Reporter; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.CounterMetric; +import org.springside.modules.metrics.metric.Gauge; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.HistogramMetric; +import org.springside.modules.metrics.metric.Timer; +import org.springside.modules.metrics.metric.TimerMetric; + +/** + * 使用Slf4j以INFO级别将Metrics打印到日志. + * + * 默认logger name是"metrics",可在构造函数中设定.用户需要在日志的配置文件中对该logger进行正确配置. + * + * TODO: + * 1.输出为JSON的选项 + * 2.日志字符串的性能调优 + */ +public class Slf4jReporter implements Reporter { + private static final String DEFAULT_LOGGER_NAME = "metrics"; + private Logger reportLogger; + + public Slf4jReporter() { + this(DEFAULT_LOGGER_NAME); + } + + public Slf4jReporter(String loggerName) { + reportLogger = LoggerFactory.getLogger(loggerName); + } + + @Override + public void report(Map gauges, Map counters, Map histograms, + Map timers) { + + for (Entry entry : getSortedMetrics(gauges).entrySet()) { + logGauge(entry.getKey(), entry.getValue().latestMetric); + } + + for (Entry entry : getSortedMetrics(counters).entrySet()) { + logCounter(entry.getKey(), entry.getValue().latestMetric); + } + + for (Entry entry : getSortedMetrics(histograms).entrySet()) { + logHistogram(entry.getKey(), entry.getValue().latestMetric); + } + + for (Entry entry : getSortedMetrics(timers).entrySet()) { + logTimer(entry.getKey(), entry.getValue().latestMetric); + } + } + + private void logGauge(String name, Number gauge) { + reportLogger.info("type=GAUGE, name={}, value={}", name, gauge); + } + + private void logCounter(String name, CounterMetric counter) { + reportLogger.info("type=COUNTER, name={}, totalCount={}, meanRate={}, latestRate={}", name, counter.totalCount, + counter.meanRate, counter.latestRate); + } + + private void logHistogram(String name, HistogramMetric histogram) { + reportLogger.info("type=HISTOGRAM, name={}, min={}, max={}, mean={}{}", name, histogram.min, histogram.max, + histogram.mean, buildPcts(histogram.pcts)); + } + + private void logTimer(String name, TimerMetric timer) { + reportLogger + .info("type=TIMER, name={}, totalCount={}, meanRate={}, latestRate={}, minLatency={}ms, maxLatency={}ms, meanLatency={}ms{}", + name, timer.counterMetric.totalCount, timer.counterMetric.meanRate, + timer.counterMetric.latestRate, timer.histogramMetric.min, timer.histogramMetric.max, + timer.histogramMetric.mean, buildPcts(timer.histogramMetric.pcts)); + } + + private String buildPcts(Map pcts) { + StringBuilder builder = new StringBuilder(); + + for (Entry entry : pcts.entrySet()) { + builder.append(", ").append(entry.getKey()).append("%<=").append(entry.getValue()).append("ms"); + } + + return builder.toString(); + } + + /** + * 返回按metrics name排序的Map. + * + */ + private SortedMap getSortedMetrics(Map metrics) { + return new TreeMap(metrics); + } +} diff --git a/modules/metrics/src/main/java/org/springside/modules/metrics/utils/Clock.java b/modules/metrics/src/main/java/org/springside/modules/metrics/utils/Clock.java new file mode 100755 index 00000000..1ce98a01 --- /dev/null +++ b/modules/metrics/src/main/java/org/springside/modules/metrics/utils/Clock.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics.utils; + +/** + * 日期提供者,使用它而不是直接取得系统时间,方便测试。 + * + * @author calvin + */ +public interface Clock { + + static final Clock DEFAULT = new DefaultClock(); + + long getCurrentTime(); + + /** + * 默认时间提供者,返回当前的时间,线程安全。 + */ + public static class DefaultClock implements Clock { + + @Override + public long getCurrentTime() { + return System.currentTimeMillis(); + } + } + + /** + * 可配置的时间提供者,用于测试. + */ + public static class MockClock implements Clock { + + private long time; + + public MockClock() { + this(0); + } + + public MockClock(long time) { + this.time = time; + } + + @Override + public long getCurrentTime() { + return time; + } + + /** + * 重新设置时间。 + */ + public void update(long newTime) { + this.time = newTime; + } + + /** + * 增加时间戳. + */ + public void increaseTime(int millis) { + time += millis; + } + + /** + * 滚动时间. + */ + public void decreaseTime(int millis) { + time -= millis; + } + } +} diff --git a/modules/metrics/src/test/java/org/springside/modules/metrics/CounterTest.java b/modules/metrics/src/test/java/org/springside/modules/metrics/CounterTest.java new file mode 100755 index 00000000..8dfdfc12 --- /dev/null +++ b/modules/metrics/src/test/java/org/springside/modules/metrics/CounterTest.java @@ -0,0 +1,74 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Before; +import org.junit.Test; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.CounterMetric; +import org.springside.modules.metrics.utils.Clock.MockClock; + +public class CounterTest { + + private MockClock clock = new MockClock(); + + @Before + public void setup() { + Counter.clock = clock; + } + + @Test + public void normal() { + + Counter counter = new Counter(); + counter.inc(10); + counter.inc(20); + counter.inc(30); + clock.increaseTime(1000); + + CounterMetric metric = counter.calculateMetric(); + assertThat(metric.totalCount).isEqualTo(60); + assertThat(metric.meanRate).isEqualTo(60); + assertThat(metric.latestCount).isEqualTo(60); + assertThat(metric.latestRate).isEqualTo(60); + + counter.inc(20); + clock.increaseTime(1000); + metric = counter.calculateMetric(); + + assertThat(metric.totalCount).isEqualTo(80); + assertThat(metric.meanRate).isEqualTo(40); + assertThat(metric.latestCount).isEqualTo(20); + assertThat(metric.latestRate).isEqualTo(20); + } + + @Test + public void incAndDec() { + Counter counter = new Counter(); + + counter.inc(20); + counter.inc(); + counter.inc(); + counter.dec(10); + counter.dec(); + clock.increaseTime(1000); + + CounterMetric metric = counter.calculateMetric(); + assertThat(metric.totalCount).isEqualTo(11); + } + + @Test + public void empty() { + Counter counter = new Counter(); + clock.increaseTime(1000); + + CounterMetric metric = counter.calculateMetric(); + assertThat(metric.totalCount).isEqualTo(0); + assertThat(metric.latestRate).isEqualTo(0); + } +} diff --git a/modules/metrics/src/test/java/org/springside/modules/metrics/HistogramTest.java b/modules/metrics/src/test/java/org/springside/modules/metrics/HistogramTest.java new file mode 100755 index 00000000..6d754e9e --- /dev/null +++ b/modules/metrics/src/test/java/org/springside/modules/metrics/HistogramTest.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.HistogramMetric; + +public class HistogramTest { + + @Test + public void normal() { + Histogram histogram = new Histogram(90d, 95d); + + for (int i = 1; i <= 100; i++) { + histogram.update(i); + } + + HistogramMetric metric = histogram.calculateMetric(); + + assertThat(metric.min).isEqualTo(1); + assertThat(metric.max).isEqualTo(100); + assertThat(metric.mean).isEqualTo(50.5); + assertThat(metric.pcts.get(90d)).isEqualTo(90); + assertThat(metric.pcts.get(95d)).isEqualTo(95); + + for (int i = 1; i <= 100; i++) { + histogram.update(i * 2); + } + + metric = histogram.calculateMetric(); + + assertThat(metric.min).isEqualTo(2); + assertThat(metric.max).isEqualTo(200); + assertThat(metric.mean).isEqualTo(101); + assertThat(metric.pcts.get(90d)).isEqualTo(180); + assertThat(metric.pcts.get(95d)).isEqualTo(190); + } + + @Test + public void fewData() { + Histogram histogram = new Histogram(90d, 95d); + + histogram.update(1); + HistogramMetric metric = histogram.calculateMetric(); + assertThat(metric.pcts.get(90d)).isEqualTo(1); + assertThat(metric.pcts.get(95d)).isEqualTo(1); + + for (int i = 1; i <= 3; i++) { + histogram.update(i); + } + metric = histogram.calculateMetric(); + + assertThat(metric.min).isEqualTo(1); + assertThat(metric.max).isEqualTo(3); + assertThat(metric.mean).isEqualTo(2); + assertThat(metric.pcts.get(90d)).isEqualTo(3); + assertThat(metric.pcts.get(95d)).isEqualTo(3); + } + + @Test + public void emptyMesures() { + Histogram histogram = new Histogram(90d, 95d); + + HistogramMetric metric = histogram.calculateMetric(); + + assertThat(metric.min).isZero(); + assertThat(metric.max).isZero(); + assertThat(metric.mean).isZero(); + assertThat(metric.pcts.get(90d)).isZero(); + } + + @Test() + public void emptyPcts() { + Histogram histogram = new Histogram(); + for (int i = 1; i <= 3; i++) { + histogram.update(i); + } + + HistogramMetric metric = histogram.calculateMetric(); + assertThat(metric.max).isEqualTo(3); + assertThat(metric.pcts).isEmpty(); + assertThat(metric.pcts.get(90d)).isNull(); + } +} diff --git a/modules/metrics/src/test/java/org/springside/modules/metrics/MetricRegistryTest.java b/modules/metrics/src/test/java/org/springside/modules/metrics/MetricRegistryTest.java new file mode 100755 index 00000000..ea013a88 --- /dev/null +++ b/modules/metrics/src/test/java/org/springside/modules/metrics/MetricRegistryTest.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Map; + +import org.junit.Test; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.HistogramMetric; +import org.springside.modules.metrics.metric.Timer; + +public class MetricRegistryTest { + + @Test + public void counter() { + MetricRegistry metricRegistry = new MetricRegistry(); + Counter counter = metricRegistry.counter(MetricRegistry.name("UserService", "getUser.counter")); + assertThat(counter).isNotNull(); + + Map counters = metricRegistry.getCounters(); + + Counter counter2 = counters.get("UserService.getUser.counter"); + assertThat(counter2).isNotNull().isSameAs(counter); + + Counter counter3 = metricRegistry.counter(MetricRegistry.name("UserService", "getUser.counter")); + assertThat(counter3).isNotNull().isSameAs(counter); + } + + @Test + public void histogram() { + MetricRegistry metricRegistry = new MetricRegistry(); + Histogram histogram = metricRegistry.histogram(MetricRegistry.name("UserService", "getUser.latency")); + assertThat(histogram).isNotNull(); + + Map histograms = metricRegistry.getHistograms(); + + Histogram histogram2 = histograms.get("UserService.getUser.latency"); + assertThat(histogram2).isNotNull().isSameAs(histogram); + + Histogram histogram3 = metricRegistry.histogram(MetricRegistry.name("UserService", "getUser.latency")); + assertThat(histogram3).isNotNull().isSameAs(histogram); + + } + + @Test + public void execution() { + MetricRegistry metricRegistry = new MetricRegistry(); + + Timer execution = metricRegistry.timer(MetricRegistry.name("UserService", "getUser.execution")); + assertThat(execution).isNotNull(); + + Map executions = metricRegistry.getTimers(); + + Timer execution2 = executions.get("UserService.getUser.execution"); + assertThat(execution2).isNotNull().isSameAs(execution); + + Timer execution3 = metricRegistry.timer(MetricRegistry.name("UserService", "getUser.execution")); + assertThat(execution3).isNotNull().isSameAs(execution); + } + + @Test + public void defaultPcts() { + MetricRegistry metricRegistry = new MetricRegistry(); + + // set pcts 60,70 + Histogram histogram = metricRegistry.histogram(MetricRegistry.name("UserService", "getUser.histogram.setPcts"), + 60d, 70d); + + for (int i = 1; i <= 100; i++) { + histogram.update(i); + } + + HistogramMetric metric = histogram.calculateMetric(); + + assertThat(metric.pcts.get(60d)).isEqualTo(60); + assertThat(metric.pcts.get(70d)).isEqualTo(70); + + // new default 50 + metricRegistry.setDefaultPcts(new Double[] { 50d }); + Histogram histogramWithNewDefaultPcts = metricRegistry.histogram(MetricRegistry.name("UserService", + "getUser.histogram.newDefault")); + + for (int i = 1; i <= 100; i++) { + histogramWithNewDefaultPcts.update(i); + } + + metric = histogramWithNewDefaultPcts.calculateMetric(); + + assertThat(metric.pcts.get(50d)).isEqualTo(50); + assertThat(metric.pcts.get(90d)).isNull(); + } +} diff --git a/modules/metrics/src/test/java/org/springside/modules/metrics/ReporterTest.java b/modules/metrics/src/test/java/org/springside/modules/metrics/ReporterTest.java new file mode 100755 index 00000000..4d3af850 --- /dev/null +++ b/modules/metrics/src/test/java/org/springside/modules/metrics/ReporterTest.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import org.junit.Ignore; +import org.junit.Test; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Histogram; +import org.springside.modules.metrics.metric.Timer; +import org.springside.modules.metrics.metric.Timer.TimerContext; +import org.springside.modules.metrics.reporter.ConsoleReporter; +import org.springside.modules.metrics.reporter.GraphiteReporter; +import org.springside.modules.metrics.reporter.Slf4jReporter; +import org.springside.modules.metrics.utils.Clock.MockClock; + +public class ReporterTest { + + @Test + public void consoleReporter() { + runReport(new ConsoleReporter()); + } + + @Test + public void slf4jReporter() { + runReport(new Slf4jReporter()); + } + + @Test + @Ignore("manual test") + public void graphiteReporter() { + runReport(new GraphiteReporter(new InetSocketAddress("localhost", 2003))); + } + + @Test + public void schedulerStartStop() throws InterruptedException { + ReportScheduler scheduler = new ReportScheduler(new MetricRegistry(), new ConsoleReporter()); + scheduler.start(1, TimeUnit.SECONDS); + Thread.sleep(2000); + scheduler.stop(); + } + + private void runReport(Reporter reporter) { + MetricRegistry metricRegistry = new MetricRegistry(); + MockClock clock = new MockClock(); + Counter.clock = clock; + Timer.clock = clock; + + // counter + Counter counter = metricRegistry.counter(MetricRegistry.name("UserService", "getUser.counter")); + counter.inc(4); + Counter counter2 = metricRegistry.counter(MetricRegistry.name("UserService", "setUser.counter")); + counter2.inc(6); + clock.increaseTime(1000); + + // histogram + Histogram histogram = metricRegistry.histogram(MetricRegistry.name("UserService", "getUser.latency")); + for (int i = 1; i <= 100; i++) { + histogram.update(i); + } + Histogram histogram2 = metricRegistry.histogram(MetricRegistry.name("UserService", "setUser.latency")); + for (int i = 1; i <= 100; i++) { + histogram2.update(i * 2); + } + + // timer + Timer timer = metricRegistry.timer(MetricRegistry.name("UserService", "getUser.timer")); + for (int i = 1; i <= 10; i++) { + TimerContext timerContext = timer.start(); + clock.increaseTime(25); + timerContext.stop(); + } + Timer timer2 = metricRegistry.timer(MetricRegistry.name("UserService", "setUser.timer")); + for (int i = 1; i <= 10; i++) { + TimerContext timerContext = timer2.start(); + clock.increaseTime(75); + timerContext.stop(); + } + + // totally 2 seconds past + ReportScheduler scheduler = new ReportScheduler(metricRegistry, reporter); + scheduler.report(); + } +} diff --git a/modules/metrics/src/test/java/org/springside/modules/metrics/TimerTest.java b/modules/metrics/src/test/java/org/springside/modules/metrics/TimerTest.java new file mode 100755 index 00000000..4f6a4049 --- /dev/null +++ b/modules/metrics/src/test/java/org/springside/modules/metrics/TimerTest.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2005, 2014 springside.github.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + *******************************************************************************/ +package org.springside.modules.metrics; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springside.modules.metrics.metric.Counter; +import org.springside.modules.metrics.metric.Timer; +import org.springside.modules.metrics.metric.TimerMetric; +import org.springside.modules.metrics.metric.Timer.TimerContext; +import org.springside.modules.metrics.utils.Clock.MockClock; + +public class TimerTest { + + @Test + public void normal() { + MockClock clock = new MockClock(); + Timer.clock = clock; + Counter.clock = clock; + Timer timer = new Timer(new Double[] { 90d }); + + TimerContext timerContext = timer.start(); + clock.increaseTime(200); + timerContext.stop(); + + TimerContext timer2 = timer.start(); + clock.increaseTime(300); + timer2.stop(); + + TimerMetric metric = timer.calculateMetric(); + + assertThat(metric.counterMetric.totalCount).isEqualTo(2); + assertThat(metric.counterMetric.meanRate).isEqualTo(4); + assertThat(metric.counterMetric.latestCount).isEqualTo(2); + assertThat(metric.counterMetric.latestRate).isEqualTo(4); + + assertThat(metric.histogramMetric.min).isEqualTo(200); + assertThat(metric.histogramMetric.mean).isEqualTo(250); + assertThat(metric.histogramMetric.pcts.get(90d)).isEqualTo(300); + } +} diff --git a/modules/metrics/src/test/resources/logback.xml b/modules/metrics/src/test/resources/logback.xml new file mode 100755 index 00000000..b2ff4323 --- /dev/null +++ b/modules/metrics/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + \ No newline at end of file diff --git a/modules/pom.xml b/modules/pom.xml index b0924c23..ae7fad58 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -8,9 +8,9 @@ Springside :: Module pom - 4.1.8.RELEASE + 4.1.8.RELEASE 5.0.0-SNAPSHOT - 18.0 + 18.0 3.4 1.10 2.4 @@ -34,5 +34,6 @@ utils core + metrics