// Copyright 2016 Twitter. 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 com.twitter.heron.common.utils.metrics; import java.time.Duration; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Logger; import com.twitter.heron.api.metric.IMetric; import com.twitter.heron.api.metric.IMetricsRegister; import com.twitter.heron.common.basics.Communicator; import com.twitter.heron.common.basics.WakeableLooper; import com.twitter.heron.proto.system.Metrics; /** * MetricsCollector could: * 1. Register a list of metrics with gathering interval * 2. Send the metrics to a queue after gathering the metrics */ public class MetricsCollector implements IMetricsRegister { private static final Logger LOG = Logger.getLogger(MetricsCollector.class.getName()); private Map<String, IMetric<?>> metrics; private Map<Integer, List<String>> timeBucketToMetricNames; private WakeableLooper runnableToGatherMetrics; private Communicator<Metrics.MetricPublisherPublishMessage> queue; public MetricsCollector(WakeableLooper runnableToGatherMetrics, Communicator<Metrics.MetricPublisherPublishMessage> queue) { metrics = new HashMap<>(); timeBucketToMetricNames = new HashMap<>(); this.queue = queue; this.runnableToGatherMetrics = runnableToGatherMetrics; } @Override public <T extends IMetric<U>, U> T registerMetric( String name, T metric, final int timeBucketSizeInSecs) { if (metrics.containsKey(name)) { throw new RuntimeException("Another metric has already been registered with name: " + name); } metrics.put(name, metric); if (timeBucketToMetricNames.containsKey(timeBucketSizeInSecs)) { timeBucketToMetricNames.get(timeBucketSizeInSecs).add(name); } else { timeBucketToMetricNames.put(timeBucketSizeInSecs, new LinkedList<String>()); timeBucketToMetricNames.get(timeBucketSizeInSecs).add(name); Runnable r = new Runnable() { public void run() { gatherMetrics(timeBucketSizeInSecs); } }; runnableToGatherMetrics.registerTimerEvent(Duration.ofSeconds(timeBucketSizeInSecs), r); } return metric; } public void registerMetricSampleRunnable( final Runnable sampleRunnable, final Duration sampleInterval) { Runnable sampleTimer = new Runnable() { @Override public void run() { sampleRunnable.run(); runnableToGatherMetrics.registerTimerEvent(sampleInterval, this); } }; runnableToGatherMetrics.registerTimerEvent(sampleInterval, sampleTimer); } // Force to gather all metrics and put them into the queue // It is most likely happen when we are handling some unexpected cases, such as exiting public void forceGatherAllMetrics() { LOG.info("Forcing to gather all metrics and flush out."); Metrics.MetricPublisherPublishMessage.Builder builder = Metrics.MetricPublisherPublishMessage.newBuilder(); for (List<String> metricNames : timeBucketToMetricNames.values()) { for (String metricName : metricNames) { gatherOneMetric(metricName, builder); } Metrics.MetricPublisherPublishMessage msg = builder.build(); queue.offer(msg); } } private void addDataToMetricPublisher(Metrics.MetricPublisherPublishMessage.Builder builder, String metricName, Object metricValue) { // Metric name is discarded if value is of type MetricsDatum or ExceptionData. if (metricValue instanceof Metrics.MetricDatum.Builder) { builder.addMetrics((Metrics.MetricDatum.Builder) metricValue); } else if (metricValue instanceof Metrics.ExceptionData.Builder) { builder.addExceptions((Metrics.ExceptionData.Builder) metricValue); } else { assert metricName != null; Metrics.MetricDatum.Builder d = Metrics.MetricDatum.newBuilder(); d.setName(metricName).setValue(metricValue.toString()); builder.addMetrics(d); } } @SuppressWarnings("unchecked") private void gatherMetrics(final int timeBucketSizeInSecs) { // Gather the metrics in Map<String, IMetric> metrics // We will get the correct metrics by: // 1. Find the name in Map<Integer, List<String>> timeBucketToMetricNames // by timeBucketSizeInSecs // 2. Find the IMetric in Map<String, IMetric> metrics by the name if (timeBucketToMetricNames.containsKey(timeBucketSizeInSecs)) { Metrics.MetricPublisherPublishMessage.Builder builder = Metrics.MetricPublisherPublishMessage.newBuilder(); for (String metricName : timeBucketToMetricNames.get(timeBucketSizeInSecs)) { gatherOneMetric(metricName, builder); } Metrics.MetricPublisherPublishMessage msg = builder.build(); queue.offer(msg); // Schedule ourselves again -- Replay itself // TODO: Use TimerTask. Runnable r = new Runnable() { public void run() { gatherMetrics(timeBucketSizeInSecs); } }; runnableToGatherMetrics.registerTimerEvent(Duration.ofSeconds(timeBucketSizeInSecs), r); } } // Gather the value of given metricName, convert it into protobuf, // and add it to MetricPublisherPublishMessage builder given. @SuppressWarnings("unchecked") private void gatherOneMetric( String metricName, Metrics.MetricPublisherPublishMessage.Builder builder) { Object metricValue = metrics.get(metricName).getValueAndReset(); // Decide how to handle the metric based on type if (metricValue == null) { return; } if (metricValue instanceof Map) { for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) metricValue).entrySet()) { if (entry.getKey() != null && entry.getValue() != null) { addDataToMetricPublisher( builder, metricName + "/" + entry.getKey().toString(), entry.getValue()); } } } else if (metricValue instanceof Collection) { int index = 0; for (Object value : (Collection) metricValue) { addDataToMetricPublisher(builder, metricName + "/" + (index++), value); } } else { addDataToMetricPublisher(builder, metricName, metricValue); } } }