Skip to content

Commit

Permalink
Allow prometheus format from /state/v1/metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
olaaun committed Mar 14, 2024
1 parent 1502728 commit d73e509
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.yahoo.container.jdisc.state.JsonUtil.sanitizeDouble;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
* A handler which returns state (health) information from this container instance: Status, metrics and vespa version.
Expand Down Expand Up @@ -121,7 +123,7 @@ protected Iterable<ByteBuffer> responseContent() {
}

private String resolveContentType(URI requestUri) {
if (resolvePath(requestUri).equals(HISTOGRAMS_PATH)) {
if (resolvePath(requestUri).equals(HISTOGRAMS_PATH) || isPrometheusRequest(requestUri.getQuery())) {
return "text/plain; charset=utf-8";
} else {
return "application/json";
Expand All @@ -135,9 +137,9 @@ private ByteBuffer buildContent(URI requestUri, List<ByteBuffer> input) {
case "" -> ByteBuffer.wrap(apiLinks(requestUri));
case CONFIG_GENERATION_PATH -> ByteBuffer.wrap(toPrettyString(config));
case HISTOGRAMS_PATH -> ByteBuffer.wrap(buildHistogramsOutput());
case HEALTH_PATH, METRICS_PATH -> ByteBuffer.wrap(buildMetricOutput(suffix));
case HEALTH_PATH, METRICS_PATH -> ByteBuffer.wrap(buildMetricOutput(suffix, requestUri.getQuery()));
case VERSION_PATH -> ByteBuffer.wrap(buildVersionOutput());
default -> ByteBuffer.wrap(buildMetricOutput(suffix)); // XXX should possibly do something else here
default -> ByteBuffer.wrap(buildMetricOutput(suffix, requestUri.getQuery())); // XXX should possibly do something else here
};
} catch (JsonProcessingException e) {
throw new RuntimeException("Bad JSON construction", e);
Expand Down Expand Up @@ -192,7 +194,9 @@ private static byte[] buildVersionOutput() throws JsonProcessingException {
.put("version", Vtag.currentVersion.toString()));
}

private byte[] buildMetricOutput(String consumer) throws JsonProcessingException {
private byte[] buildMetricOutput(String consumer, String query) throws JsonProcessingException {
if (isPrometheusRequest(query))
return buildPrometheusForConsumer(consumer);
return toPrettyString(buildJsonForConsumer(consumer));
}

Expand All @@ -212,6 +216,56 @@ private ObjectNode buildJsonForConsumer(String consumer) {
return ret;
}

private byte[] buildPrometheusForConsumer(String consumer) {
var snapshot = getSnapshot();
if (snapshot == null)
return new byte[0];

var timestamp = snapshot.getToTime(TimeUnit.MILLISECONDS);
var builder = new StringBuilder();
builder.append("# NOTE: THIS API IS NOT INTENDED FOR PUBLIC USE\n");
for (var tuple : collapseMetrics(snapshot, consumer)) {
var dims = toPrometheusDimensions(tuple.dim);
var metricName = prometheusSanitizedName(tuple.key) + "_";
if (tuple.val instanceof GaugeMetric gauge) {
appendPrometheusEntry(builder, metricName + "max", dims, gauge.getMax(), timestamp);
appendPrometheusEntry(builder, metricName + "sum", dims, gauge.getSum(), timestamp);
appendPrometheusEntry(builder, metricName + "count", dims, gauge.getCount(), timestamp);
if (gauge.getPercentiles().isPresent()) {
for (Tuple2<String, Double> prefixAndValue : gauge.getPercentiles().get()) {
appendPrometheusEntry(builder, metricName + prefixAndValue.first + "percentile", dims, prefixAndValue.second, timestamp);
}
}
} else if (tuple.val instanceof CountMetric count) {
appendPrometheusEntry(builder, metricName + "count", dims, count.getCount(), timestamp);
}
}
return builder.toString().getBytes(UTF_8);
}

private void appendPrometheusEntry(StringBuilder builder, String metricName, String dimension, Number value, long timeStamp) {
builder.append("# HELP ")
.append(metricName)
.append("\n# TYPE ")
.append(metricName)
.append(" untyped\n");

builder.append(metricName)
.append("{").append(dimension).append("}")
.append(" ").append(sanitizeIfDouble(value)).append(" ")
.append(timeStamp).append("\n");
}

private String toPrometheusDimensions(MetricDimensions dimensions) {
if (dimensions == null) return "";
StringBuilder builder = new StringBuilder();
dimensions.forEach(entry -> {
var sanitized = prometheusSanitizedName(entry.getKey()) + "=\"" + entry.getValue() + "\",";
builder.append(sanitized);
});
return builder.toString();
}

private MetricSnapshot getSnapshot() {
return snapshotProvider.latestSnapshot();
}
Expand Down Expand Up @@ -322,6 +376,19 @@ static List<Tuple> flattenAllMetrics(MetricSnapshot snapshot) {
return metrics;
}

private boolean isPrometheusRequest(String query) {
if (query == null) return false;
return Arrays.asList(query.split("&")).contains("format=prometheus");
}

private String prometheusSanitizedName(String name) {
return name.replaceAll("\\.", "_");
}

private Number sanitizeIfDouble(Number num) {
return num instanceof Double d ? sanitizeDouble(d) : num;
}

private static byte[] toPrettyString(JsonNode resources) throws JsonProcessingException {
return jsonMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(resources)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,35 @@ void gaugeSnapshotsTracksCountMinMaxAvgPerPeriod() throws Exception {
assertEquals(2, metricValues.get("count").asInt(), json.toString());
}

@Test
public void testPrometheusFormat() {
var counterContext = StateMetricContext.newInstance(Map.of("label1", "val1", "label2", "val2"));
var snapshot = new MetricSnapshot(0L, SNAPSHOT_INTERVAL, TimeUnit.MILLISECONDS);
snapshot.set(null, "bar", 20);
snapshot.set(null, "bar", 40);
snapshot.add(counterContext, "some.counter", 10);
snapshot.add(counterContext, "some.counter", 20);
snapshotProvider.setSnapshot(snapshot);

var response = requestAsString(V1_URI + "metrics?format=prometheus");
var expectedResponse = """
# NOTE: THIS API IS NOT INTENDED FOR PUBLIC USE
# HELP bar_max
# TYPE bar_max untyped
bar_max{} 40.0 300000
# HELP bar_sum
# TYPE bar_sum untyped
bar_sum{} 60.0 300000
# HELP bar_count
# TYPE bar_count untyped
bar_count{} 2 300000
# HELP some_counter_count
# TYPE some_counter_count untyped
some_counter_count{label1="val1",label2="val2",} 30 300000
""";
assertEquals(expectedResponse, response);
}

private JsonNode getFirstMetricValueNode(JsonNode root) {
assertEquals(1, root.get("metrics").get("values").size(), root.toString());
JsonNode metricValues = root.get("metrics").get("values").get(0).get("values");
Expand Down

0 comments on commit d73e509

Please sign in to comment.