feat(metrics): Add native histograms and Prometheus protobuf exposition - #856
ethanolchik wants to merge 4 commits into
Conversation
| * @returns {Function} aggregator function | ||
| */ | ||
| function AggregatorFactory(aggregatorFn) { | ||
| function AggregatorFactory(aggregatorFn, nativeAggregatorFn) { |
There was a problem hiding this comment.
I don't see why you need the second parameter.
There was a problem hiding this comment.
I added it so sum and first can pass their native histogram aggregators while keeping the existing classic histogram callbacks unchanged. The existing callbacks returns numbers whereas the native callbacks return native histogram snapshots. It's an optional field but I'm happy to structure it differently.
There was a problem hiding this comment.
I'd be curious to see how the other clients solve this. Do we need to reuse the aggregators across two incompatible types? I'll see if I can look this up.
| function AggregatorFactory(aggregatorFn, nativeAggregatorFn) { | ||
| return metrics => { | ||
| if (metrics.length === 0) return; | ||
| const hasNativeHistograms = metrics.some( |
There was a problem hiding this comment.
And this check runs on every single metric, on every single scrape. This is not when and where to do a sanity check. That should be farther up the chain.
Also why would this happen? You get one histogram with a particular name. Either all of the values will be natives or they won't, right?
There was a problem hiding this comment.
Yeah true, this was probably more defensive than necessary. I've changed it so it uses the first snapshot only.
| if (defaultLabelNames !== undefined) { | ||
| for (const labelName of defaultLabelNames) { | ||
| seriesLabels[labelName] ??= this._defaultLabels[labelName]; | ||
| if ( |
There was a problem hiding this comment.
This fixes a case where a histogram label is null or undefined and the registry has a default for that label. The existing line applies the default to seriesLabels, but the formatter uses the value from sharedLabels instead, so the default is ignored.
I've moved this change into a separate commit in line with your other comment.
| } | ||
|
|
||
| async getMetricsAsString(metrics) { | ||
| async getMetricsAsString(metrics, contentType = this.contentType) { |
There was a problem hiding this comment.
since encodeMetricFamily doesn't return a string, this is not the way to wire this up.
You're also returning from the middle of a function now.
There was a problem hiding this comment.
Ah yeah true, that's my bad. I've moved protobuf encoding into metrics() where the output format is selected.
| if (contentType === Registry.PROMETHEUS_PROTOBUF_CONTENT_TYPE) { | ||
| const { encodeMetricFamily } = require('./protobuf'); | ||
| return encodeMetricFamily(metric, this._defaultLabels); | ||
| } |
There was a problem hiding this comment.
This should be hoisted up to the calling function, which fixes the early exit and the mismatched function name and method signature.
There was a problem hiding this comment.
Ok sure, I've moved this into metrics().
| return encodeMetricFamily(metric, this._defaultLabels); | ||
| } | ||
|
|
||
| const isOpenMetrics = contentType === Registry.OPENMETRICS_CONTENT_TYPE; |
There was a problem hiding this comment.
You've got way too many changes in a single commit. I don't see how this one or the one I questioned below are part of native histograms. I know some devs prefer large commits that tie to the ticket, but I've yet to meet one who actually does forensics in git history so I'm pretty sure that's a Chesterton's Fence situation.
If you're going to fix other bugs in a driveby I'd prefer the be done as a separate commit in the same PR. It'll also help because there's already an open PR touching some of the label code and this is going to make a hash of things.
|
So am I correct in thinking that native histograms are incompatible with the default Prometheus data type? If so then I'm not sure how to keep this compatible with |
faff229 to
6924fdf
Compare
Well, I though that because |
|
Can you rebase on main? There were some benchmark changes and another PR that may or may not conflict, and I'm interested in seeing if this slows down metrics() |
Signed-off-by: Ethan Olchik <eitan.olchik@gmail.com>
Signed-off-by: Ethan Olchik <eitan.olchik@gmail.com>
Extend Histogram with opt-in exponential native buckets, exemplars, and resolution reduction. Preserve native snapshots through registry, worker, and cluster aggregation. Add protobuf exposition for all existing metric types, binary registry return types, schema generation, documentation, and an HTTP example. Fixes prometheus#576 Assisted-by: Codex Signed-off-by: Ethan Olchik <eolchik@cloudflare.com>
6924fdf to
527ad33
Compare
rebased |
|
Thanks! I finally took a peek at the python code. The aggregation is pretty much only for multiprocess work, so we are free to name aggregators anything we want to name them. I suspect the simpler solution here is to introduce new aggregators for the native histograms, and establish a default aggregator for those types. So a 'sum-native' or 'sumNative' type instead of changing the factory function to take 2 parameters one of which is never used for 99% of all stats generated. |
|
Yeah that sounds simpler, thanks for that! I'll give native histograms their own aggregators and default them to |
Signed-off-by: Ethan Olchik <eitan.olchik@gmail.com>
krajorama
left a comment
There was a problem hiding this comment.
I've looked at this from native histograms point of view - I've worked with native histograms for the past 3 years in Prometheus.
LGTM from native histograms point of view, with a few caveats:
- one can implement more sophisticated ways to keep the number of buckets down and try to avoid reducing the resolution (called coarsen here), which is done in client_golang. Probably fine for now, but should be explained more in the README that there is no protection if a histogram is saturated other than restarting the program or implementing some monitoring and reset over histograms.
- for exemplars it would be nice if the number was configurable and maybe the client_golang algorithm was applied to get a distribution that's more likely to keep outliers - which we assumed are more valuable insight - nothing to do in this PR though
- classic and native histograms can observer NaN and +- infinity, but it's an existing limitation that they are rejected - nothing to do in this PR
(Also I think this PR is a bit large, it could be split up into at least: 2 PRs for the two bugfixes in the first two commit, a PR for adding Protobuf and a PR for adding native histograms).
| a zero bucket. The default zero threshold is `2 ** -128`, configurable with | ||
| `nativeHistogramZeroThreshold`. The default budget of 160 populated buckets per | ||
| label set can be configured with `nativeHistogramMaxBucketNumber` (0 disables | ||
| the budget). When needed, resolution is reduced down to schema -4; at that |
There was a problem hiding this comment.
nit:
| the budget). When needed, resolution is reduced down to schema -4; at that | |
| the budget). When needed, resolution is progressively reduced down to schema -4; at that |
| this.zeroCount = 0; | ||
| this.positiveBuckets = new Map(); | ||
| this.negativeBuckets = new Map(); | ||
| this.createdTimestamp = nowTimestamp(); |
There was a problem hiding this comment.
I'm loving this, but it means counters and summaries and classic histograms are now different and don't have created timestamp. So maybe this should be commented out with a TODO() on top.
| } | ||
|
|
||
| /** | ||
| * Initialize the metrics for the given combination of labels to zero. |
There was a problem hiding this comment.
I think this function needs a warning that prohibits usage in flight when there's also sum or sumNative aggregation being used. The reason is that if you reset a histogram in the sum*, then the sum is no longer guaranteed to be monotonic. This is best illustrated if you imagine having zero() function on counters. Assume you have two counters that are at count==1 , then you reset of them and leave it at 0. The sum will look like: 2 2 2 2 1 1 1 1 . Which means that PromQL will measure an increase of 1 over this time range as it will detect a reset at the 5th sample.
So what you'd actually have to do is to reset all counters/histograms that you aggregate.
Applications currently cannot expose native histogram samples from this client. This adds opt-in native collection to
Histogramand the Prometheus protobuf exposition needed to scrape it.Fixes #576.
Existing histogram configurations retain classic behavior. Native histograms can retain explicit classic buckets for migration, or use
buckets: []for native-only protobuf output. Protobuf registry methods return aBuffer, represented asUint8Arrayin the public TypeScript declarations.The implementation includes:
sum,first, andomitaggregation through registries, clusters, and workers, including reconciliation of different schemas and zero thresholds.protobufjs/lightuses the checked-inlib/metrics.jsondescriptor;lib/metrics.protoandnpm run generate-protobufmake its source and regeneration available.Validation performed locally:
checkworkflow passed throughacton Node 24.21.0: ESLint, Prettier, and TypeScript.npm run benchmarkscompleted on macOS/Node 26.4.0. A focused registry comparison against upstream, with increased sampling, found no significant regression above a 5% threshold; default-label cases measured roughly 2–4% overhead.The existing benchmark suite covers classic metrics. Dedicated native workload benchmarks and application-specific rollout validation remain follow-up work. Applications select the protobuf response format themselves; HTTP Accept negotiation is outside this change.
Developed with AI assistance (Codex), also disclosed in the commit trailer.