Skip to content

Commit 4d9f22f

Browse files
committed
[SPARK-53325] Support Prometheus 2.0 text-based-format and best practices for metrics naming
### What changes were proposed in this pull request? This PR adds support for Prometheus text-based-format and best practices for metrics naming Existing format ``` metrics_jvm_bufferPool_direct_capacity_Number{type="gauges"} 98348 metrics_jvm_bufferPool_direct_capacity_Value{type="gauges"} 98348 metrics_jvm_bufferPool_direct_count_Number{type="gauges"} 41 metrics_jvm_bufferPool_direct_count_Value{type="gauges"} 41 metrics_kubernetes_client_http_response_latency_nanos_Count{type="histograms"} 26910 metrics_kubernetes_client_http_response_latency_nanos_Max{type="histograms"} 232417143 metrics_kubernetes_client_http_response_latency_nanos_Mean{type="histograms"} 1.1410164260725182E7 metrics_kubernetes_client_http_response_latency_nanos_Min{type="histograms"} 2931711 metrics_kubernetes_client_http_response_latency_nanos_50thPercentile{type="histograms"} 7559152.0 metrics_kubernetes_client_http_response_latency_nanos_75thPercentile{type="histograms"} 9440850.0 metrics_kubernetes_client_http_response_latency_nanos_95thPercentile{type="histograms"} 1.2576766E7 metrics_kubernetes_client_http_response_latency_nanos_98thPercentile{type="histograms"} 1.34034482E8 metrics_kubernetes_client_http_response_latency_nanos_99thPercentile{type="histograms"} 1.34034482E8 metrics_kubernetes_client_http_response_latency_nanos_999thPercentile{type="histograms"} 1.34034482E8 metrics_kubernetes_client_http_response_latency_nanos_StdDev{type="histograms"} 2.177784612259799E7 metrics_kubernetes_client_pods_get_Count{type="counters"} 8967 metrics_kubernetes_client_pods_get_MeanRate{type="counters"} 0.02678169644780033 metrics_kubernetes_client_pods_get_OneMinuteRate{type="counters"} 0.049758750361204154 metrics_kubernetes_client_pods_get_FiveMinuteRate{type="counters"} 0.035255140329213855 metrics_kubernetes_client_pods_get_FifteenMinuteRate{type="counters"} 0.02931221844089468 ``` with this patch, operator would be able to export format matching Prometheus 2.0 recommended practice like ``` # HELP jvm.bufferPool.direct.capacity Gauge metric # TYPE jvm.bufferPool.direct.capacity gauge jvm_bufferpool_direct_capacity 99011 # HELP jvm.bufferPool.direct.count Gauge metric # TYPE jvm.bufferPool.direct.count gauge jvm_bufferpool_direct_count 53 # HELP kubernetes_client_1xx_total Meter count # TYPE kubernetes_client_1xx_total counter kubernetes_client_1xx_total 36 # HELP kubernetes_client_http_response_latency_nanos Histogram metric # TYPE kubernetes_client_http_response_latency_nanos histogram kubernetes_client_http_response_latency_nanos_bucket{le="0.5"} 72 kubernetes_client_http_response_latency_nanos_bucket{le="0.75"} 72 kubernetes_client_http_response_latency_nanos_bucket{le="0.95"} 72 kubernetes_client_http_response_latency_nanos_bucket{le="0.98"} 72 kubernetes_client_http_response_latency_nanos_bucket{le="0.99"} 72 kubernetes_client_http_response_latency_nanos_bucket{le="+Inf"} 72 kubernetes_client_http_response_latency_nanos_count 72 kubernetes_client_http_response_latency_nanos_sum 4.656066964000001E9 ``` ### Why are the changes needed? It's Prometheus 2.0 best practice for using the next format with necessary comments. Also, some common scrapers (like Datadog) rely on these metadata (e.g. # HELP and # TYPE) to parse metrics correctly. They may skip metrics if these are missing. ### Does this PR introduce _any_ user-facing change? New functionalities becomes available (for metrics format) ### How was this patch tested? CIs / curl on :19090/prometheus to validate the format ### Was this patch authored or co-authored using generative AI tooling? No
1 parent b89c5cc commit 4d9f22f

File tree

4 files changed

+360
-7
lines changed

4 files changed

+360
-7
lines changed

docs/config_properties.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
| spark.kubernetes.operator.metrics.clientMetricsEnabled | Boolean | true | false | Enable KubernetesClient metrics for measuring the HTTP traffic to the Kubernetes API Server. Since the metrics is collected via Okhttp interceptors, can be disabled when opt in customized interceptors. |
3030
| spark.kubernetes.operator.metrics.clientMetricsGroupByResponseCodeEnabled | Boolean | true | false | When enabled, additional metrics group by http response code group(1xx, 2xx, 3xx, 4xx, 5xx) received from API server will be added. Users can disable it when their monitoring system can combine lower level kubernetes.client.http.response.<3-digit-response-code> metrics. |
3131
| spark.kubernetes.operator.metrics.port | Integer | 19090 | false | The port used for checking metrics |
32+
| spark.kubernetes.operator.enablePrometheusTextBasedFormat | Boolean | true | false | Whether or not to enable text-based format for Prometheus 2.0, as recommended by https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format |
33+
| spark.kubernetes.operator.enableSanitizePrometheusMetricsName | Boolean | true | false | Whether or not to enable automatic name sanitizing for all metrics based on best-practice guide from Prometheus https://prometheus.io/docs/practices/naming/ |
3234
| spark.kubernetes.operator.health.probePort | Integer | 19091 | false | The port used for health/readiness check probe status. |
3335
| spark.kubernetes.operator.health.sentinelExecutorPoolSize | Integer | 3 | false | Size of executor service in Sentinel Managers to check the health of sentinel resources. |
3436
| spark.kubernetes.operator.health.sentinelResourceReconciliationDelaySeconds | Integer | 60 | true | Allowed max time(seconds) between spec update and reconciliation for sentinel resources. |

spark-operator/src/main/java/org/apache/spark/k8s/operator/config/SparkOperatorConf.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,30 @@ public final class SparkOperatorConf {
334334
.defaultValue(19090)
335335
.build();
336336

337+
public static final ConfigOption<Boolean> EnablePrometheusTextBasedFormat =
338+
ConfigOption.<Boolean>builder()
339+
.key("spark.kubernetes.operator.enablePrometheusTextBasedFormat")
340+
.enableDynamicOverride(false)
341+
.description(
342+
"Whether or not to enable text-based format for Prometheus 2.0, as "
343+
+ "recommended by "
344+
+ "https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format")
345+
.typeParameterClass(Boolean.class)
346+
.defaultValue(true)
347+
.build();
348+
349+
public static final ConfigOption<Boolean> EnableSanitizePrometheusMetricsName =
350+
ConfigOption.<Boolean>builder()
351+
.key("spark.kubernetes.operator.enableSanitizePrometheusMetricsName")
352+
.enableDynamicOverride(false)
353+
.description(
354+
"Whether or not to enable automatic name sanitizing for all metrics based on "
355+
+ "best-practice guide from Prometheus "
356+
+ "https://prometheus.io/docs/practices/naming/")
357+
.typeParameterClass(Boolean.class)
358+
.defaultValue(true)
359+
.build();
360+
337361
public static final ConfigOption<Integer> OPERATOR_PROBE_PORT =
338362
ConfigOption.<Integer>builder()
339363
.key("spark.kubernetes.operator.health.probePort")

spark-operator/src/main/java/org/apache/spark/k8s/operator/metrics/PrometheusPullModelHandler.java

Lines changed: 227 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,39 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.Properties;
31+
import java.util.concurrent.TimeUnit;
3132

33+
import com.codahale.metrics.Counter;
34+
import com.codahale.metrics.Gauge;
35+
import com.codahale.metrics.Histogram;
36+
import com.codahale.metrics.Meter;
3237
import com.codahale.metrics.MetricRegistry;
38+
import com.codahale.metrics.Snapshot;
39+
import com.codahale.metrics.Timer;
3340
import com.sun.net.httpserver.HttpExchange;
3441
import com.sun.net.httpserver.HttpHandler;
3542
import jakarta.servlet.http.HttpServletRequest;
43+
import lombok.Getter;
3644
import lombok.extern.slf4j.Slf4j;
3745

46+
import org.apache.spark.k8s.operator.config.SparkOperatorConf;
3847
import org.apache.spark.metrics.sink.PrometheusServlet;
3948

4049
/** Serves as simple Prometheus sink (pull model), presenting metrics snapshot as HttpHandler. */
4150
@Slf4j
4251
public class PrometheusPullModelHandler extends PrometheusServlet implements HttpHandler {
4352
private static final String EMPTY_RECORD_VALUE = "[]";
53+
@Getter private final MetricRegistry registry;
54+
@Getter private final boolean enablePrometheusTextBasedFormat;
55+
@Getter private final boolean enableSanitizePrometheusMetricsName;
4456

4557
public PrometheusPullModelHandler(Properties properties, MetricRegistry registry) {
4658
super(properties, registry);
59+
this.registry = registry;
60+
this.enablePrometheusTextBasedFormat =
61+
SparkOperatorConf.EnablePrometheusTextBasedFormat.getValue();
62+
this.enableSanitizePrometheusMetricsName =
63+
SparkOperatorConf.EnableSanitizePrometheusMetricsName.getValue();
4764
}
4865

4966
@Override
@@ -58,13 +75,21 @@ public void stop() {
5875

5976
@Override
6077
public void handle(HttpExchange exchange) throws IOException {
61-
HttpServletRequest httpServletRequest = null;
62-
String value = getMetricsSnapshot(httpServletRequest);
63-
sendMessage(
64-
exchange,
65-
HTTP_OK,
66-
String.join("\n", filterNonEmptyRecords(value)),
67-
Map.of("Content-Type", Collections.singletonList("text/plain;version=0.0.4")));
78+
if (SparkOperatorConf.EnablePrometheusTextBasedFormat.getValue()) {
79+
sendMessage(
80+
exchange,
81+
HTTP_OK,
82+
formatMetricsSnapshot(),
83+
Map.of("Content-Type", Collections.singletonList("text/plain;version=0.0.4")));
84+
} else {
85+
HttpServletRequest httpServletRequest = null;
86+
String value = getMetricsSnapshot(httpServletRequest);
87+
sendMessage(
88+
exchange,
89+
HTTP_OK,
90+
String.join("\n", filterNonEmptyRecords(value)),
91+
Map.of("Content-Type", Collections.singletonList("text/plain;version=0.0.4")));
92+
}
6893
}
6994

7095
protected List<String> filterNonEmptyRecords(String metricsSnapshot) {
@@ -82,4 +107,199 @@ protected List<String> filterNonEmptyRecords(String metricsSnapshot) {
82107
}
83108
return filteredRecords;
84109
}
110+
111+
protected String formatMetricsSnapshot() {
112+
Map<String, Gauge> gauges = registry.getGauges();
113+
Map<String, Counter> counters = registry.getCounters();
114+
Map<String, Histogram> histograms = registry.getHistograms();
115+
Map<String, Meter> meters = registry.getMeters();
116+
Map<String, Timer> timers = registry.getTimers();
117+
118+
StringBuilder stringBuilder = new StringBuilder();
119+
120+
for (Map.Entry<String, Gauge> entry : gauges.entrySet()) {
121+
stringBuilder.append(formatGauge(entry.getKey(), entry.getValue()));
122+
}
123+
124+
// Counters
125+
for (Map.Entry<String, Counter> entry : counters.entrySet()) {
126+
String name = sanitize(entry.getKey()) + "_total";
127+
Counter counter = entry.getValue();
128+
stringBuilder.append(formatCounter(name, counter));
129+
}
130+
131+
// Histograms
132+
for (Map.Entry<String, Histogram> entry : histograms.entrySet()) {
133+
stringBuilder.append(formatHistogram(entry.getKey(), entry.getValue()));
134+
}
135+
136+
// Meters
137+
for (Map.Entry<String, Meter> entry : meters.entrySet()) {
138+
stringBuilder.append(formatMeter(entry.getKey(), entry.getValue()));
139+
}
140+
141+
// Timers (Meter + Histogram in nanoseconds)
142+
for (Map.Entry<String, Timer> entry : timers.entrySet()) {
143+
stringBuilder.append(formatTimer(entry.getKey(), entry.getValue()));
144+
}
145+
return stringBuilder.toString();
146+
}
147+
148+
protected String formatGauge(String name, Gauge gauge) {
149+
if (gauge != null
150+
&& gauge.getValue() != null
151+
&& !EMPTY_RECORD_VALUE.equals(gauge.getValue())
152+
&& gauge.getValue() instanceof Number) {
153+
return "# HELP "
154+
+ name
155+
+ " Gauge metric\n"
156+
+ "# TYPE "
157+
+ name
158+
+ " gauge\n"
159+
+ sanitize(name)
160+
+ ' '
161+
+ gauge.getValue()
162+
+ "\n\n";
163+
}
164+
return null;
165+
}
166+
167+
protected String formatCounter(String name, Counter counter) {
168+
if (counter != null) {
169+
return "# HELP "
170+
+ name
171+
+ " Counter metric\n"
172+
+ "# TYPE "
173+
+ name
174+
+ " counter\n"
175+
+ name
176+
+ " "
177+
+ counter.getCount()
178+
+ "\n\n";
179+
}
180+
return null;
181+
}
182+
183+
protected String formatHistogram(String name, Histogram histogram) {
184+
if (histogram != null && histogram.getSnapshot() != null) {
185+
StringBuilder stringBuilder = new StringBuilder(300);
186+
String baseName = sanitize(name);
187+
Snapshot snap = histogram.getSnapshot();
188+
long count = histogram.getCount();
189+
stringBuilder
190+
.append("# HELP ")
191+
.append(baseName)
192+
.append(" Histogram metric\n# TYPE ")
193+
.append(baseName)
194+
.append(" histogram\n");
195+
196+
appendBucket(stringBuilder, baseName, "le=\"0.5\"", count); // approximated
197+
appendBucket(stringBuilder, baseName, "le=\"0.75\"", count);
198+
appendBucket(stringBuilder, baseName, "le=\"0.95\"", count);
199+
appendBucket(stringBuilder, baseName, "le=\"0.98\"", count);
200+
appendBucket(stringBuilder, baseName, "le=\"0.99\"", count);
201+
appendBucket(stringBuilder, baseName, "le=\"+Inf\"", count);
202+
stringBuilder
203+
.append(baseName)
204+
.append("_count ")
205+
.append(count)
206+
.append('\n')
207+
.append(baseName)
208+
.append("_sum ")
209+
.append(snap.getMean() * count)
210+
.append("\n\n");
211+
return stringBuilder.toString();
212+
}
213+
return null;
214+
}
215+
216+
protected String formatMeter(String name, Meter meter) {
217+
if (meter != null) {
218+
StringBuilder stringBuilder = new StringBuilder(200);
219+
String baseName = sanitize(name);
220+
stringBuilder
221+
.append("# HELP ")
222+
.append(baseName)
223+
.append("_total Meter count\n# TYPE ")
224+
.append(baseName)
225+
.append("_total counter\n")
226+
.append(baseName)
227+
.append("_total ")
228+
.append(meter.getCount())
229+
.append("\n\n# TYPE ")
230+
.append(baseName)
231+
.append("_rate gauge\n")
232+
.append(baseName)
233+
.append("_rate{interval=\"1m\"} ")
234+
.append(meter.getOneMinuteRate())
235+
.append('\n')
236+
.append(baseName)
237+
.append("_rate{interval=\"5m\"} ")
238+
.append(meter.getFiveMinuteRate())
239+
.append('\n')
240+
.append(baseName)
241+
.append("_rate{interval=\"15m\"} ")
242+
.append(meter.getFifteenMinuteRate())
243+
.append("\n\n");
244+
return stringBuilder.toString();
245+
}
246+
return null;
247+
}
248+
249+
protected String formatTimer(String name, Timer timer) {
250+
if (timer != null && timer.getSnapshot() != null) {
251+
StringBuilder stringBuilder = new StringBuilder(300);
252+
String baseName = sanitize(name);
253+
Snapshot snap = timer.getSnapshot();
254+
long count = timer.getCount();
255+
stringBuilder
256+
.append("# HELP ")
257+
.append(baseName)
258+
.append("_duration_seconds Timer histogram\n# TYPE ")
259+
.append(baseName)
260+
.append("_duration_seconds histogram\n");
261+
appendBucket(stringBuilder, baseName + "_duration_seconds", "le=\"0.5\"", count);
262+
appendBucket(stringBuilder, baseName + "_duration_seconds", "le=\"0.75\"", count);
263+
appendBucket(stringBuilder, baseName + "_duration_seconds", "le=\"0.95\"", count);
264+
appendBucket(stringBuilder, baseName + "_duration_seconds", "le=\"+Inf\"", count);
265+
stringBuilder
266+
.append(baseName)
267+
.append("_duration_seconds_count ")
268+
.append(count)
269+
.append('\n')
270+
.append(baseName)
271+
.append("_duration_seconds_sum ")
272+
.append(nanosToSeconds(snap.getMean() * count))
273+
.append("\n\n# TYPE ")
274+
.append(baseName)
275+
.append("_calls_total counter\n")
276+
.append(baseName)
277+
.append("_calls_total ")
278+
.append(count)
279+
.append("\n\n");
280+
return stringBuilder.toString();
281+
}
282+
return null;
283+
}
284+
285+
protected void appendBucket(StringBuilder builder, String baseName, String leLabel, long value) {
286+
builder
287+
.append(baseName)
288+
.append("_bucket{")
289+
.append(leLabel)
290+
.append("} ")
291+
.append(value)
292+
.append('\n');
293+
}
294+
295+
protected double nanosToSeconds(double nanos) {
296+
return TimeUnit.SECONDS.convert((long) nanos, TimeUnit.NANOSECONDS);
297+
}
298+
299+
protected String sanitize(String name) {
300+
if (enableSanitizePrometheusMetricsName) {
301+
return name.replaceAll("[^a-zA-Z0-9_:]", "_").toLowerCase();
302+
}
303+
return name;
304+
}
85305
}

0 commit comments

Comments
 (0)