diff --git a/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesResponseListener.java b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesResponseListener.java
new file mode 100644
index 0000000000000..faab4b5468670
--- /dev/null
+++ b/x-pack/plugin/prometheus/src/main/java/org/elasticsearch/xpack/prometheus/rest/PrometheusLabelValuesResponseListener.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.prometheus.rest;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestResponse;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.json.JsonXContent;
+import org.elasticsearch.xpack.esql.action.EsqlQueryResponse;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts an {@link EsqlQueryResponse} into the
+ * Prometheus {@code /api/v1/label/{name}/values} JSON response format.
+ *
+ *
ESQL has already sorted and deduplicated the values via {@code STATS BY} and {@code ORDER BY}.
+ * This listener iterates the rows and strips storage prefixes before returning values:
+ *
+ *
{@code labels.} prefix — ESQL resolves the passthrough field to its stored path
+ * {@code labels.job} for Prometheus data; OTel stores without prefix so no stripping needed.
+ *
{@code metrics.} prefix — the {@code metric_name} column returned by MetricsInfo for the
+ * {@code __name__} plan branch carries the raw ES field path, e.g. {@code metrics.up}.
+ *
+ *
+ *
Truncation detection uses a {@code limit + 1} sentinel: if the result contains exactly
+ * {@code limit + 1} rows, the last row is dropped and a warning is emitted.
+ *
+ *
When ESQL returns a {@code "Unknown column"} BAD_REQUEST error (label name absent from all
+ * index mappings), the listener converts it into an empty {@code data:[]} success response — the
+ * correct Prometheus behaviour for a label with no values.
+ */
+public class PrometheusLabelValuesResponseListener {
+
+ private static final Logger logger = LogManager.getLogger(PrometheusLabelValuesResponseListener.class);
+
+ private static final String LABELS_PREFIX = "labels.";
+ private static final String METRICS_PREFIX = "metrics.";
+ private static final String CONTENT_TYPE = "application/json";
+
+ private PrometheusLabelValuesResponseListener() {}
+
+ public static ActionListener create(RestChannel channel, int limit) {
+ // Do NOT close/decRef the response here: the framework (via respondAndRelease) calls
+ // decRef() after this method returns, which is the correct single release.
+ return ActionListener.wrap(ignored -> {}, e -> {
+ logger.debug("Label values query failed", e);
+ // When ESQL cannot find the label name in any index mapping it throws a BAD_REQUEST
+ // "Unknown column []" error. The correct Prometheus response for a label that has
+ // no values is an empty data array, not an error.
+ if (isUnknownColumn(e)) {
+ try {
+ channel.sendResponse(buildSuccessResponse(List.of(), 0));
+ } catch (Exception sendEx) {
+ sendEx.addSuppressed(e);
+ logger.warn("Failed to send empty-data response for unknown column", sendEx);
+ }
+ } else {
+ PrometheusErrorResponse.send(channel, e, logger);
+ }
+ }).delegateFailureAndWrap((l, response) -> {
+ List values = collectValues(response.rows());
+ channel.sendResponse(buildSuccessResponse(values, limit));
+ });
+ }
+
+ /**
+ * Collects label values from the single-column ESQL result rows, stripping storage prefixes:
+ * {@code "labels."} (Prometheus passthrough field) and {@code "metrics."} (raw ES field path
+ * returned by MetricsInfo for the {@code __name__} plan branch). OTel values carry neither
+ * prefix and are returned as-is. Package-private for testing.
+ */
+ static List collectValues(Iterable extends Iterable