/* * Copyright (c) 2015 Spotify AB. * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.spotify.heroic.metric; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.spotify.heroic.common.DateRange; import com.spotify.heroic.common.Statistics; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.UUID; import lombok.Data; import lombok.NonNull; @Data @JsonSerialize(using = QueryMetricsResponse.Serializer.class) public class QueryMetricsResponse { @NonNull private final UUID queryId; @NonNull private final DateRange range; @NonNull private final List<ShardedResultGroup> result; @NonNull private final Statistics statistics = Statistics.empty(); @NonNull private final List<RequestError> errors; @NonNull private final QueryTrace trace; @NonNull private final ResultLimits limits; @NonNull private final Optional<Long> preAggregationSampleSize; public static class Serializer extends JsonSerializer<QueryMetricsResponse> { @Override public void serialize( QueryMetricsResponse response, JsonGenerator g, SerializerProvider provider ) throws IOException { final List<ShardedResultGroup> result = response.getResult(); final Map<String, SortedSet<String>> common = calculateCommon(g, result); g.writeStartObject(); g.writeObjectField("queryId", response.getQueryId()); g.writeObjectField("range", response.getRange()); g.writeObjectField("trace", response.getTrace()); g.writeObjectField("limits", response.getLimits()); g.writeFieldName("commonTags"); serializeCommonTags(g, common); g.writeFieldName("result"); serializeResult(g, common, result); g.writeObjectField("preAggregationSampleSize", response.getPreAggregationSampleSize()); g.writeFieldName("errors"); serializeErrors(g, response.getErrors()); g.writeEndObject(); } private void serializeCommonTags( final JsonGenerator g, final Map<String, SortedSet<String>> common ) throws IOException { g.writeStartObject(); for (final Map.Entry<String, SortedSet<String>> e : common.entrySet()) { g.writeFieldName(e.getKey()); g.writeStartArray(); for (final String value : e.getValue()) { g.writeString(value); } g.writeEndArray(); } g.writeEndObject(); } private void serializeErrors(final JsonGenerator g, final List<RequestError> errors) throws IOException { g.writeStartArray(); for (final RequestError error : errors) { g.writeObject(error); } g.writeEndArray(); } private Map<String, SortedSet<String>> calculateCommon( final JsonGenerator g, final List<ShardedResultGroup> result ) { final Map<String, SortedSet<String>> common = new HashMap<>(); final Set<String> blacklist = new HashSet<>(); for (final ShardedResultGroup r : result) { final Set<Map.Entry<String, SortedSet<String>>> entries = SeriesValues.fromSeries(r.getSeries().iterator()).getTags().entrySet(); for (final Map.Entry<String, SortedSet<String>> e : entries) { if (blacklist.contains(e.getKey())) { continue; } final SortedSet<String> previous = common.put(e.getKey(), e.getValue()); if (previous == null) { continue; } if (previous.equals(e.getValue())) { continue; } blacklist.add(e.getKey()); common.remove(e.getKey()); } } return common; } private void serializeResult( final JsonGenerator g, final Map<String, SortedSet<String>> common, final List<ShardedResultGroup> result ) throws IOException { g.writeStartArray(); for (final ShardedResultGroup group : result) { g.writeStartObject(); final MetricCollection collection = group.getMetrics(); final SeriesValues series = SeriesValues.fromSeries(group.getSeries().iterator()); g.writeStringField("type", collection.getType().identifier()); g.writeStringField("hash", Integer.toHexString(group.hashGroup())); g.writeObjectField("shard", group.getShard()); g.writeNumberField("cadence", group.getCadence()); g.writeObjectField("values", collection.getData()); writeKey(g, series.getKeys()); writeTags(g, common, series.getTags()); writeTagCounts(g, series.getTags()); g.writeEndObject(); } g.writeEndArray(); } void writeKey(JsonGenerator g, final SortedSet<String> keys) throws IOException { g.writeFieldName("key"); if (keys.size() == 1) { g.writeString(keys.iterator().next()); } else { g.writeNull(); } } void writeTags( JsonGenerator g, final Map<String, SortedSet<String>> common, final Map<String, SortedSet<String>> tags ) throws IOException { g.writeFieldName("tags"); g.writeStartObject(); for (final Map.Entry<String, SortedSet<String>> pair : tags.entrySet()) { // TODO: enable this when commonTags is used. /*if (common.containsKey(pair.getKey())) { continue; }*/ final SortedSet<String> values = pair.getValue(); if (values.size() != 1) { continue; } g.writeStringField(pair.getKey(), values.iterator().next()); } g.writeEndObject(); } void writeTagCounts(JsonGenerator g, final Map<String, SortedSet<String>> tags) throws IOException { g.writeFieldName("tagCounts"); g.writeStartObject(); for (final Map.Entry<String, SortedSet<String>> pair : tags.entrySet()) { final SortedSet<String> values = pair.getValue(); if (values.size() <= 1) { continue; } g.writeNumberField(pair.getKey(), values.size()); } g.writeEndObject(); } } public Summary summarize() { return new Summary(range, ShardedResultGroup.summarize(result), statistics, errors, trace, limits, preAggregationSampleSize); } // Only include data suitable to log to query log @Data public class Summary { private final DateRange range; private final ShardedResultGroup.MultiSummary result; private final Statistics statistics; private final List<RequestError> errors; private final QueryTrace trace; private final ResultLimits limits; private final Optional<Long> preAggregationSampleSize; } }