blob: 29db7843a932d0e64836be469b4fa724220c69bd [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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.android.tradefed.postprocessor;
import com.android.os.StatsLog.ConfigMetricsReportList;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.LogFile;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A post processor that processes binary proto statsd reports into key-value pairs by expanding the
* report as a tree structure.
*
* <p>This processor is agnostic to the type of metric reports it encounters. It also serves as the
* base class for other statsd post processors by including common code to retrieve and read statsd
* reports.
*/
@OptionClass(alias = "statsd-generic-processor")
public class StatsdGenericPostProcessor extends BasePostProcessor {
// TODO(harrytczhang): Add ability to dump the results out to a JSON file once saving files from
// post processors is possible.
@Option(
name = "statsd-report-data-prefix",
description =
"Prefix for identifying a data name that points to a statsd report. "
+ "Can be repeated. "
+ "Will also be prepended to the outcoming metric to \"namespace\" them."
)
private Set<String> mReportPrefixes = new HashSet<>();
@VisibleForTesting static final String METRIC_SEP = "-";
@VisibleForTesting static final String INDEX_SEP = "#";
// A few fields to skip as they are large and not currently used.
private static final String UID_MAP_FIELD_NAME = "uid_map";
private static final String STRINGS_FIELD_NAME = "strings";
private static final ImmutableList FIELDS_TO_SKIP =
ImmutableList.of(UID_MAP_FIELD_NAME, STRINGS_FIELD_NAME);
@Override
public Map<String, Metric.Builder> processTestMetricsAndLogs(
TestDescription testDescription,
HashMap<String, Metric> testMetrics,
Map<String, LogFile> testLogs) {
return processStatsdReportsFromLogs(testLogs);
}
@Override
public Map<String, Metric.Builder> processRunMetricsAndLogs(
HashMap<String, Metric> rawMetrics, Map<String, LogFile> runLogs) {
return processStatsdReportsFromLogs(runLogs);
}
/**
* Parse metrics from a {@link ConfigMetricsReportList} read from a statsd report proto.
*
* <p>This is the main interface for subclasses of this statsd post processor.
*/
protected Map<String, Metric.Builder> parseMetricsFromReportList(
ConfigMetricsReportList reportList) {
return convertProtoMessage(reportList);
}
/**
* Uses the metrics to locate the statsd report files and then call into the processing method
* to parse the reports into metrics.
*/
private Map<String, Metric.Builder> processStatsdReportsFromLogs(Map<String, LogFile> logs) {
Map<String, Metric.Builder> parsedMetrics = new HashMap<>();
for (String key : logs.keySet()) {
// Go through the logs and match them to the list of prefixes. Only proceed when a match
// is found.
Optional<String> reportPrefix =
mReportPrefixes.stream().filter(prefix -> key.startsWith(prefix)).findAny();
if (!reportPrefix.isPresent()) {
continue;
}
File reportFile = new File(logs.get(key).getPath());
try (FileInputStream reportStream = new FileInputStream(reportFile)) {
ConfigMetricsReportList reportList =
ConfigMetricsReportList.parseFrom(reportStream);
if (reportList.getReportsList().isEmpty()) {
CLog.i("No reports collected for %s.", key);
continue;
}
Map<String, Metric.Builder> metricsForReport =
parseMetricsFromReportList(reportList);
// Prepend the report prefix to serve as top-level organization of the outputted
// metrics. Please see comment on the prepend method for details.
parsedMetrics.putAll(addPrefixToMetrics(metricsForReport, reportPrefix.get()));
} catch (FileNotFoundException e) {
CLog.e("Report file does not exist at supposed path %s.", logs.get(key).getPath());
} catch (IOException e) {
CLog.i("Failed to read report file due to error %s.", e.toString());
}
}
return parsedMetrics;
}
/**
* Add a prefix to the metrics out of a single report.
*
* <p>This is used to add top-level organization for the metric output as due to the nesting
* nature of the metrics the actual metric key can be hard to read at a glance without the
* prefixes added here. As an example, if we use "statsd-app-start" as a prefix, then some
* of the metrics would read:
*
* <pre>
* statsd-app-start-reports-metrics-event_metrics-data-atom-app_start_occurred-type: WARM
* statsd-app-start-reports-metrics-event_metrics-data-atom-app_start_occurred-uid: 10149
* statsd-app-start-reports-metrics-event_metrics-data-elapsed_timestamp_nanos: 1449806339931935
*/
private Map<String, Metric.Builder> addPrefixToMetrics(
Map<String, Metric.Builder> metrics, String prefix) {
return metrics.entrySet()
.stream()
.collect(
Collectors.toMap(
e -> String.join(METRIC_SEP, prefix, e.getKey()),
e -> e.getValue()));
}
/**
* Flatten a proto message to a set of key-value pairs which become metrics.
*
* <p>It treats a message as a tree and uses the concatenated path from the root to a
* non-message value as the key, while the non-message value becomes the metric value. Nodes
* from repeated fields are distinguished by having a 1-based index number appended to all
* elements after the first element. The first element is not appended as in most cases only one
* element is in the list field and having it appear as-is is easier to read.
*
* <p>TODO(b/140432161): Separate this out into a utility should the need arise.
*/
protected Map<String, Metric.Builder> convertProtoMessage(Message reportMessage) {
Map<FieldDescriptor, Object> fields = reportMessage.getAllFields();
Map<String, Metric.Builder> convertedMetrics = new HashMap<String, Metric.Builder>();
for (Entry<FieldDescriptor, Object> entry : fields.entrySet()) {
if (FIELDS_TO_SKIP.contains(entry.getKey().getName())) {
continue;
}
if (entry.getValue() instanceof Message) {
Map<String, Metric.Builder> messageMetrics =
convertProtoMessage((Message) entry.getValue());
for (Entry<String, Metric.Builder> metricEntry : messageMetrics.entrySet()) {
convertedMetrics.put(
String.join(METRIC_SEP, entry.getKey().getName(), metricEntry.getKey()),
metricEntry.getValue());
}
} else if (entry.getValue() instanceof List) {
List<? extends Object> listMetrics = (List) entry.getValue();
for (int i = 0; i < listMetrics.size(); i++) {
String metricKeyRoot =
(i == 0
? entry.getKey().getName()
: String.join(
INDEX_SEP,
entry.getKey().getName(),
String.valueOf(i + 1)));
if (listMetrics.get(i) instanceof Message) {
Map<String, Metric.Builder> messageMetrics =
convertProtoMessage((Message) listMetrics.get(i));
for (Entry<String, Metric.Builder> metricEntry :
messageMetrics.entrySet()) {
convertedMetrics.put(
String.join(METRIC_SEP, metricKeyRoot, metricEntry.getKey()),
metricEntry.getValue());
}
} else {
convertedMetrics.put(
metricKeyRoot,
TfMetricProtoUtil.stringToMetric(listMetrics.get(i).toString())
.toBuilder());
}
}
} else {
convertedMetrics.put(
entry.getKey().getName(),
TfMetricProtoUtil.stringToMetric(entry.getValue().toString()).toBuilder());
}
}
return convertedMetrics;
}
}