diff options
author | Ola Aunronning <olaa@yahooinc.com> | 2024-03-27 13:08:50 +0100 |
---|---|---|
committer | Ola Aunronning <olaa@yahooinc.com> | 2024-03-27 13:08:50 +0100 |
commit | 3e226d94dc32c79911c65fd31468879d47bc8ff2 (patch) | |
tree | 3ac0a1fad75c9424f3023ad0d44cff67c94ae36c | |
parent | f5965423c6a6808c06aab21a6af532df630dfbed (diff) |
Group metrics by name
Support NaN and +/- Inf
Escape label value chars
Remove unnecessary comments
-rw-r--r-- | container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java | 91 | ||||
-rw-r--r-- | container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java | 32 |
2 files changed, 87 insertions, 36 deletions
diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java index c2f101d7784..ebd5c38e3a3 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java @@ -123,8 +123,10 @@ public class StateHandler extends AbstractRequestHandler implements CapabilityRe } private String resolveContentType(URI requestUri) { - if (resolvePath(requestUri).equals(HISTOGRAMS_PATH) || isPrometheusRequest(requestUri.getQuery())) { + if (resolvePath(requestUri).equals(HISTOGRAMS_PATH)) { return "text/plain; charset=utf-8"; + } else if (isPrometheusRequest(requestUri.getQuery())) { + return "text/plain; version=0.0.4"; } else { return "application/json"; } @@ -224,45 +226,38 @@ public class StateHandler extends AbstractRequestHandler implements CapabilityRe var timestamp = snapshot.getToTime(TimeUnit.MILLISECONDS); var builder = new StringBuilder(); builder.append("# NOTE: THIS API IS NOT INTENDED FOR PUBLIC USE\n"); + var metrics = new ArrayList<PrometheusEntry>(); + 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); + metrics.add(new PrometheusEntry(metricName + "max", dims, gauge.getMax())); + metrics.add(new PrometheusEntry(metricName + "sum", dims, gauge.getSum())); + metrics.add(new PrometheusEntry(metricName + "count", dims, gauge.getCount())); if (gauge.getPercentiles().isPresent()) { for (Tuple2<String, Double> prefixAndValue : gauge.getPercentiles().get()) { - appendPrometheusEntry(builder, metricName + prefixAndValue.first + "percentile", dims, prefixAndValue.second, timestamp); + metrics.add(new PrometheusEntry(metricName + prefixAndValue.first + "percentile", dims, prefixAndValue.second)); } } } else if (tuple.val instanceof CountMetric count) { - appendPrometheusEntry(builder, metricName + "count", dims, count.getCount(), timestamp); + metrics.add(new PrometheusEntry(metricName + "count", dims, count.getCount())); } } + Collections.sort(metrics); + metrics.forEach(prometheusEntry -> prometheusEntry.appendPrometheusEntry(builder, 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 ""; + if (dimensions == null || !dimensions.iterator().hasNext()) return ""; StringBuilder builder = new StringBuilder(); + builder.append("{"); dimensions.forEach(entry -> { - var sanitized = prometheusSanitizedName(entry.getKey()) + "=\"" + entry.getValue() + "\","; + var sanitized = prometheusSanitizedName(entry.getKey()) + "=\"" + escapedLabelValue(entry.getValue()) + "\","; builder.append(sanitized); }); + builder.append("}"); return builder.toString(); } @@ -385,8 +380,33 @@ public class StateHandler extends AbstractRequestHandler implements CapabilityRe return name.replaceAll("\\.", "_"); } - private Number sanitizeIfDouble(Number num) { - return num instanceof Double d ? sanitizeDouble(d) : num; + private String sanitizeIfDouble(Number num) { + return num instanceof Double d ? prettyDouble(d) : num.toString(); + } + + private String escapedLabelValue(String labelValue) { + var builder = new StringBuilder(); + for (int i = 0; i < labelValue.length(); i++) { + var c = labelValue.charAt(i); + switch (c) { + case '\n': + builder.append("\\n"); + break; + case '\\': + case '"': + builder.append("\\") + .append(c); + break; + default: + builder.append(c); + } + } + return builder.toString(); + } + + private String prettyDouble(Double d) { + if (Double.isFinite(d) || d.isNaN()) return d.toString(); + return d.equals(Double.NEGATIVE_INFINITY) ? "-Inf" : "Inf"; } private static byte[] toPrettyString(JsonNode resources) throws JsonProcessingException { @@ -419,4 +439,29 @@ public class StateHandler extends AbstractRequestHandler implements CapabilityRe } } + class PrometheusEntry implements Comparable<PrometheusEntry> { + final String metricName; + final String dimensions; + final Number value; + + public PrometheusEntry(String metricName, String dimensions, Number value) { + this.metricName = metricName; + this.dimensions = dimensions; + this.value = value; + } + + @Override + public int compareTo(PrometheusEntry o) { + int comparison = this.metricName.compareTo(o.metricName); + return comparison != 0 ? comparison : this.dimensions.compareTo(o.dimensions); + } + + public void appendPrometheusEntry(StringBuilder builder, long timestamp) { + builder.append(metricName) + .append(dimensions) + .append(" ").append(sanitizeIfDouble(value)).append(" ") + .append(timestamp).append("\n"); + } + } + } diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java index 3d1a4a3583e..c99a61781cb 100644 --- a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java +++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java @@ -80,28 +80,34 @@ public class StateHandlerTest extends StateHandlerTestBase { @Test public void testPrometheusFormat() { var counterContext = StateMetricContext.newInstance(Map.of("label1", "val1", "label2", "val2")); + var otherContext = StateMetricContext.newInstance(Map.of( + "label1", "This label has \"quotes\"", + "label2", "This label, a\nnewline")); 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); + snapshot.add(otherContext, "some.counter", 1); + snapshot.add(otherContext, "some.counter", 2); + snapshot.set(null, "bar", 20); + snapshot.set(null, "bar", 40); + snapshot.set(null, "testing.infinity", Double.NEGATIVE_INFINITY); + snapshot.set(null, "testing.nan", Double.NaN); 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 + bar_count 2 300000 + bar_max 40.0 300000 + bar_sum 60.0 300000 + some_counter_count{label1="This label has \\"quotes\\"",label2="This label, a\\nnewline",} 3 300000 some_counter_count{label1="val1",label2="val2",} 30 300000 + testing_infinity_count 1 300000 + testing_infinity_max -Inf 300000 + testing_infinity_sum -Inf 300000 + testing_nan_count 1 300000 + testing_nan_max NaN 300000 + testing_nan_sum NaN 300000 """; assertEquals(expectedResponse, response); } |