blob: 8833ed5c334ae58cb87b1ecdb0b66a53140cb2fb [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.EventMetricData;
import com.android.os.StatsLog.ConfigMetricsReport;
import com.android.os.StatsLog.ConfigMetricsReportList;
import com.android.os.StatsLog.StatsLogReport;
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.util.MultiMap;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* A post processor that processes event metrics in statsd reports into key-value pairs, using the
* formatters specified on the processor.
*/
@OptionClass(alias = "statsd-event-metric-processor")
public class StatsdEventMetricPostProcessor extends StatsdGenericPostProcessor {
@Option(
name = "metric-formatter",
description =
"A formatter to format a statsd atom into a key-value pair for a metric. "
+ "Format: Use the atom field name as key and a 'key=value' string "
+ "as value, and enclose atom field reference in square brackets, "
+ "where they will be substituted with the field values in the atom. "
+ "Example: key: app_start_occurred, "
+ "value: [type]_startup_[pkg_name]=[windows_drawn_delay_millis]."
+ "Additionally, use '[_<field reference>]' to access the other fields of "
+ "the EventMetricData message, e.g. [_elapsed_timestamp_nanos] for the "
+ "elapsed_timestamp_nanos field that records when the event occurred. "
+ "At most one reference to repeated fields in each formatter is supported."
+ "Field definitions can be found in the atoms.proto file under "
+ "frameworks/base/cmds/statsd/src in the source tree."
)
private MultiMap<String, String> mMetricFormatters = new MultiMap<>();
// Corresponds to a field reference, e.g., "[field1_name.field2_name.field3_name]".
private static final Pattern FIELD_REF_PATTERN =
Pattern.compile("\\[(?:[a-zA-Z_]+\\.)*(?:[a-zA-Z_]+)\\]");
/**
* Parse the event metrics from the {@link ConfigMetricsReportList} using the atom formatters.
*
* <p>Event metrics resulting in duplicate keys will be stored as comma separated values.
*/
@Override
protected Map<String, Metric.Builder> parseMetricsFromReportList(
ConfigMetricsReportList reportList) {
// A multimap is used to store metrics with multiple potential values.
MultiMap<String, String> parsedMetrics = new MultiMap<>();
for (ConfigMetricsReport report : reportList.getReportsList()) {
for (StatsLogReport metric : report.getMetricsList()) {
if (!metric.hasEventMetrics()) {
continue;
}
// Look through each EventMetricData object's atom's every field, and extract
// metrics when the atom field name matches a set of configured metric formatters.
List<EventMetricData> dataItems = metric.getEventMetrics().getDataList();
for (EventMetricData data : dataItems) {
if (!data.hasAtom()) {
continue;
}
Map<FieldDescriptor, Object> atomFields = data.getAtom().getAllFields();
for (FieldDescriptor field : atomFields.keySet()) {
if (mMetricFormatters.containsKey(field.getName())) {
parsedMetrics.putAll(
getMetricsByFormatters(
data, field, mMetricFormatters.get(field.getName())));
}
}
}
}
}
// Convert the multimap to a normal map with list of values turned into comma separated
// values.
Map<String, Metric.Builder> finalMetrics = new HashMap<>();
for (String key : parsedMetrics.keySet()) {
// TODO(b/140434593): Move to repeated String fields and make sure that is supported in
// other post processors as well.
String value = String.join(",", parsedMetrics.get(key));
finalMetrics.put(key, TfMetricProtoUtil.stringToMetric(value).toBuilder());
}
return finalMetrics;
}
/**
* Helper method to get metrics from an {@link EventMetricData} instance using the desired atom
* field and metric formatters.
*/
private MultiMap<String, String> getMetricsByFormatters(
EventMetricData data, FieldDescriptor atomField, List<String> formatters) {
MultiMap<String, String> metrics = new MultiMap<>();
Message atomContent = (Message) data.getAtom().getField(atomField);
for (String formatter : formatters) {
String keyFormatter = formatter.split("=")[0];
String valueFormatter = formatter.split("=")[1];
List<String> metricKeys = fillInPlaceholders(keyFormatter, data, atomContent);
List<String> metricValues = fillInPlaceholders(valueFormatter, data, atomContent);
if (metricKeys.size() > 1 && metricValues.size() > 1) {
// If a repeated field is referenced in more than one location in the same
// formatter, it would be hard to determine whether there is a "pairing" relation
// between the two fields with the current field reference syntax. Specifically, one
// might want repeated_field.field1 and repeated_field.field2 to only appear paired
// within the same repeated_field instance, but the current logic produces a cross
// product between all repeated_field.field1 and repeated_field.field2 values, hence
// the warning below.
CLog.w(
"Found repeated fields in both metric key and value in formatting pair "
+ "%s: %s. This is currently unsupported and could result in "
+ "meaningless data. Skipping reporting on this pair.",
atomField.getName(), formatter);
continue;
}
for (String metricKey : metricKeys) {
for (String metricValue : metricValues) {
metrics.put(metricKey, metricValue);
}
}
}
return metrics;
}
/**
* Get a nested field reference, i.e. field_1.field_2.field_3, from a proto message as a string.
* Returns an empty list when a field cannot be found, either because it's invalid or does not
* exist in the message.
*
* <p>If the field reference contains repeated fields, each instance is expanded, resulting in a
* list of strings.
*/
private List<String> getNestedFieldFromMessageAsStrings(
Object messageOrObject, List<String> references) {
if (references.isEmpty()) {
return Arrays.asList(String.valueOf(messageOrObject));
}
if (!(messageOrObject instanceof Message)) {
CLog.e(
"Attempting to read field %s from object of type %s, "
+ "which is not a proto message.",
references.get(0), messageOrObject.getClass());
return new ArrayList<String>();
}
Message message = (Message) messageOrObject;
String reference = references.get(0);
FieldDescriptor fieldDescriptor = message.getDescriptorForType().findFieldByName(reference);
if (fieldDescriptor == null) {
CLog.e("Could not find field %s in message %s.", reference, message);
return new ArrayList<String>();
}
Object fieldValue = message.getField(fieldDescriptor);
if (fieldValue instanceof List) {
return ((List<? extends Object>) fieldValue)
.stream()
.flatMap(
v ->
getNestedFieldFromMessageAsStrings(
v, references.subList(1, references.size()))
.stream())
.collect(Collectors.toList());
}
return getNestedFieldFromMessageAsStrings(
fieldValue, references.subList(1, references.size()));
}
/** Fill in the placeholders in the formatter using the proto message as source. */
private List<String> fillInPlaceholders(
String formatter, EventMetricData eventMetric, Message atomContent) {
Matcher matcher = FIELD_REF_PATTERN.matcher(formatter);
List<String> results = Arrays.asList(formatter);
while (matcher.find()) {
String placeholder = matcher.group();
// Strip the brackets.
String fieldReference = placeholder.substring(1, placeholder.length() - 1);
List<String> actual = new ArrayList();
if (fieldReference.startsWith("_")) {
actual.addAll(
getNestedFieldFromMessageAsStrings(
eventMetric,
Arrays.asList(fieldReference.substring(1).split("\\."))));
} else {
actual.addAll(
getNestedFieldFromMessageAsStrings(
atomContent, Arrays.asList(fieldReference.split("\\."))));
}
// If both the existing expansion results and newly expanded results have multiple
// entries, then both the existing expansion and new expansion referred to repeated
// fields.
if (results.size() > 1 && actual.size() > 1) {
// If a repeated field is referenced in more than one location in the same
// formatter, it would be hard to determine whether there is a "pairing" relation
// between the two fields with the current field reference syntax. Specifically, one
// might want repeated_field.field1 and repeated_field.field2 to only appear paired
// within the same repeated_field instance, but the current logic produces a cross
// product between all repeated_field.field1 and repeated_field.field2 values, hence
// the warning below.
CLog.w(
"Found repeated fields in both metric key and value in formatter %s. This "
+ "is currently unsupported and could result in meaningless data. "
+ "Skipping reporting on this formatter.",
formatter);
return new ArrayList<>();
}
List<String> updatedResults =
results.stream()
.flatMap(r -> actual.stream().map(a -> r.replace(placeholder, a)))
.collect(Collectors.toList());
results = updatedResults;
}
return results;
}
}