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

FrankChen021 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 5b67e6f120e feat: add configurable metric sampling to the Kafka 
emitter (#20559)
5b67e6f120e is described below

commit 5b67e6f120edf77c1b2ec02fd0e383c1f79b5488
Author: Nagarjunanm <[email protected]>
AuthorDate: Wed Oct 21 18:03:27 2026 +0530

    feat: add configurable metric sampling to the Kafka emitter (#20559)
    
    * feat: add configurable metric sampling to the Kafka emitter
    
    Adds optional sampling of ServiceMetricEvents to the kafka-emitter 
extension,
    so operators can drop a percentage of high-volume metrics before they reach 
the
    Kafka producer.
    
    New properties under druid.emitter.kafka:
    - samplingRate (0-100, default 100 = no sampling); values outside 0-100 are 
rejected
    - sampledMetrics: exact metric names eligible for sampling (empty = all)
    - sampledNodeTypes: services eligible for sampling (empty = all)
    
    Only metric events are sampled; alerts, request logs and segment metadata 
are
    always emitted. When an event has an "id" dimension, the keep/drop decision 
is
    based on a hash of that id, so all metrics for one query are kept or dropped
    together; otherwise events are kept at random. The emitter counts sampled 
and
    emitted metric events and logs a summary every five minutes.
    
    Fixes #20246
    
    * fix: advance the sampling summary window every interval
    
    The emitted-metric baseline only moved forward in intervals that sampled
    something. After a quiet interval, the next summary reported emitted and
    total counts covering several intervals instead of the past five minutes.
    
    Move the baseline forward on every status check, and keep logging the
    summary only when the interval sampled events. Adds a test for this case.
    
    * perf: decide on metric sampling before serializing the event
    
    Sampled-out metrics were still converted to a map, given extra dimensions,
    serialized to JSON and encoded as UTF-8 before being dropped. At low
    sampling rates that is wasted CPU and allocation on most metrics, and a
    sampled-out event with an unserializable dimension was counted as invalid
    instead of sampled.
    
    Check shouldSampleOut() first and return early, so only metrics that will
    be queued are serialized. Adds a test for this case.
    
    * refactor: rename sampledNodeTypes to sampledServiceTypes; address review
    
    - Rename druid.emitter.kafka.sampledNodeTypes to sampledServiceTypes, to
      match the "service" name used by ServiceMetricEvent.
    - Docs: state that samplingRate is an integer from 0 to 100.
    - Tests: fix a Javadoc that said rate 10 while the test uses 0, and remove
      testShouldSampleOut_noSamplingWhenSampledMetricsEmpty. It used rate 100,
      so it could not tell which setting kept the event, and its name described
      behaviour the feature doesn't have: an empty sampledMetrics samples every
      metric. Both real cases are already covered by
      testShouldSampleOut_rate100OverridesAllFilters and
      testShouldSampleOut_metricOnlyFilterNonMatchingMetric.
---
 .../extensions-contrib/kafka-emitter.md            |  27 ++
 .../apache/druid/emitter/kafka/KafkaEmitter.java   | 102 +++++
 .../druid/emitter/kafka/KafkaEmitterConfig.java    |  60 ++-
 .../emitter/kafka/KafkaEmitterConfigTest.java      | 148 ++++++-
 .../druid/emitter/kafka/KafkaEmitterTest.java      | 474 ++++++++++++++++++++-
 5 files changed, 808 insertions(+), 3 deletions(-)

diff --git a/docs/development/extensions-contrib/kafka-emitter.md 
b/docs/development/extensions-contrib/kafka-emitter.md
index 772c0ff405c..c08bcecad21 100644
--- a/docs/development/extensions-contrib/kafka-emitter.md
+++ b/docs/development/extensions-contrib/kafka-emitter.md
@@ -49,6 +49,9 @@ All the configuration parameters for the Kafka emitter are 
under `druid.emitter.
 | `druid.emitter.kafka.extra.dimensions` | Optional JSON configuration to 
specify a map of extra string dimensions for the events emitted. These can help 
make groups in your monitoring environment. | no | none |
 | `druid.emitter.kafka.producer.hiddenProperties`    | JSON configuration to 
specify sensitive Kafka producer properties such as username and password.  
This property accepts a 
[DynamicConfigProvider](../../operations/dynamic-config-provider.md) 
implementation. | no | none |
 | `druid.emitter.kafka.producer.shutdownTimeout`    | Duration in milliseconds 
the Kafka producer waits for pending requests to finish before shutting down. | 
no | Long.MAX_VALUE |
+| `druid.emitter.kafka.samplingRate` | Integer from 0 to 100: the percentage 
of matching metric events to keep. `100` keeps every event; `0` drops every 
matching event. Decimal values aren't supported. See [Metric 
sampling](#metric-sampling). | no | 100 |
+| `druid.emitter.kafka.sampledMetrics` | JSON array of metric names to sample, 
such as `["query/time"]`. Names must match exactly. If empty, sampling applies 
to all metrics. | no | `[]` |
+| `druid.emitter.kafka.sampledServiceTypes` | JSON array of services to 
sample, such as `["druid/broker"]`. If empty, sampling applies to all services. 
| no | `[]` |
 
 ### Example
 
@@ -64,3 +67,27 @@ 
druid.emitter.kafka.extra.dimensions={"region":"us-east-1","environment":"prePro
 druid.emitter.kafka.producer.hiddenProperties={"config":{"sasl.jaas.config": 
"org.apache.kafka.common.security.plain.PlainLoginModule required 
username=\\"KV...NI\\" password=\\"gA3...n6a/\\";"}}
 ```
 
+### Metric sampling
+
+High-traffic clusters can emit a large volume of metric events. Metrics such 
as `query/time` are emitted for every query. To reduce Kafka produce volume, 
network traffic, and storage, the Kafka emitter can drop a percentage of metric 
events before they reach the Kafka producer.
+
+Sampling works as follows:
+
+- Only metric events are sampled. Alerts, request logs, and segment metadata 
events are always emitted.
+- A metric event is sampled only if its metric name is in `sampledMetrics` and 
its service is in `sampledServiceTypes`. An empty list matches everything.
+- Metric events that don't match are always emitted.
+- If a matching event has an `id` dimension, such as the query ID, the 
decision to keep or drop it is based on a hash of that ID. Every metric for the 
same query is then kept or dropped together. Events without an `id` dimension 
are kept at random.
+
+Every five minutes, the emitter logs how many metric events it emitted and how 
many it dropped because of sampling.
+
+The following example keeps 10% of query metrics from Brokers:
+
+```
+druid.emitter.kafka.samplingRate=10
+druid.emitter.kafka.sampledMetrics=["query/time", "query/bytes", 
"sqlQuery/time"]
+druid.emitter.kafka.sampledServiceTypes=["druid/broker"]
+```
+
+:::info
+Sampling changes absolute counts. When you count sampled events or sum their 
values, scale the result by `100 / samplingRate` to estimate the totals. 
Distributions such as percentiles don't need scaling.
+:::
diff --git 
a/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitter.java
 
b/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitter.java
index b647e425ebc..0698b94d9c3 100644
--- 
a/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitter.java
+++ 
b/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitter.java
@@ -46,6 +46,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -66,6 +67,10 @@ public class KafkaEmitter implements Emitter
   private final AtomicLong previousRequestLost;
   private final AtomicLong previousSegmentMetadataLost;
   private final AtomicLong previousInvalidLost;
+  private final AtomicLong metricSampled;
+  private final AtomicLong previousMetricSampled;
+  private final AtomicLong metricEmitted;
+  private final AtomicLong previousMetricEmitted;
 
   private final KafkaEmitterConfig config;
   private final Producer<String, String> producer;
@@ -103,6 +108,10 @@ public class KafkaEmitter implements Emitter
     this.previousRequestLost = new AtomicLong(0);
     this.previousSegmentMetadataLost = new AtomicLong(0);
     this.previousInvalidLost = new AtomicLong(0);
+    this.metricSampled = new AtomicLong(0);
+    this.previousMetricSampled = new AtomicLong(0);
+    this.metricEmitted = new AtomicLong(0);
+    this.previousMetricEmitted = new AtomicLong(0);
   }
 
   private Callback setProducerCallback(AtomicLong lostCounter)
@@ -175,6 +184,21 @@ public class KafkaEmitter implements Emitter
         currentSegmentMetadataLost - 
previousSegmentMetadataLost.getAndSet(currentSegmentMetadataLost);
     long deltaInvalidLost = currentInvalidLost - 
previousInvalidLost.getAndSet(currentInvalidLost);
 
+    long currentMetricSampled = metricSampled.get();
+    long deltaMetricSampled = currentMetricSampled - 
previousMetricSampled.getAndSet(currentMetricSampled);
+    // Advance the emitted baseline every interval, even when nothing was 
sampled, so that each summary
+    // only covers the latest interval.
+    long currentMetricEmitted = metricEmitted.get();
+    long deltaMetricEmitted = currentMetricEmitted - 
previousMetricEmitted.getAndSet(currentMetricEmitted);
+
+    if (deltaMetricSampled > 0) {
+      long deltaTotal = deltaMetricSampled + deltaMetricEmitted;
+      log.info(
+          "Metrics sampling for past %d minutes: total=[%d], emitted=[%d], 
sampledOut=[%d], cumulativeSampledOut=[%d]",
+          DEFAULT_SEND_LOST_INTERVAL_MINUTES,
+          deltaTotal, deltaMetricEmitted, deltaMetricSampled, 
currentMetricSampled);
+    }
+
     // Refrain from clogging logs when there are no lost messages.
     if (deltaMetricLost + deltaAlertLost + deltaRequestLost + 
deltaSegmentMetadataLost + deltaInvalidLost > 0) {
       log.info(
@@ -229,6 +253,11 @@ public class KafkaEmitter implements Emitter
   public void emit(final Event event)
   {
     if (event != null) {
+      // Decide on sampling first, so sampled-out metrics skip serialization 
entirely.
+      if (event instanceof ServiceMetricEvent && 
shouldSampleOut((ServiceMetricEvent) event)) {
+        metricSampled.incrementAndGet();
+        return;
+      }
       try {
         EventMap map = event.toMap();
         map = addExtraDimensionsToEvent(map);
@@ -244,6 +273,8 @@ public class KafkaEmitter implements Emitter
         if (event instanceof ServiceMetricEvent) {
           if (!eventTypes.contains(EventType.METRICS) || 
!metricQueue.offer(objectContainer)) {
             metricLost.incrementAndGet();
+          } else {
+            metricEmitted.incrementAndGet();
           }
         } else if (event instanceof AlertEvent) {
           if (!eventTypes.contains(EventType.ALERTS) || 
!alertQueue.offer(objectContainer)) {
@@ -268,6 +299,55 @@ public class KafkaEmitter implements Emitter
     }
   }
 
+  /**
+   * Determines whether a {@link ServiceMetricEvent} should be sampled out 
(i.e., dropped).
+   * <p>
+   * Sampling is skipped (event is always emitted) when:
+   * <ul>
+   *   <li>{@code samplingRate} is 100 (no sampling)</li>
+   *   <li>The metric name is null</li>
+   *   <li>The metric name is not in the configured {@code sampledMetrics} set 
(exact match only)</li>
+   *   <li>The event's service is not in the configured {@code 
sampledServiceTypes} set (when configured)</li>
+   * </ul>
+   * When sampling applies, if the event has an {@code id} user dimension, 
consistent hash-based
+   * sampling is used so that the same id always gets the same sampling 
decision. Otherwise,
+   * random sampling is used.
+   *
+   * @return true if the event should be dropped, false if it should be emitted
+   */
+  @VisibleForTesting
+  boolean shouldSampleOut(ServiceMetricEvent serviceMetricEvent)
+  {
+    int samplingRate = config.getSamplingRate();
+    Set<String> sampledMetrics = config.getSampledMetrics();
+    Set<String> sampledServiceTypes = config.getSampledServiceTypes();
+
+    // No sampling when rate is 100 or metric is null
+    if (samplingRate == 100
+        || serviceMetricEvent.getMetric() == null) {
+      return false;
+    }
+
+    // If sampledMetrics is configured, the metric must be in the set
+    if (!sampledMetrics.isEmpty() && 
!sampledMetrics.contains(serviceMetricEvent.getMetric())) {
+      return false;
+    }
+
+    // If sampledServiceTypes is configured, the service must be in the set
+    if (!sampledServiceTypes.isEmpty()
+        && (serviceMetricEvent.getService() == null
+            || 
!sampledServiceTypes.contains(serviceMetricEvent.getService()))) {
+      return false;
+    }
+
+    // Use consistent hashing on "id" dimension if available, otherwise random
+    Object id = serviceMetricEvent.getUserDims().get("id");
+    if (id == null) {
+      return ThreadLocalRandom.current().nextInt(100) >= samplingRate;
+    }
+    return Math.abs(id.hashCode() % 100) >= samplingRate;
+  }
+
   private EventMap addExtraDimensionsToEvent(EventMap map)
   {
     if (config.getClusterName() != null || config.getExtraDimensions() != 
null) {
@@ -322,6 +402,16 @@ public class KafkaEmitter implements Emitter
     return segmentMetadataLost.get();
   }
 
+  public long getMetricSampledCount()
+  {
+    return metricSampled.get();
+  }
+
+  public long getMetricEmittedCount()
+  {
+    return metricEmitted.get();
+  }
+
   @VisibleForTesting
   protected long getPreviousMetricLostCount()
   {
@@ -351,4 +441,16 @@ public class KafkaEmitter implements Emitter
   {
     return previousInvalidLost.get();
   }
+
+  @VisibleForTesting
+  protected long getPreviousMetricSampledCount()
+  {
+    return previousMetricSampled.get();
+  }
+
+  @VisibleForTesting
+  protected long getPreviousMetricEmittedCount()
+  {
+    return previousMetricEmitted.get();
+  }
 }
diff --git 
a/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitterConfig.java
 
b/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitterConfig.java
index a690637c27d..f700a58fcd2 100644
--- 
a/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitterConfig.java
+++ 
b/extensions-contrib/kafka-emitter/src/main/java/org/apache/druid/emitter/kafka/KafkaEmitterConfig.java
@@ -33,6 +33,7 @@ import org.apache.kafka.clients.producer.ProducerConfig;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -83,6 +84,14 @@ public class KafkaEmitterConfig
   private final DynamicConfigProvider<String> kafkaProducerSecrets;
   @JsonProperty("producer.shutdownTimeout")
   private final Long shutdownTimeout;
+  @JsonProperty("samplingRate")
+  private final int samplingRate;
+  @JsonProperty("sampledMetrics")
+  @Nonnull
+  private final Set<String> sampledMetrics;
+  @JsonProperty("sampledServiceTypes")
+  @Nonnull
+  private final Set<String> sampledServiceTypes;
 
   @JsonCreator
   public KafkaEmitterConfig(
@@ -96,7 +105,10 @@ public class KafkaEmitterConfig
       @Nullable @JsonProperty("extra.dimensions") Map<String, String> 
extraDimensions,
       @JsonProperty("producer.config") @Nullable Map<String, String> 
kafkaProducerConfig,
       @JsonProperty("producer.hiddenProperties") @Nullable 
DynamicConfigProvider<String> kafkaProducerSecrets,
-      @JsonProperty("producer.shutdownTimeout") @Nullable Long shutdownTimeout
+      @JsonProperty("producer.shutdownTimeout") @Nullable Long shutdownTimeout,
+      @JsonProperty("samplingRate") @Nullable Integer samplingRate,
+      @JsonProperty("sampledMetrics") @Nullable Set<String> sampledMetrics,
+      @JsonProperty("sampledServiceTypes") @Nullable Set<String> 
sampledServiceTypes
   )
   {
     this.eventTypes = maybeUpdateEventTypes(eventTypes, requestTopic);
@@ -143,6 +155,17 @@ public class KafkaEmitterConfig
     this.kafkaProducerConfig = kafkaProducerConfig == null ? ImmutableMap.of() 
: kafkaProducerConfig;
     this.kafkaProducerSecrets = kafkaProducerSecrets == null ? new 
MapStringDynamicConfigProvider(ImmutableMap.of()) : kafkaProducerSecrets;
     this.shutdownTimeout = Configs.valueOrDefault(shutdownTimeout, 
Long.MAX_VALUE);
+    this.samplingRate = Configs.valueOrDefault(samplingRate, 100);
+    if (this.samplingRate < 0 || this.samplingRate > 100) {
+      throw DruidException.forPersona(DruidException.Persona.OPERATOR)
+                          .ofCategory(DruidException.Category.INVALID_INPUT)
+                          .build(
+                              "druid.emitter.kafka.samplingRate must be 
between 0 and 100, got [%d].",
+                              this.samplingRate
+                          );
+    }
+    this.sampledMetrics = sampledMetrics == null ? Collections.emptySet() : 
sampledMetrics;
+    this.sampledServiceTypes = sampledServiceTypes == null ? 
Collections.emptySet() : sampledServiceTypes;
   }
 
   @Nonnull
@@ -228,6 +251,26 @@ public class KafkaEmitterConfig
     return shutdownTimeout;
   }
 
+  @JsonProperty
+  public int getSamplingRate()
+  {
+    return samplingRate;
+  }
+
+  @JsonProperty
+  @Nonnull
+  public Set<String> getSampledMetrics()
+  {
+    return sampledMetrics;
+  }
+
+  @JsonProperty
+  @Nonnull
+  public Set<String> getSampledServiceTypes()
+  {
+    return sampledServiceTypes;
+  }
+
   @Override
   public boolean equals(Object o)
   {
@@ -273,6 +316,15 @@ public class KafkaEmitterConfig
     if (!getShutdownTimeout().equals(that.getShutdownTimeout())) {
       return false;
     }
+    if (getSamplingRate() != that.getSamplingRate()) {
+      return false;
+    }
+    if (!getSampledMetrics().equals(that.getSampledMetrics())) {
+      return false;
+    }
+    if (!getSampledServiceTypes().equals(that.getSampledServiceTypes())) {
+      return false;
+    }
     return 
getKafkaProducerSecrets().getConfig().equals(that.getKafkaProducerSecrets().getConfig());
   }
 
@@ -289,6 +341,9 @@ public class KafkaEmitterConfig
     result = 31 * result + (getExtraDimensions() != null ? 
getExtraDimensions().hashCode() : 0);
     result = 31 * result + getKafkaProducerConfig().hashCode();
     result = 31 * result + getShutdownTimeout().hashCode();
+    result = 31 * result + Integer.hashCode(getSamplingRate());
+    result = 31 * result + getSampledMetrics().hashCode();
+    result = 31 * result + getSampledServiceTypes().hashCode();
     result = 31 * result + getKafkaProducerSecrets().getConfig().hashCode();
     return result;
   }
@@ -308,6 +363,9 @@ public class KafkaEmitterConfig
            ", producer.config=" + kafkaProducerConfig + '\'' +
            ", producer.hiddenProperties=" + kafkaProducerSecrets +
            ", producer.shutdownTimeout=" + shutdownTimeout +
+           ", samplingRate=" + samplingRate +
+           ", sampledMetrics=" + sampledMetrics +
+           ", sampledServiceTypes=" + sampledServiceTypes +
            '}';
   }
 }
diff --git 
a/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterConfigTest.java
 
b/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterConfigTest.java
index 6951be915e4..98ffa4f5433 100644
--- 
a/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterConfigTest.java
+++ 
b/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterConfigTest.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.metadata.DynamicConfigProvider;
@@ -57,7 +58,10 @@ public class KafkaEmitterConfigTest
         ImmutableMap.<String, String>builder()
                     .put("testKey", "testValue").build(),
         DEFAULT_PRODUCER_SECRETS,
-        50L
+        50L,
+        null,
+        null,
+        null
     );
     String kafkaEmitterConfigString = 
MAPPER.writeValueAsString(kafkaEmitterConfig);
     KafkaEmitterConfig kafkaEmitterConfigExpected = 
MAPPER.readerFor(KafkaEmitterConfig.class)
@@ -80,6 +84,9 @@ public class KafkaEmitterConfigTest
         ImmutableMap.<String, String>builder()
                     .put("testKey", "testValue").build(),
         DEFAULT_PRODUCER_SECRETS,
+        null,
+        null,
+        null,
         null
     );
     String kafkaEmitterConfigString = 
MAPPER.writeValueAsString(kafkaEmitterConfig);
@@ -105,6 +112,9 @@ public class KafkaEmitterConfigTest
         ImmutableMap.<String, String>builder()
                     .put("testKey", "testValue").build(),
         DEFAULT_PRODUCER_SECRETS,
+        null,
+        null,
+        null,
         null
     );
     String kafkaEmitterConfigString = 
MAPPER.writeValueAsString(kafkaEmitterConfig);
@@ -127,6 +137,9 @@ public class KafkaEmitterConfigTest
         ImmutableMap.of("env", "preProd"),
         null,
         null,
+        null,
+        null,
+        null,
         null
     );
     String kafkaEmitterConfigString = 
MAPPER.writeValueAsString(kafkaEmitterConfig);
@@ -182,6 +195,9 @@ public class KafkaEmitterConfigTest
                 null,
                 null,
                 null,
+                null,
+                null,
+                null,
                 null
             )
         ),
@@ -206,6 +222,9 @@ public class KafkaEmitterConfigTest
                 null,
                 null,
                 null,
+                null,
+                null,
+                null,
                 null
             )
         ),
@@ -230,6 +249,9 @@ public class KafkaEmitterConfigTest
                 null,
                 null,
                 null,
+                null,
+                null,
+                null,
                 null
             )
         ),
@@ -254,6 +276,9 @@ public class KafkaEmitterConfigTest
                 null,
                 null,
                 null,
+                null,
+                null,
+                null,
                 null
             )
         ),
@@ -278,6 +303,9 @@ public class KafkaEmitterConfigTest
                 null,
                 null,
                 null,
+                null,
+                null,
+                null,
                 null
             )
         ),
@@ -302,6 +330,9 @@ public class KafkaEmitterConfigTest
         ImmutableMap.<String, String>builder()
                     .put("testKey", "testValue").build(),
         DEFAULT_PRODUCER_SECRETS,
+        null,
+        null,
+        null,
         null
     );
     String kafkaEmitterConfigString = 
MAPPER.writeValueAsString(kafkaEmitterConfig);
@@ -310,6 +341,121 @@ public class KafkaEmitterConfigTest
     Assertions.assertEquals(Long.MAX_VALUE, (long) 
kafkaEmitterConfigExpected.getShutdownTimeout());
   }
 
+  @Test
+  public void testSamplingDefaults()
+  {
+    KafkaEmitterConfig config = newSamplingConfig(null, null, null);
+    Assertions.assertEquals(100, config.getSamplingRate());
+    Assertions.assertEquals(Collections.emptySet(), 
config.getSampledMetrics());
+    Assertions.assertEquals(Collections.emptySet(), 
config.getSampledServiceTypes());
+  }
+
+  @Test
+  public void testSerDeserSamplingConfig() throws IOException
+  {
+    KafkaEmitterConfig config = newSamplingConfig(
+        10,
+        ImmutableSet.of("query/time", "query/bytes"),
+        ImmutableSet.of("druid/broker")
+    );
+    KafkaEmitterConfig roundTripped = 
MAPPER.readerFor(KafkaEmitterConfig.class)
+                                            
.readValue(MAPPER.writeValueAsString(config));
+    Assertions.assertEquals(config, roundTripped);
+    Assertions.assertEquals(config.hashCode(), roundTripped.hashCode());
+    Assertions.assertEquals(10, roundTripped.getSamplingRate());
+    Assertions.assertEquals(ImmutableSet.of("query/time", "query/bytes"), 
roundTripped.getSampledMetrics());
+    Assertions.assertEquals(ImmutableSet.of("druid/broker"), 
roundTripped.getSampledServiceTypes());
+  }
+
+  @Test
+  public void testDeserSamplingConfigFromPropertyNames() throws IOException
+  {
+    KafkaEmitterConfig config = MAPPER.readValue(
+        "{\"bootstrap.servers\": \"localhost:9092\","
+        + " \"metric.topic\": \"metrics\","
+        + " \"alert.topic\": \"alerts\","
+        + " \"samplingRate\": 25,"
+        + " \"sampledMetrics\": [\"query/time\"],"
+        + " \"sampledServiceTypes\": [\"druid/broker\", 
\"druid/historical\"]}",
+        KafkaEmitterConfig.class
+    );
+    Assertions.assertEquals(25, config.getSamplingRate());
+    Assertions.assertEquals(ImmutableSet.of("query/time"), 
config.getSampledMetrics());
+    Assertions.assertEquals(ImmutableSet.of("druid/broker", 
"druid/historical"), config.getSampledServiceTypes());
+  }
+
+  @Test
+  public void testSamplingConfigsAffectEquality()
+  {
+    KafkaEmitterConfig base = newSamplingConfig(10, 
ImmutableSet.of("query/time"), ImmutableSet.of("druid/broker"));
+    Assertions.assertEquals(
+        base,
+        newSamplingConfig(10, ImmutableSet.of("query/time"), 
ImmutableSet.of("druid/broker"))
+    );
+    Assertions.assertNotEquals(
+        base,
+        newSamplingConfig(20, ImmutableSet.of("query/time"), 
ImmutableSet.of("druid/broker"))
+    );
+    Assertions.assertNotEquals(
+        base,
+        newSamplingConfig(10, ImmutableSet.of("query/bytes"), 
ImmutableSet.of("druid/broker"))
+    );
+    Assertions.assertNotEquals(
+        base,
+        newSamplingConfig(10, ImmutableSet.of("query/time"), 
ImmutableSet.of("druid/historical"))
+    );
+  }
+
+  @Test
+  public void testSamplingRateBoundariesAccepted()
+  {
+    Assertions.assertEquals(0, newSamplingConfig(0, null, 
null).getSamplingRate());
+    Assertions.assertEquals(100, newSamplingConfig(100, null, 
null).getSamplingRate());
+  }
+
+  @Test
+  public void testSamplingRateOutOfRangeRejected()
+  {
+    for (int invalidRate : new int[]{-1, 101}) {
+      DruidException exception = Assertions.assertThrows(
+          DruidException.class,
+          () -> newSamplingConfig(invalidRate, null, null)
+      );
+      Assertions.assertAll(
+          () -> Assertions.assertEquals(DruidException.Persona.OPERATOR, 
exception.getTargetPersona()),
+          () -> Assertions.assertEquals(DruidException.Category.INVALID_INPUT, 
exception.getCategory()),
+          () -> Assertions.assertEquals(
+              "druid.emitter.kafka.samplingRate must be between 0 and 100, got 
[" + invalidRate + "].",
+              exception.getMessage()
+          )
+      );
+    }
+  }
+
+  private static KafkaEmitterConfig newSamplingConfig(
+      Integer samplingRate,
+      Set<String> sampledMetrics,
+      Set<String> sampledServiceTypes
+  )
+  {
+    return new KafkaEmitterConfig(
+        "localhost:9092",
+        null,
+        "metrics",
+        "alerts",
+        null,
+        null,
+        null,
+        null,
+        null,
+        null,
+        null,
+        samplingRate,
+        sampledMetrics,
+        sampledServiceTypes
+    );
+  }
+
   private void assertOperatorException(final DruidException exception, final 
String expectedMessage)
   {
     Assertions.assertAll(
diff --git 
a/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterTest.java
 
b/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterTest.java
index 0f6f91df1c6..34c3bbd0a10 100644
--- 
a/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterTest.java
+++ 
b/extensions-contrib/kafka-emitter/src/test/java/org/apache/druid/emitter/kafka/KafkaEmitterTest.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.java.util.common.StringUtils;
@@ -52,6 +53,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -151,6 +153,9 @@ public class KafkaEmitterTest
         ImmutableMap.of("clusterId", "cluster-101"),
         null,
         null,
+        null,
+        null,
+        null,
         null
     );
 
@@ -206,6 +211,9 @@ public class KafkaEmitterTest
         ImmutableMap.of("clusterId", "cluster-101", "env", "staging"),
         null,
         null,
+        null,
+        null,
+        null,
         null
     );
 
@@ -257,6 +265,9 @@ public class KafkaEmitterTest
         null,
         null,
         null,
+        null,
+        null,
+        null,
         null
     );
 
@@ -307,6 +318,9 @@ public class KafkaEmitterTest
         null,
         null,
         null,
+        null,
+        null,
+        null,
         null
     );
 
@@ -383,6 +397,9 @@ public class KafkaEmitterTest
         null,
         null,
         null,
+        null,
+        null,
+        null,
         null
     );
 
@@ -432,7 +449,10 @@ public class KafkaEmitterTest
         null,
         null,
         "cluster-102",
-        ImmutableMap.of("clusterName", "cluster-101", "env", "staging"), // 
clusterName again, extraDimensions should take precedence
+        ImmutableMap.of("clusterName", "cluster-101", "env", "staging"),
+        null,
+        null,
+        null,
         null,
         null,
         null
@@ -530,6 +550,9 @@ public class KafkaEmitterTest
         extraDimensions,
         ImmutableMap.of(ProducerConfig.BUFFER_MEMORY_CONFIG, 
String.valueOf(totalBufferSize)),
         null,
+        null,
+        null,
+        null,
         null
     );
 
@@ -672,4 +695,453 @@ public class KafkaEmitterTest
       return "testFeed";
     }
   }
+
+  // ==================== Sampling Tests ====================
+
+  @Test
+  public void testShouldSampleOut_noSamplingWhenRateIs100()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(100, 
ImmutableSet.of("query/time"), null));
+    ServiceMetricEvent event = 
ServiceMetricEvent.builder().setMetric("query/time", 100).build("svc", "host");
+    Assertions.assertFalse(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * When both sampledMetrics and sampledServiceTypes are empty, every metric 
is eligible for sampling.
+   * samplingRate=0 makes the result deterministic: every metric is dropped.
+   */
+  @Test
+  public void testShouldSampleOut_samplesAllWhenRateSetButFiltersEmpty()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, null, 
null));
+    ServiceMetricEvent event = 
ServiceMetricEvent.builder().setMetric("query/time", 100).build("svc", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(event));
+  }
+
+  @Test
+  public void testShouldSampleOut_noSamplingForNonMatchingMetric()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(
+        10,
+        ImmutableSet.of("query/time", "query/bytes"),
+        null
+    ));
+    // "jvm/mem/used" is not in sampledMetrics, so should never be sampled out
+    ServiceMetricEvent event = 
ServiceMetricEvent.builder().setMetric("jvm/mem/used", 100).build("svc", 
"host");
+    Assertions.assertFalse(emitter.shouldSampleOut(event));
+  }
+
+  @Test
+  public void testShouldSampleOut_samplesMatchingMetricWithZeroRate()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, 
ImmutableSet.of("query/time"), null));
+    ServiceMetricEvent event = 
ServiceMetricEvent.builder().setMetric("query/time", 100).build("svc", "host");
+    // With rate=0, all matching metrics should be sampled out
+    Assertions.assertTrue(emitter.shouldSampleOut(event));
+  }
+
+  @Test
+  public void testShouldSampleOut_consistentHashOnId()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(50, 
ImmutableSet.of("query/time"), null));
+
+    // Build event with an "id" dimension
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setDimension("id", "query-abc-123")
+        .setMetric("query/time", 100)
+        .build("svc", "host");
+
+    // Consistent hashing: same id should always produce the same result
+    boolean firstResult = emitter.shouldSampleOut(event);
+    for (int i = 0; i < 100; i++) {
+      Assertions.assertEquals(
+          firstResult,
+          emitter.shouldSampleOut(event),
+          "Consistent hashing should give same result for same id"
+      );
+    }
+  }
+
+  // ==================== Service Type Sampling Tests ====================
+
+  /**
+   * samplingRate=0, sampledMetrics=["query/time"], 
sampledServiceTypes=["druid/broker"]
+   * Event from druid/broker with query/time → should be sampled out
+   */
+  @Test
+  public void testShouldSampleOut_matchingMetricAndServiceType()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(
+        0,
+        ImmutableSet.of("query/time"),
+        ImmutableSet.of("druid/broker")
+    ));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/broker", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, sampledMetrics=["query/time"], 
sampledServiceTypes=["druid/broker"]
+   * Event from druid/historical with query/time → should NOT be sampled 
(wrong service type)
+   */
+  @Test
+  public void testShouldSampleOut_matchingMetricButWrongServiceType()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(
+        0,
+        ImmutableSet.of("query/time"),
+        ImmutableSet.of("druid/broker")
+    ));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/historical", "host");
+    Assertions.assertFalse(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, sampledMetrics=empty, sampledServiceTypes=["druid/broker"]
+   * Event from druid/broker with any metric → should be sampled out (service 
type filter only)
+   */
+  @Test
+  public void testShouldSampleOut_serviceTypeOnlyFilter()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, null, 
ImmutableSet.of("druid/broker")));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("jvm/mem/used", 100)
+        .build("druid/broker", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, sampledMetrics=empty, sampledServiceTypes=["druid/broker"]
+   * Event from druid/historical → should NOT be sampled (wrong service type)
+   */
+  @Test
+  public void testShouldSampleOut_serviceTypeOnlyFilterWrongServiceType()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, null, 
ImmutableSet.of("druid/broker")));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("jvm/mem/used", 100)
+        .build("druid/historical", "host");
+    Assertions.assertFalse(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, sampledMetrics=["query/time"], sampledServiceTypes=empty
+   * Event with query/time from any node → should be sampled out (metric 
filter only)
+   */
+  @Test
+  public void testShouldSampleOut_metricOnlyFilter()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, 
ImmutableSet.of("query/time"), null));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/historical", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, sampledMetrics=["query/time"], sampledServiceTypes=empty
+   * Event with jvm/mem/used → should NOT be sampled (metric doesn't match)
+   */
+  @Test
+  public void testShouldSampleOut_metricOnlyFilterNonMatchingMetric()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, 
ImmutableSet.of("query/time"), null));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("jvm/mem/used", 100)
+        .build("druid/historical", "host");
+    Assertions.assertFalse(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=100 always means no sampling, even with filters configured
+   */
+  @Test
+  public void testShouldSampleOut_rate100OverridesAllFilters()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(
+        100,
+        ImmutableSet.of("query/time"),
+        ImmutableSet.of("druid/broker")
+    ));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/broker", "host");
+    Assertions.assertFalse(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, both filters empty → sample ALL metrics from all nodes 
(drop everything)
+   */
+  @Test
+  public void testShouldSampleOut_rate0WithNoFiltersDropsEverything()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(0, null, 
null));
+    ServiceMetricEvent event = ServiceMetricEvent.builder()
+        .setMetric("anything/at/all", 100)
+        .build("druid/coordinator", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(event));
+  }
+
+  /**
+   * samplingRate=0, multiple service types configured
+   */
+  @Test
+  public void testShouldSampleOut_multipleServiceTypes()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(
+        0,
+        null,
+        ImmutableSet.of("druid/broker", "druid/historical")
+    ));
+
+    // broker → sampled
+    ServiceMetricEvent brokerEvent = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/broker", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(brokerEvent));
+
+    // historical → sampled
+    ServiceMetricEvent historicalEvent = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/historical", "host");
+    Assertions.assertTrue(emitter.shouldSampleOut(historicalEvent));
+
+    // coordinator → NOT sampled (not in set)
+    ServiceMetricEvent coordinatorEvent = ServiceMetricEvent.builder()
+        .setMetric("query/time", 100)
+        .build("druid/coordinator", "host");
+    Assertions.assertFalse(emitter.shouldSampleOut(coordinatorEvent));
+  }
+
+  /**
+   * With 10% sampling and no "id" dimension, roughly 10% of matching events 
should be kept.
+   * The bounds are wide enough (about 13 standard deviations) for this check 
never to flake.
+   */
+  @Test
+  public void testShouldSampleOut_approximateRetentionRate()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(10, 
ImmutableSet.of("query/time"), null));
+    final ServiceMetricEvent event = ServiceMetricEvent.builder()
+                                                       
.setMetric("query/time", 100)
+                                                       .build("druid/broker", 
"host");
+    final int total = 100_000;
+    int kept = 0;
+    for (int i = 0; i < total; i++) {
+      if (!emitter.shouldSampleOut(event)) {
+        kept++;
+      }
+    }
+    Assertions.assertTrue(kept > 8_800 && kept < 11_200, "Expected about 10% 
kept, but kept " + kept + " of " + total);
+  }
+
+  /**
+   * With 50% sampling, events with different "id" values should be split 
between kept and dropped,
+   * while each individual id keeps getting the same decision.
+   */
+  @Test
+  public void testShouldSampleOut_hashOnIdSplitsAcrossIds()
+  {
+    final KafkaEmitter emitter = initKafkaEmitter(newSamplingConfig(50, 
ImmutableSet.of("query/time"), null));
+    final int total = 10_000;
+    int kept = 0;
+    for (int i = 0; i < total; i++) {
+      final ServiceMetricEvent event = ServiceMetricEvent.builder()
+                                                         .setDimension("id", 
"query-" + i)
+                                                         
.setMetric("query/time", 100)
+                                                         
.build("druid/broker", "host");
+      final boolean dropped = emitter.shouldSampleOut(event);
+      Assertions.assertEquals(dropped, emitter.shouldSampleOut(event));
+      if (!dropped) {
+        kept++;
+      }
+    }
+    Assertions.assertTrue(kept > 4_000 && kept < 6_000, "Expected about 50% 
kept, but kept " + kept + " of " + total);
+  }
+
+  /**
+   * End-to-end: with samplingRate=0, metric events never reach the Kafka 
producer, while alerts, requests and
+   * segment metadata are still emitted. Sampled-out events are counted as 
sampled, not as lost.
+   */
+  @Test
+  @Timeout(value = 10_000, unit = TimeUnit.MILLISECONDS)
+  public void testSampledMetricsAreDroppedBeforeProducerAndOtherEventsAreKept()
+      throws InterruptedException, JsonProcessingException
+  {
+    final KafkaEmitterConfig kafkaEmitterConfig = new KafkaEmitterConfig(
+        "",
+        new HashSet<>(Arrays.asList(KafkaEmitterConfig.EventType.values())),
+        "metrics",
+        "alerts",
+        "requests",
+        "segments",
+        "clusterName",
+        null,
+        null,
+        null,
+        null,
+        0,
+        null,
+        null
+    );
+    final KafkaEmitter kafkaEmitter = initKafkaEmitter(kafkaEmitterConfig);
+
+    final List<Event> metricEvents = flattenEvents(SERVICE_METRIC_EVENTS);
+    final List<Event> keptEvents = flattenEvents(ALERT_EVENTS, 
REQUEST_LOG_EVENTS, SEGMENT_METADATA_EVENTS);
+    final CountDownLatch eventLatch = new CountDownLatch(keptEvents.size());
+
+    final Map<String, List<EventMap>> feedToExpectedEvents = 
trackExpectedEventsPerFeed(
+        keptEvents,
+        kafkaEmitterConfig.getClusterName(),
+        kafkaEmitterConfig.getExtraDimensions()
+    );
+    final Map<String, List<EventMap>> feedToActualEvents = 
trackActualEventsPerFeed(eventLatch);
+
+    emitEvents(kafkaEmitter, flattenEvents(metricEvents, keptEvents), 
eventLatch);
+
+    validateEvents(feedToExpectedEvents, feedToActualEvents);
+    Assertions.assertEquals(metricEvents.size(), 
kafkaEmitter.getMetricSampledCount());
+    Assertions.assertEquals(0, kafkaEmitter.getMetricEmittedCount());
+    Assertions.assertEquals(0, kafkaEmitter.getMetricLostCount());
+    Assertions.assertEquals(0, kafkaEmitter.getAlertLostCount());
+    Assertions.assertEquals(0, kafkaEmitter.getRequestLostCount());
+    Assertions.assertEquals(0, kafkaEmitter.getSegmentMetadataLostCount());
+  }
+
+  /**
+   * End-to-end: only the configured metric is sampled; other metrics are 
still emitted.
+   */
+  @Test
+  @Timeout(value = 10_000, unit = TimeUnit.MILLISECONDS)
+  public void testOnlyConfiguredMetricsAreSampledEndToEnd() throws 
InterruptedException, JsonProcessingException
+  {
+    final KafkaEmitterConfig kafkaEmitterConfig = newSamplingConfig(0, 
ImmutableSet.of("query/time"), null);
+    final KafkaEmitter kafkaEmitter = initKafkaEmitter(kafkaEmitterConfig);
+
+    final List<Event> sampledEvents = ImmutableList.of(
+        ServiceMetricEvent.builder().setMetric("query/time", 
10).build("druid/broker", "host"),
+        ServiceMetricEvent.builder().setMetric("query/time", 
20).build("druid/broker", "host")
+    );
+    final List<Event> keptEvents = ImmutableList.of(
+        ServiceMetricEvent.builder().setMetric("jvm/mem/used", 
30).build("druid/broker", "host"),
+        ServiceMetricEvent.builder().setMetric("query/bytes", 
40).build("druid/broker", "host")
+    );
+    final CountDownLatch eventLatch = new CountDownLatch(keptEvents.size());
+
+    final Map<String, List<EventMap>> feedToExpectedEvents = 
trackExpectedEventsPerFeed(
+        keptEvents,
+        kafkaEmitterConfig.getClusterName(),
+        kafkaEmitterConfig.getExtraDimensions()
+    );
+    final Map<String, List<EventMap>> feedToActualEvents = 
trackActualEventsPerFeed(eventLatch);
+
+    emitEvents(kafkaEmitter, flattenEvents(sampledEvents, keptEvents), 
eventLatch);
+
+    validateEvents(feedToExpectedEvents, feedToActualEvents);
+    Assertions.assertEquals(sampledEvents.size(), 
kafkaEmitter.getMetricSampledCount());
+    Assertions.assertEquals(keptEvents.size(), 
kafkaEmitter.getMetricEmittedCount());
+    Assertions.assertEquals(0, kafkaEmitter.getMetricLostCount());
+
+    // Exercises the periodic sampling summary log path.
+    kafkaEmitter.logLostMessagesStatus();
+  }
+
+  /**
+   * The sampling summary must only cover the latest interval. An interval 
that emits metrics but samples none
+   * skips the log line, but must still move the emitted-count baseline 
forward, so its emissions are not
+   * reported again in a later interval's summary.
+   */
+  @Test
+  public void testSamplingSummaryWindowAdvancesWhenNothingSampled()
+  {
+    final KafkaEmitter kafkaEmitter = initKafkaEmitter(newSamplingConfig(0, 
ImmutableSet.of("query/time"), null));
+    final ServiceMetricEvent keptEvent = ServiceMetricEvent.builder()
+                                                           
.setMetric("jvm/mem/used", 1)
+                                                           
.build("druid/broker", "host");
+    final ServiceMetricEvent sampledEvent = ServiceMetricEvent.builder()
+                                                              
.setMetric("query/time", 1)
+                                                              
.build("druid/broker", "host");
+
+    // Interval 1: two metrics emitted, none sampled out.
+    kafkaEmitter.emit(keptEvent);
+    kafkaEmitter.emit(keptEvent);
+    kafkaEmitter.logLostMessagesStatus();
+    Assertions.assertEquals(0, kafkaEmitter.getMetricSampledCount());
+    Assertions.assertEquals(2, kafkaEmitter.getPreviousMetricEmittedCount());
+
+    // Interval 2: one emitted and one sampled out. The window must start from 
interval 1's end.
+    kafkaEmitter.emit(keptEvent);
+    kafkaEmitter.emit(sampledEvent);
+    kafkaEmitter.logLostMessagesStatus();
+    Assertions.assertEquals(3, kafkaEmitter.getPreviousMetricEmittedCount());
+    Assertions.assertEquals(1, kafkaEmitter.getPreviousMetricSampledCount());
+  }
+
+  /**
+   * Sampling is decided before the event is serialized, so a sampled-out 
metric never pays for serialization.
+   * A dimension that fails to serialize proves it: the sampled-out event is 
counted as sampled, not invalid,
+   * while an event that is kept still goes through serialization and is 
counted as invalid as before.
+   */
+  @Test
+  public void testSamplingHappensBeforeSerialization()
+  {
+    final KafkaEmitter kafkaEmitter = initKafkaEmitter(newSamplingConfig(0, 
ImmutableSet.of("query/time"), null));
+
+    kafkaEmitter.emit(
+        ServiceMetricEvent.builder()
+                          .setDimension("bad", new UnserializableValue())
+                          .setMetric("query/time", 1)
+                          .build("druid/broker", "host")
+    );
+    Assertions.assertEquals(1, kafkaEmitter.getMetricSampledCount());
+    Assertions.assertEquals(0, kafkaEmitter.getInvalidLostCount());
+
+    kafkaEmitter.emit(
+        ServiceMetricEvent.builder()
+                          .setDimension("bad", new UnserializableValue())
+                          .setMetric("jvm/mem/used", 1)
+                          .build("druid/broker", "host")
+    );
+    Assertions.assertEquals(1, kafkaEmitter.getMetricSampledCount());
+    Assertions.assertEquals(1, kafkaEmitter.getInvalidLostCount());
+  }
+
+  /**
+   * A dimension value that Jackson cannot serialize, because its only 
property getter throws.
+   */
+  private static class UnserializableValue
+  {
+    @SuppressWarnings("unused")
+    public String getValue()
+    {
+      throw new IllegalStateException("not serializable");
+    }
+  }
+
+  private static KafkaEmitterConfig newSamplingConfig(
+      final Integer samplingRate,
+      final Set<String> sampledMetrics,
+      final Set<String> sampledServiceTypes
+  )
+  {
+    return new KafkaEmitterConfig(
+        "localhost:9092",
+        null,
+        "metrics",
+        "alerts",
+        null,
+        null,
+        null,
+        null,
+        null,
+        null,
+        null,
+        samplingRate,
+        sampledMetrics,
+        sampledServiceTypes
+    );
+  }
 }


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

Reply via email to