ai.vespa.metricsproxy.service.MetricsParser Maven / Gradle / Ivy
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.metricsproxy.service;
import com.yahoo.json.Jackson;
import ai.vespa.metricsproxy.metric.Metric;
import ai.vespa.metricsproxy.metric.model.DimensionId;
import ai.vespa.metricsproxy.metric.model.MetricId;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static ai.vespa.metricsproxy.metric.model.DimensionId.toDimensionId;
/**
* Fetch metrics for a given vespa service
*
* @author Jo Kristian Bergum
*/
public class MetricsParser {
private static final Double ZERO_DOUBLE = 0d;
public interface Collector {
void accept(Metric metric);
}
private static final ObjectMapper jsonMapper = Jackson.mapper();
public static void parse(String data, Collector consumer) throws IOException {
try (JsonParser parser = jsonMapper.createParser(data)) {
parse(parser, consumer);
}
}
static void parse(InputStream data, Collector consumer) throws IOException {
try (JsonParser parser = jsonMapper.createParser(data)) {
parse(parser, consumer);
}
}
// Top level 'metrics' object, with e.g. 'time', 'status' and 'metrics'.
private static void parse(JsonParser parser, Collector consumer) throws IOException {
if (parser.nextToken() != JsonToken.START_OBJECT) {
throw new IOException("Expected start of object, got " + parser.currentToken());
}
for (parser.nextToken(); parser.currentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String fieldName = parser.currentName();
JsonToken token = parser.nextToken();
if (fieldName.equals("metrics")) {
parseMetrics(parser, consumer);
} else {
if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
parser.skipChildren();
}
}
}
}
static private Instant parseSnapshot(JsonParser parser) throws IOException {
if (parser.currentToken() != JsonToken.START_OBJECT) {
throw new IOException("Expected start of 'snapshot' object, got " + parser.currentToken());
}
Instant timestamp = Instant.now();
for (parser.nextToken(); parser.currentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String fieldName = parser.currentName();
JsonToken token = parser.nextToken();
if (fieldName.equals("to")) {
timestamp = Instant.ofEpochSecond(parser.getLongValue());
timestamp = Instant.ofEpochSecond(Metric.adjustTime(timestamp.getEpochSecond(), Instant.now().getEpochSecond()));
} else {
if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
parser.skipChildren();
}
}
}
return timestamp;
}
// 'metrics' object with 'snapshot' and 'values' arrays
static private void parseMetrics(JsonParser parser, Collector consumer) throws IOException {
if (parser.currentToken() != JsonToken.START_OBJECT) {
throw new IOException("Expected start of 'metrics' object, got " + parser.currentToken());
}
Instant timestamp = Instant.now();
for (parser.nextToken(); parser.currentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String fieldName = parser.currentName();
JsonToken token = parser.nextToken();
if (fieldName.equals("snapshot")) {
timestamp = parseSnapshot(parser);
} else if (fieldName.equals("values")) {
parseMetricValues(parser, timestamp, consumer);
} else {
if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
parser.skipChildren();
}
}
}
}
// 'values' array
static private void parseMetricValues(JsonParser parser, Instant timestamp, Collector consumer) throws IOException {
if (parser.currentToken() != JsonToken.START_ARRAY) {
throw new IOException("Expected start of 'metrics:values' array, got " + parser.currentToken());
}
Map, Map> uniqueDimensions = new HashMap<>();
while (parser.nextToken() == JsonToken.START_OBJECT) {
handleValue(parser, timestamp, consumer, uniqueDimensions);
}
}
// One item in the 'values' array, where each item has 'name', 'values' and 'dimensions'
static private void handleValue(JsonParser parser, Instant timestamp, Collector consumer,
Map, Map> uniqueDimensions) throws IOException {
String name = "";
String description = "";
Map dim = Map.of();
List> values = List.of();
for (parser.nextToken(); parser.currentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String fieldName = parser.currentName();
JsonToken token = parser.nextToken();
switch (fieldName) {
case "name" -> name = parser.getText();
case "description" -> description = parser.getText();
case "dimensions" -> dim = parseDimensions(parser, uniqueDimensions);
case "values" -> values = parseValues(parser);
default -> {
if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
parser.skipChildren();
}
}
}
}
if (name.isEmpty()) {
throw new IOException("missing name for entry in 'values' array");
}
for (Map.Entry value : values) {
consumer.accept(new Metric(MetricId.toMetricId(name+"."+value.getKey()), value.getValue(), timestamp, dim, description));
}
}
private static Map parseDimensions(JsonParser parser,
Map, Map> uniqueDimensions) throws IOException {
Set dimensions = new HashSet<>();
for (parser.nextToken(); parser.currentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String fieldName = parser.currentName();
JsonToken token = parser.nextToken();
if (token == JsonToken.VALUE_STRING){
String value = parser.getValueAsString();
dimensions.add(Dimension.of(fieldName, value));
} else if (token == JsonToken.VALUE_NULL) {
// TODO Should log a warning if this happens
} else {
throw new IllegalArgumentException("Dimension '" + fieldName + "' must be a string");
}
}
return uniqueDimensions.computeIfAbsent(dimensions,
key -> dimensions.stream().collect(Collectors.toUnmodifiableMap(
dim -> toDimensionId(dim.id), dim -> dim.value)));
}
record Dimension(String id, String value) {
static Dimension of(String id, String value) {
return new Dimension(id, value);
}
}
private static List> parseValues(JsonParser parser) throws IOException {
List> metrics = new ArrayList<>();
for (parser.nextToken(); parser.currentToken() != JsonToken.END_OBJECT; parser.nextToken()) {
String metricName = parser.currentName();
JsonToken token = parser.nextToken();
if (token == JsonToken.VALUE_NUMBER_INT) {
metrics.add(Map.entry(metricName, parser.getLongValue()));
} else if (token == JsonToken.VALUE_NUMBER_FLOAT) {
double value = parser.getValueAsDouble();
metrics.add(Map.entry(metricName, value == ZERO_DOUBLE ? ZERO_DOUBLE : value));
} else {
throw new IllegalArgumentException("Value for aggregator '" + metricName + "' is not a number");
}
}
return metrics;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy