Skip to content

Prometheus per label ttl#18689

Merged
abhishekrb19 merged 16 commits into
apache:masterfrom
aho135:prometheus-per-label-ttl
Nov 4, 2025
Merged

Prometheus per label ttl#18689
abhishekrb19 merged 16 commits into
apache:masterfrom
aho135:prometheus-per-label-ttl

Conversation

@aho135

@aho135 aho135 commented Oct 23, 2025

Copy link
Copy Markdown
Contributor

Fixes #14638

This is an extension of #18598 to enhance the TTL to track metrics at the label level instead of just the metric level.

Description

This PR enhances the current metric TTL tracking in the prometheus-emitter so that all unique label combinations are tracked instead of just the metric name. This is done using a ConcurrentHashMap which maintains a mapping of label values to a Stopwatch. The map needs to handle concurrent updates because there is a possibility for the TTL scheduler to remove a key from a map at the same that the key is updated during metric emission. These are handled through the two atomic operations removeIfExpired and resetLastUpdateTime

Release note

Enhances the prometheus-emitter metric TTL tracking to consider all label value combinations instead of just the metric name.


Key changed/added classes in this PR
  • PrometheusEmitter
  • PrometheusEmitterTest
  • DimensionsAndCollector

This PR has:

  • been self-reviewed.
  • added documentation for new or modified features or behaviors.
  • a release note entry in the PR description.
  • added Javadocs for most classes and all non-trivial methods. Linked related entities via Javadoc links.
  • added or updated version, license, or notice information in licenses.yaml
  • added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold for code coverage is met.
  • added integration tests.
  • been tested in a test Druid cluster.

@kfaraz
kfaraz requested review from abhishekrb19 and kfaraz October 24, 2025 07:52

@abhishekrb19 abhishekrb19 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aho135! Did a high level review and left some thoughts

@@ -33,7 +37,7 @@ public class DimensionsAndCollector
private final SimpleCollector collector;
private final double conversionFactor;
private final double[] histogramBuckets;
private final Stopwatch updateTimer;
private final ConcurrentHashMap<List<String>, Stopwatch> labelValuesToStopwatch;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you observe any memory pressure with this change? Previously there were some concerns on the memory usage with this approach, so it would be nice if you could share any observations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't done any in depth memory profiling for this. I think it largely depends on how many unique label combinations there are. For something like queryId I'd imagine the memory usage would be higher under heavy query load. But for dimensions like taskId the memory usage should be much lower. I think it roughly gets offset anyway because we are clearing the label from the underlying SimpleCollector map now

metric.getMillisSinceLastUpdate()
);
metric.getCollector().clear();
Set<List<String>> labelValuesCopy = new HashSet<>(metric.getLabelValuesToStopwatch().keySet());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is a copy of the map required here if the underlying map is already concurrent safe from using ConcurrentMap? We're also doing computeIfPresent() on the map entries whihh should be safe

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good point. Felt a little dangerous to be iterating over while removing keys, but I think the consistency guarantees of the ConcurrentMap make this safe

"Metric should be expired",
testMetric.removeIfExpired(Arrays.asList("historical", "druid.test.cn", "historical1"))
);
Assert.assertFalse(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for these tests!

From a functional PoV, it would be better to verify the end state of the collector, that the metrics are indeed cleared, rather than calling the internal function removeIfExpired(); as it is, I think we don't have coverage for cleanUpStaleMetrics().

What do you think of something like:

  • emitter.emit(metric1 - labelset1)
  • emitter.emit(metric1 - labelset2)
  • emitter.emit(metric1 - labelset1)

verify that metric1 - labelset1 is not cleared from the collector, whereas metric1 - labelset2 is cleared.

I think we could leverage the SimpleCollector's samples for verification. We could also pass BlockingExecutorService from the test to the prometheus emitter to control the executions (a bunch of tests do this to have better control of async executors). Please let me know what you think.

@aho135 aho135 Oct 27, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed. Ideally I'd wanted to check SimpleCollector.children and ensure that the labels were removed by cleanUpStaleMetrics(). Unfortunately the SimpleCollector doesn't expose any function to access the children Map. I did verify via the debugger that the entry is removed from the child map after the TTL though

The tests do actually run cleanUpStaleMetrics() because the executor is created in emitter.start() and manual sleep times allow the scheduled executor to run

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case does roughly model the behavior you suggested:

Except I update metric1 - labelset2 instead

emitter.emit(metric1 - labelset1)
emitter.emit(metric1 - labelset2)
emitter.emit(metric1 - labelset2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, the SimpleCollector exposes a collect() API to get the metrics collected/samples. Perhaps something like this should work?

List<Collector.MetricFamilySamples.Sample> samples = testMetric.getCollector().collect().get(0).samples;
// assert the labels & the sample size is 1 during a valid period

After the TTL period, the sample size would be 0? I think that would give us sufficient functionality validation on cleanUpStaleMetrics()

"Metric should be expired",
testMetric.removeIfExpired(Arrays.asList("historical", "druid.test.cn", "historical1"))
);
Assert.assertFalse(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, the SimpleCollector exposes a collect() API to get the metrics collected/samples. Perhaps something like this should work?

List<Collector.MetricFamilySamples.Sample> samples = testMetric.getCollector().collect().get(0).samples;
// assert the labels & the sample size is 1 during a valid period

After the TTL period, the sample size would be 0? I think that would give us sufficient functionality validation on cleanUpStaleMetrics()

| `druid.emitter.prometheus.addServiceAsLabel` | Flag to include the druid service name (e.g. `druid/broker`, `druid/coordinator`, etc.) as a prometheus label. | no | false |
| `druid.emitter.prometheus.pushGatewayAddress` | Pushgateway address. Required if using `pushgateway` strategy. | no | none |
| `druid.emitter.prometheus.flushPeriod` | When using the `pushgateway` strategy metrics are emitted every `flushPeriod` seconds. <br/>When using the `exporter` strategy this configures the metric TTL such that if the metric value is not updated within `flushPeriod` seconds then it will stop being emitted. Note that unique label combinations per metric are currently not subject to TTL expiration. It is recommended to set this to at least 3 * `scrape_interval`. | Required if `pushgateway` strategy is used, optional otherwise. | 15 seconds for `pushgateway` strategy. <br/>None for `exporter` strategy. |
| `druid.emitter.prometheus.flushPeriod` | When using the `pushgateway` strategy metrics are emitted every `flushPeriod` seconds. <br/>When using the `exporter` strategy this configures the metric TTL such that if the metric value is not updated within `flushPeriod` seconds then it will stop being emitted. It is recommended to set this to at least 3 * `scrape_interval`. | Required if `pushgateway` strategy is used, optional otherwise. | 15 seconds for `pushgateway` strategy. <br/>None for `exporter` strategy. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perhaps amend this to say that this reset also applies to unique label combinations?

@abhishekrb19 abhishekrb19 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm, some minor comments. Thanks, @aho135!

@abhishekrb19

Copy link
Copy Markdown
Contributor

@aho135 when you get a chance, could you please pull the latest master to this branch? That should fix the IT failures due to openjdk version.

@abhishekrb19

Copy link
Copy Markdown
Contributor

The failing embedded test is unrelated to this PR. It's been flakey on a few other PRs, a retry didn't help:

Error:  Tests run: 5, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 246.174 s <<< FAILURE! - in org.apache.druid.testing.embedded.docker.IngestionDockerTest
Error:  test_runIndexTask_andKillData  Time elapsed: 128.148 s  <<< ERROR!
org.apache.druid.java.util.common.ISE: Timed out waiting for event

cc: @kfaraz

@abhishekrb19
abhishekrb19 merged commit 5f70c83 into apache:master Nov 4, 2025
88 of 90 checks passed
@kfaraz

kfaraz commented Nov 5, 2025

Copy link
Copy Markdown
Contributor

Thanks, @abhishekrb19 ! I think this test flakes out every now and then.
Will try to debug this soon.

abhishekrb19 pushed a commit that referenced this pull request Nov 5, 2025
…8718)

Follow up to #18689 to not track labels if the TTL isn't set



Co-authored-by: Andrew Ho <[email protected]>
@kgyrtkirk kgyrtkirk added this to the 36.0.0 milestone Jan 19, 2026
riovic918data pushed a commit to riovic918data/druid that referenced this pull request Jun 12, 2026
Fixes apache#14638

This is an extension of apache#18598 to enhance the TTL to track metrics at the label level instead of just the metric level. This should allow operators to configure medium cardinality dimensions on the prometheus-emitter without putting pressure on the Prometheus collector/sub-system.
riovic918data pushed a commit to riovic918data/druid that referenced this pull request Jun 12, 2026
…ache#18718)

Follow up to apache#18689 to not track labels if the TTL isn't set



Co-authored-by: Andrew Ho <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prometheus-emitter metrics ttl

4 participants