This is an automated email from the ASF dual-hosted git repository.

tuglu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 4b624b2cb47 Emit status code tag in (sqlQuery|query)/time metrics 
(#18631)
4b624b2cb47 is described below

commit 4b624b2cb47071d20dc4d0fd03bc027551f56b92
Author: jtuglu1 <[email protected]>
AuthorDate: Tue Oct 14 18:35:30 2025 -0700

    Emit status code tag in (sqlQuery|query)/time metrics (#18631)
    
    This adds a code dimension to the query/time and sqlQuery/time metrics 
which reports the result status code of the query. This is more verbose/helpful 
than success true/false and allows you to do quick aggregations filtering on 
code.
---
 docs/operations/metrics.md                         |  4 +-
 .../apache/druid/query/DefaultQueryMetrics.java    |  7 ++
 .../java/org/apache/druid/query/DruidMetrics.java  | 26 ++++++
 .../java/org/apache/druid/query/QueryMetrics.java  |  7 ++
 .../query/search/DefaultSearchQueryMetrics.java    |  8 ++
 .../org/apache/druid/query/DruidMetricsTest.java   | 48 +++++++++++
 .../org/apache/druid/server/QueryLifecycle.java    |  2 +
 .../org/apache/druid/server/QueryResourceTest.java | 98 +++++++++++++++++++---
 .../druid/server/AsyncQueryForwardingServlet.java  |  8 +-
 .../server/AsyncQueryForwardingServletTest.java    |  7 +-
 .../org/apache/druid/sql/SqlExecutionReporter.java |  6 ++
 .../org/apache/druid/sql/http/SqlResourceTest.java | 31 +++++++
 12 files changed, 236 insertions(+), 16 deletions(-)

diff --git a/docs/operations/metrics.md b/docs/operations/metrics.md
index 55a6dd449b3..f5fdcecccd3 100644
--- a/docs/operations/metrics.md
+++ b/docs/operations/metrics.md
@@ -45,7 +45,7 @@ Most metric values reset each emission period, as specified 
in `druid.monitoring
 
 |Metric|Description|Dimensions|Normal value|
 |------|-----------|----------|------------|
-|`query/time`|Milliseconds taken to complete a query.|Native Query: 
`dataSource`, `type`, `interval`, `hasFilters`, `duration`, `context`, 
`remoteAddress`, `id`.|< 1s|
+|`query/time`|Milliseconds taken to complete a query.|Native Query: 
`dataSource`, `type`, `interval`, `hasFilters`, `duration`, `context`, 
`remoteAddress`, `id`, `code`.|< 1s|
 
 ### Broker
 
@@ -64,7 +64,7 @@ Most metric values reset each emission period, as specified 
in `druid.monitoring
 |`query/timeout/count`|Number of timed out queries.|This metric is only 
available if the `QueryCountStatsMonitor` module is included.| |
 |`query/segments/count`|This metric is not enabled by default. See the 
`QueryMetrics` Interface for reference regarding enabling this metric. Number 
of segments that will be touched by the query. In the broker, it makes a plan 
to distribute the query to realtime tasks and historicals based on a snapshot 
of segment distribution state. If there are some segments moved after this 
snapshot is created, certain historicals and realtime tasks can report those 
segments as missing to the broker.  [...]
 |`query/priority`|Assigned lane and priority, only if Laning strategy is 
enabled. Refer to [Laning 
strategies](../configuration/index.md#laning-strategies)|`lane`, `dataSource`, 
`type`|0|
-|`sqlQuery/time`|Milliseconds taken to complete a SQL query.|`id`, 
`nativeQueryIds`, `dataSource`, `remoteAddress`, `success`, `engine`|< 1s|
+|`sqlQuery/time`|Milliseconds taken to complete a SQL query.|`id`, 
`nativeQueryIds`, `dataSource`, `remoteAddress`, `success`, `engine`, `code`|< 
1s|
 |`sqlQuery/planningTimeMs`|Milliseconds taken to plan a SQL to native 
query.|`id`, `nativeQueryIds`, `dataSource`, `remoteAddress`, `success`, 
`engine`| |
 |`sqlQuery/bytes`|Number of bytes returned in the SQL query response.|`id`, 
`nativeQueryIds`, `dataSource`, `remoteAddress`, `success`, `engine`| |
 |`serverview/init/time`|Time taken to initialize the broker server view. 
Useful to detect if brokers are taking too long to start.||Depends on the 
number of segments.|
diff --git 
a/processing/src/main/java/org/apache/druid/query/DefaultQueryMetrics.java 
b/processing/src/main/java/org/apache/druid/query/DefaultQueryMetrics.java
index 9aab67e9f1a..624a6f783fe 100644
--- a/processing/src/main/java/org/apache/druid/query/DefaultQueryMetrics.java
+++ b/processing/src/main/java/org/apache/druid/query/DefaultQueryMetrics.java
@@ -27,6 +27,7 @@ import 
org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
 import org.joda.time.Interval;
 
+import javax.annotation.Nullable;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
@@ -193,6 +194,12 @@ public class DefaultQueryMetrics<QueryType extends 
Query<?>> implements QueryMet
     setDimension("success", String.valueOf(success));
   }
 
+  @Override
+  public void code(@Nullable Throwable error)
+  {
+    setDimension(DruidMetrics.CODE, DruidMetrics.computeStatusCode(error));
+  }
+
   @Override
   public void segment(String segmentIdentifier)
   {
diff --git a/processing/src/main/java/org/apache/druid/query/DruidMetrics.java 
b/processing/src/main/java/org/apache/druid/query/DruidMetrics.java
index 923eb11656f..61c4733a64e 100644
--- a/processing/src/main/java/org/apache/druid/query/DruidMetrics.java
+++ b/processing/src/main/java/org/apache/druid/query/DruidMetrics.java
@@ -19,9 +19,11 @@
 
 package org.apache.druid.query;
 
+import org.apache.druid.error.DruidException;
 import org.apache.druid.query.aggregation.AggregatorFactory;
 import org.apache.druid.segment.column.ValueType;
 
+import javax.annotation.Nullable;
 import java.util.List;
 
 /**
@@ -35,6 +37,7 @@ public class DruidMetrics
   public static final String INTERVAL = "interval";
   public static final String ID = "id";
   public static final String SUBQUERY_ID = "subQueryId";
+  public static final String CODE = "code";
   public static final String STATUS = "status";
   public static final String ENGINE = "engine";
   public static final String DURATION = "duration";
@@ -87,4 +90,27 @@ public class DruidMetrics
     queryMetrics.remoteAddress(remoteAddr);
     return queryMetrics;
   }
+
+  /**
+   * Computes the HTTP status code based on the query error (if any) for 
tagged metric emission.
+   * <ul>
+   *   <li>If error is null: returns 200 (success)</li>
+   *   <li>If error is a DruidException: returns the category's expected HTTP 
status</li>
+   *   <li>Otherwise (unclassified error): returns 500 (internal server 
error)</li>
+   * </ul>
+   *
+   * @param error The throwable error, or null if successful
+   * @return HTTP status code appropriate for the error
+   */
+  public static int computeStatusCode(@Nullable Throwable error)
+  {
+    if (error == null) {
+      return 200;
+    }
+    if (error instanceof DruidException) {
+      return ((DruidException) error).getCategory().getExpectedStatus();
+    }
+    // Unclassified errors default to 500 (defensive)
+    return DruidException.Category.DEFENSIVE.getExpectedStatus();
+  }
 }
diff --git a/processing/src/main/java/org/apache/druid/query/QueryMetrics.java 
b/processing/src/main/java/org/apache/druid/query/QueryMetrics.java
index 284da48c42b..403420b4b04 100644
--- a/processing/src/main/java/org/apache/druid/query/QueryMetrics.java
+++ b/processing/src/main/java/org/apache/druid/query/QueryMetrics.java
@@ -27,6 +27,7 @@ import org.apache.druid.query.filter.Filter;
 import org.apache.druid.query.filter.FilterBundle;
 import org.apache.druid.query.search.SearchQueryMetricsFactory;
 
+import javax.annotation.Nullable;
 import java.util.List;
 
 /**
@@ -241,6 +242,12 @@ public interface QueryMetrics<QueryType extends Query<?>>
 
   void success(boolean success);
 
+  /**
+   * Translates the given query exception into the appropriate failure status 
code.
+   * See {@link DruidMetrics#computeStatusCode}.
+   */
+  void code(@Nullable Throwable error);
+
   void segment(String segmentIdentifier);
 
   /**
diff --git 
a/processing/src/main/java/org/apache/druid/query/search/DefaultSearchQueryMetrics.java
 
b/processing/src/main/java/org/apache/druid/query/search/DefaultSearchQueryMetrics.java
index 85696d69ef9..9a694824c3b 100644
--- 
a/processing/src/main/java/org/apache/druid/query/search/DefaultSearchQueryMetrics.java
+++ 
b/processing/src/main/java/org/apache/druid/query/search/DefaultSearchQueryMetrics.java
@@ -27,6 +27,8 @@ import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryMetrics;
 import org.apache.druid.query.filter.FilterBundle;
 
+import javax.annotation.Nullable;
+
 /**
  * This class is implemented with delegation to another QueryMetrics for 
compatibility, see "Making subinterfaces of
  * QueryMetrics for emitting custom dimensions and/or metrics for specific 
query types" section in {@link QueryMetrics}
@@ -145,6 +147,12 @@ public class DefaultSearchQueryMetrics implements 
SearchQueryMetrics
     delegateQueryMetrics.success(success);
   }
 
+  @Override
+  public void code(@Nullable Throwable error)
+  {
+    delegateQueryMetrics.code(error);
+  }
+
   @Override
   public void segment(String segmentIdentifier)
   {
diff --git 
a/processing/src/test/java/org/apache/druid/query/DruidMetricsTest.java 
b/processing/src/test/java/org/apache/druid/query/DruidMetricsTest.java
new file mode 100644
index 00000000000..0ee2ec9beb6
--- /dev/null
+++ b/processing/src/test/java/org/apache/druid/query/DruidMetricsTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.apache.druid.query;
+
+import org.apache.druid.error.DruidException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DruidMetricsTest
+{
+  @Test
+  public void testComputeStatusCode_nullError()
+  {
+    Assert.assertEquals(200, DruidMetrics.computeStatusCode(null));
+  }
+
+  @Test
+  public void testComputeStatusCode_allDruidExceptionCategories()
+  {
+    for (DruidException.Category cat : DruidException.Category.values()) {
+      Assert.assertEquals(
+          cat.getExpectedStatus(), DruidMetrics.computeStatusCode(
+              DruidException.forPersona(DruidException.Persona.USER)
+                            .ofCategory(cat)
+                            .build("test")
+          )
+      );
+    }
+  }
+}
+
diff --git a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java 
b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
index b36a1167752..8fb3381f16f 100644
--- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
+++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java
@@ -401,6 +401,7 @@ public class QueryLifecycle
           StringUtils.nullToEmptyNonDruidDataString(remoteAddress)
       );
       queryMetrics.success(success);
+      queryMetrics.code(e);
       queryMetrics.reportQueryTime(queryTimeNs);
 
       if (bytesWritten >= 0) {
@@ -417,6 +418,7 @@ public class QueryLifecycle
       statsMap.put("query/time", TimeUnit.NANOSECONDS.toMillis(queryTimeNs));
       statsMap.put("query/bytes", bytesWritten);
       statsMap.put("success", success);
+      statsMap.put(DruidMetrics.CODE, DruidMetrics.computeStatusCode(e));
 
       if (authenticationResult != null) {
         statsMap.put("identity", authenticationResult.getIdentity());
diff --git 
a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java 
b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
index 16fcf562ccd..1f116bc3963 100644
--- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java
@@ -52,10 +52,13 @@ import org.apache.druid.java.util.common.guava.Yielders;
 import org.apache.druid.java.util.common.guava.YieldingAccumulator;
 import org.apache.druid.java.util.emitter.EmittingLogger;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
+import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
+import org.apache.druid.java.util.metrics.StubServiceEmitter;
 import org.apache.druid.query.BadJsonQueryException;
 import org.apache.druid.query.DefaultGenericQueryMetricsFactory;
 import org.apache.druid.query.DefaultQueryConfig;
 import org.apache.druid.query.DefaultQueryRunnerFactoryConglomerate;
+import org.apache.druid.query.DruidMetrics;
 import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryCapacityExceededException;
 import org.apache.druid.query.QueryException;
@@ -126,6 +129,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 public class QueryResourceTest
 {
@@ -233,6 +237,7 @@ public class QueryResourceTest
   private QueryResource queryResource;
   private QueryScheduler queryScheduler;
   private TestRequestLogger testRequestLogger;
+  private StubServiceEmitter emitter;
 
   @BeforeClass
   public static void staticSetup()
@@ -253,6 +258,7 @@ public class QueryResourceTest
 
     queryScheduler = QueryStackTests.DEFAULT_NOOP_SCHEDULER;
     testRequestLogger = new TestRequestLogger();
+    emitter = new StubServiceEmitter();
     queryResource = createQueryResource(ResponseContextConfig.newConfig(true));
   }
 
@@ -263,7 +269,7 @@ public class QueryResourceTest
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -312,7 +318,7 @@ public class QueryResourceTest
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -392,7 +398,7 @@ public class QueryResourceTest
               }
             },
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -417,6 +423,8 @@ public class QueryResourceTest
 
     final Response response = 
expectSynchronousRequestFlow(SIMPLE_TIMESERIES_QUERY);
     Assert.assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), 
response.getStatus());
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(500, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
 
     final ErrorResponse entity = (ErrorResponse) response.getEntity();
     MatcherAssert.assertThat(
@@ -490,7 +498,7 @@ public class QueryResourceTest
               }
             },
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -524,6 +532,9 @@ public class QueryResourceTest
 
     
Assert.assertTrue(fields.containsKey(QueryResource.RESPONSE_COMPLETE_TRAILER_HEADER));
     
Assert.assertEquals(fields.get(QueryResource.RESPONSE_COMPLETE_TRAILER_HEADER), 
"false");
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(504, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -572,7 +583,7 @@ public class QueryResourceTest
               }
             },
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -604,6 +615,8 @@ public class QueryResourceTest
         + "\"errorMessage\":\"mid-flight exception\",\"context\":{}}]",
         actualOutput
     );
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(400, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -668,7 +681,7 @@ public class QueryResourceTest
               }
             },
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -705,6 +718,9 @@ public class QueryResourceTest
 
     
Assert.assertTrue(fields.containsKey(QueryResource.RESPONSE_COMPLETE_TRAILER_HEADER));
     Assert.assertEquals("true", 
fields.get(QueryResource.RESPONSE_COMPLETE_TRAILER_HEADER));
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
 
@@ -748,7 +764,7 @@ public class QueryResourceTest
                 CONGLOMERATE,
                 querySegmentWalker,
                 new DefaultGenericQueryMetricsFactory(),
-                new NoopServiceEmitter(),
+                emitter,
                 testRequestLogger,
                 AuthTestUtils.TEST_AUTHORIZER_MAPPER,
                 overrideConfig,
@@ -761,6 +777,7 @@ public class QueryResourceTest
               @Override
               public void emitLogsAndMetrics(@Nullable Throwable e, @Nullable 
String remoteAddress, long bytesWritten)
               {
+                super.emitLogsAndMetrics(e, remoteAddress, bytesWritten);
                 
Assert.assertTrue(Throwables.getStackTraceAsString(e).contains(embeddedExceptionMessage));
               }
             };
@@ -794,6 +811,8 @@ public class QueryResourceTest
         )
             .expectMessageIs("something")
     );
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(500, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -807,7 +826,7 @@ public class QueryResourceTest
             CONGLOMERATE,
             TEST_SEGMENT_WALKER,
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -854,6 +873,8 @@ public class QueryResourceTest
         -1,
         
testRequestLogger.getNativeQuerylogs().get(0).getQuery().getContext().get(overrideConfigKey)
     );
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -878,6 +899,8 @@ public class QueryResourceTest
         expectedException,
         jsonMapper.readValue(response.baos.toByteArray(), 
QueryInterruptedException.class).toString()
     );
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(500, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -892,6 +915,10 @@ public class QueryResourceTest
         queryResource
     );
     Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(1, queryResource.getSuccessfulQueryCount());
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -904,6 +931,10 @@ public class QueryResourceTest
     Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
     //since accept header is null, the response content type should be same as 
the value of 'Content-Type' header
     Assert.assertEquals(MediaType.APPLICATION_JSON, response.getContentType());
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(1, queryResource.getSuccessfulQueryCount());
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -917,6 +948,10 @@ public class QueryResourceTest
     Assert.assertEquals(HttpStatus.SC_OK, response.getStatus());
     //since accept header is empty, the response content type should be same 
as the value of 'Content-Type' header
     Assert.assertEquals(MediaType.APPLICATION_JSON, response.getContentType());
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(1, queryResource.getSuccessfulQueryCount());
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -932,6 +967,10 @@ public class QueryResourceTest
 
     // Content-Type in response should be Smile
     Assert.assertEquals(SmileMediaTypes.APPLICATION_JACKSON_SMILE, 
response.getContentType());
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(1, queryResource.getSuccessfulQueryCount());
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -952,6 +991,10 @@ public class QueryResourceTest
 
     // Content-Type in response should be Smile
     Assert.assertEquals(SmileMediaTypes.APPLICATION_JACKSON_SMILE, 
response.getContentType());
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(1, queryResource.getSuccessfulQueryCount());
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -971,6 +1014,10 @@ public class QueryResourceTest
 
     // Content-Type in response should default to Content-Type from request
     Assert.assertEquals(SmileMediaTypes.APPLICATION_JACKSON_SMILE, 
response.getContentType());
+
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(1, queryResource.getSuccessfulQueryCount());
+    Assert.assertEquals(200, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -1131,7 +1178,7 @@ public class QueryResourceTest
             CONGLOMERATE,
             timeoutSegmentWalker,
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
@@ -1180,6 +1227,8 @@ public class QueryResourceTest
     Assert.assertEquals(QueryException.QUERY_TIMEOUT_ERROR_CODE, 
ex.getErrorCode());
     Assert.assertEquals(1, timeoutQueryResource.getTimedOutQueryCount());
 
+    emitter.verifyEmitted("query/time", 1);
+    Assert.assertEquals(504, 
emitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test(timeout = 60_000L)
@@ -1480,6 +1529,16 @@ public class QueryResourceTest
     }
     Assert.assertEquals(2, queryResource.getSuccessfulQueryCount());
     Assert.assertEquals(1, queryResource.getFailedQueryCount());
+
+    emitter.verifyEmitted("query/time", 3);
+    Map<Integer, Long> codeFrequencies = 
emitter.getMetricEvents("query/time").stream()
+                                                .map(ServiceMetricEvent::toMap)
+                                                .map(map -> (int) 
map.get(DruidMetrics.CODE))
+                                                .collect(Collectors.groupingBy(
+                                                    code -> code,
+                                                    Collectors.counting()
+                                                ));
+    Assert.assertEquals(Map.of(200, 2L, 429, 1L), codeFrequencies);
   }
 
   @Test(timeout = 10_000L)
@@ -1550,6 +1609,16 @@ public class QueryResourceTest
     for (Future<Boolean> theFuture : back2) {
       Assert.assertTrue(theFuture.get());
     }
+
+    emitter.verifyEmitted("query/time", 3);
+    Map<Integer, Long> codeFrequencies = 
emitter.getMetricEvents("query/time").stream()
+                                                .map(ServiceMetricEvent::toMap)
+                                                .map(map -> (int) 
map.get(DruidMetrics.CODE))
+                                                .collect(Collectors.groupingBy(
+                                                    code -> code,
+                                                    Collectors.counting()
+                                                ));
+    Assert.assertEquals(Map.of(200, 2L, 429, 1L), codeFrequencies);
   }
 
   @Test(timeout = 10_000L)
@@ -1618,6 +1687,15 @@ public class QueryResourceTest
     for (Future<Boolean> theFuture : back2) {
       Assert.assertTrue(theFuture.get());
     }
+    emitter.verifyEmitted("query/time", 3);
+    Map<Integer, Long> codeFrequencies = 
emitter.getMetricEvents("query/time").stream()
+                                                .map(ServiceMetricEvent::toMap)
+                                                .map(map -> (int) 
map.get(DruidMetrics.CODE))
+                                                .collect(Collectors.groupingBy(
+                                                    code -> code,
+                                                    Collectors.counting()
+                                                ));
+    Assert.assertEquals(Map.of(200, 2L, 429, 1L), codeFrequencies);
   }
 
   @Test
@@ -1706,7 +1784,7 @@ public class QueryResourceTest
             CONGLOMERATE,
             texasRanger,
             new DefaultGenericQueryMetricsFactory(),
-            new NoopServiceEmitter(),
+            emitter,
             testRequestLogger,
             new AuthConfig(),
             NoopPolicyEnforcer.instance(),
diff --git 
a/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
 
b/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
index 0fa20e24227..829f973fdac 100644
--- 
a/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
+++ 
b/services/src/main/java/org/apache/druid/server/AsyncQueryForwardingServlet.java
@@ -766,7 +766,7 @@ public class AsyncQueryForwardingServlet extends 
AsyncProxyServlet implements Qu
       } else {
         failedQueryCount.incrementAndGet();
       }
-      emitQueryTime(requestTimeNs, success, sqlQueryId, queryId);
+      emitQueryTime(requestTimeNs, success, sqlQueryId, queryId, 
result.getFailure());
 
       AuthenticationResult authenticationResult = 
AuthorizationUtils.authenticationResultFromRequest(req);
 
@@ -853,7 +853,7 @@ public class AsyncQueryForwardingServlet extends 
AsyncProxyServlet implements Qu
       }
 
       failedQueryCount.incrementAndGet();
-      emitQueryTime(requestTimeNs, false, sqlQueryId, queryId);
+      emitQueryTime(requestTimeNs, false, sqlQueryId, queryId, failure);
       AuthenticationResult authenticationResult = 
AuthorizationUtils.authenticationResultFromRequest(req);
 
       //noinspection VariableNotUsedInsideIf
@@ -929,7 +929,8 @@ public class AsyncQueryForwardingServlet extends 
AsyncProxyServlet implements Qu
         long requestTimeNs,
         boolean success,
         @Nullable String sqlQueryId,
-        @Nullable String queryId
+        @Nullable String queryId,
+        Throwable queryException
     )
     {
       QueryMetrics queryMetrics;
@@ -950,6 +951,7 @@ public class AsyncQueryForwardingServlet extends 
AsyncProxyServlet implements Qu
         );
       }
       queryMetrics.success(success);
+      queryMetrics.code(queryException);
       queryMetrics.reportQueryTime(requestTimeNs).emit(emitter);
     }
   }
diff --git 
a/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
 
b/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
index 2e6e2b3f593..2bd6c5107a3 100644
--- 
a/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
+++ 
b/services/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java
@@ -51,6 +51,7 @@ import org.apache.druid.java.util.common.jackson.JacksonUtils;
 import org.apache.druid.java.util.common.lifecycle.Lifecycle;
 import org.apache.druid.java.util.metrics.StubServiceEmitter;
 import org.apache.druid.query.DefaultGenericQueryMetricsFactory;
+import org.apache.druid.query.DruidMetrics;
 import org.apache.druid.query.Druids;
 import org.apache.druid.query.MapQueryToolChestWarehouse;
 import org.apache.druid.query.Query;
@@ -719,11 +720,15 @@ public class AsyncQueryForwardingServletTest extends 
BaseJettyTest
     }
     catch (NullPointerException ignored) {
     }
-    // Assert.assertEquals("query/time", 
stubServiceEmitter.getEvents().get(0).toMap().get("metric"));
     stubServiceEmitter.verifyEmitted("query/time", 1);
     if (!isJDBCSql) {
       Assert.assertEquals("dummy", 
stubServiceEmitter.getEvents().get(0).toMap().get("id"));
     }
+    if (isFailure) {
+      Assert.assertEquals(500, 
stubServiceEmitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
+    } else {
+      Assert.assertEquals(200, 
stubServiceEmitter.getMetricEvents("query/time").get(0).toMap().get(DruidMetrics.CODE));
+    }
 
     // This test is mostly about verifying that the servlet calls the right 
methods the right number of times.
     EasyMock.verify(hostFinder, requestMock);
diff --git a/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java 
b/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java
index f531144b5f0..74a52345f8a 100644
--- a/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java
+++ b/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java
@@ -24,6 +24,7 @@ import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.java.util.emitter.service.ServiceEmitter;
 import org.apache.druid.java.util.emitter.service.ServiceMetricEvent;
+import org.apache.druid.query.DruidMetrics;
 import org.apache.druid.query.QueryInterruptedException;
 import org.apache.druid.query.QueryTimeoutException;
 import org.apache.druid.server.QueryStats;
@@ -112,6 +113,10 @@ public class SqlExecutionReporter
       }
       metricBuilder.setDimension("remoteAddress", 
StringUtils.nullToEmptyNonDruidDataString(remoteAddress));
       metricBuilder.setDimension("success", String.valueOf(success));
+
+      final int statusCode = DruidMetrics.computeStatusCode(e);
+      metricBuilder.setDimension(DruidMetrics.CODE, statusCode);
+
       emitter.emit(metricBuilder.setMetric("sqlQuery/time", 
TimeUnit.NANOSECONDS.toMillis(queryTimeNs)));
       if (bytesWritten >= 0) {
         emitter.emit(metricBuilder.setMetric("sqlQuery/bytes", bytesWritten));
@@ -128,6 +133,7 @@ public class SqlExecutionReporter
       statsMap.put("sqlQuery/planningTimeMs", 
TimeUnit.NANOSECONDS.toMillis(planningTimeNanos));
       statsMap.put("sqlQuery/bytes", bytesWritten);
       statsMap.put("success", success);
+      statsMap.put(DruidMetrics.CODE, statusCode);
       Map<String, Object> queryContext = stmt.queryContext;
       if (plannerContext != null) {
         statsMap.put("identity", 
plannerContext.getAuthenticationResult().getIdentity());
diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java 
b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
index 799cf1cc79f..a808d0e4633 100644
--- a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
@@ -54,6 +54,7 @@ import org.apache.druid.math.expr.ExprMacroTable;
 import org.apache.druid.query.BadQueryContextException;
 import org.apache.druid.query.BaseQuery;
 import org.apache.druid.query.DefaultQueryConfig;
+import org.apache.druid.query.DruidMetrics;
 import org.apache.druid.query.Query;
 import org.apache.druid.query.QueryCapacityExceededException;
 import org.apache.druid.query.QueryContexts;
@@ -404,6 +405,8 @@ public class SqlResourceTest extends CalciteTestBase
     );
     checkSqlRequestLog(true);
     Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(200, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -491,6 +494,7 @@ public class SqlResourceTest extends CalciteTestBase
     stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
     stubServiceEmitter.verifyValue("sqlQuery/bytes", 27L);
     stubServiceEmitter.verifyEmitted("sqlQuery/planningTimeMs", 1);
+    Assert.assertEquals(200, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
 
@@ -1542,6 +1546,8 @@ public class SqlResourceTest extends CalciteTestBase
     );
     checkSqlRequestLog(false);
     Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(400, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -1563,6 +1569,8 @@ public class SqlResourceTest extends CalciteTestBase
     );
     checkSqlRequestLog(false);
     Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(400, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   /**
@@ -1585,6 +1593,8 @@ public class SqlResourceTest extends CalciteTestBase
     );
     checkSqlRequestLog(false);
     Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(400, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -1776,6 +1786,8 @@ public class SqlResourceTest extends CalciteTestBase
             .expectMessageIs("Calcite assertion violated: [not a literal: 
assertion_error()]")
     );
     Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(400, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -1872,6 +1884,15 @@ public class SqlResourceTest extends CalciteTestBase
     Assert.assertEquals(1, limited);
     Assert.assertEquals(3, testRequestLogger.getSqlQueryLogs().size());
     Assert.assertTrue(lifecycleManager.getAll(sqlQueryId).isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 3);
+    Map<Integer, Long> codeFrequencies = 
stubServiceEmitter.getMetricEvents("sqlQuery/time").stream()
+                                                           .map(event -> 
event.toMap())
+                                                           .map(map -> (int) 
map.get(DruidMetrics.CODE))
+                                                           
.collect(Collectors.groupingBy(
+                                                               code -> code,
+                                                               
Collectors.counting()
+                                                           ));
+    Assert.assertEquals(Map.of(200, 2L, 429, 1L), codeFrequencies);
   }
 
   @Test
@@ -1905,6 +1926,8 @@ public class SqlResourceTest extends CalciteTestBase
         ""
     );
     Assert.assertTrue(lifecycleManager.getAll(sqlQueryId).isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(504, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -1940,6 +1963,8 @@ public class SqlResourceTest extends CalciteTestBase
         null,
         ""
     );
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(500, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -1968,6 +1993,8 @@ public class SqlResourceTest extends CalciteTestBase
 
     ErrorResponse exception = deserializeResponse(queryResponse, 
ErrorResponse.class);
     validateLegacyQueryExceptionErrorResponse(exception, "Query cancelled", 
null, "");
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(500, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -2050,6 +2077,8 @@ public class SqlResourceTest extends CalciteTestBase
     );
     checkSqlRequestLog(false);
     Assert.assertTrue(lifecycleManager.getAll(sqlQueryId).isEmpty());
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(400, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   @Test
@@ -2066,6 +2095,8 @@ public class SqlResourceTest extends CalciteTestBase
         "Query context parameter [sqlInsertSegmentGranularity] is not allowed"
     );
     checkSqlRequestLog(false);
+    stubServiceEmitter.verifyEmitted("sqlQuery/time", 1);
+    Assert.assertEquals(400, 
stubServiceEmitter.getMetricEvents("sqlQuery/time").get(0).toMap().get(DruidMetrics.CODE));
   }
 
   private void checkSqlRequestLog(boolean success)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to