All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ai.vespa.metricsproxy.service.MetricsParser Maven / Gradle / Ivy

There is a newer version: 8.458.13
Show newest version
// 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