Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[receiver/prometheusreceiver] implement append native histogram #28663

Merged
merged 47 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ae09fe9
prometheusreceiver: first step towards being able to append native hi…
krajorama Oct 27, 2023
1dd30ef
Start adding some unit for transaction.go
krajorama Nov 4, 2023
2ab22e6
Add buckets conversion logic
krajorama Nov 14, 2023
cb6eee8
Set timestamps and attribues
krajorama Nov 4, 2023
94b13af
Add to adjustmetrics and prefer exponential histograms
krajorama Nov 5, 2023
26ad5e0
Fix lint error
krajorama Nov 5, 2023
7ed1c48
More lint fixes
krajorama Nov 5, 2023
8a20021
Update startTimeMetricAdjuster
krajorama Nov 10, 2023
7ff5be1
Improve comment
krajorama Nov 10, 2023
2c35f51
Add first e2e test
krajorama Nov 14, 2023
db01595
Fix and test scrape_classic_histograms scrape option basic handling
krajorama Nov 14, 2023
68614ab
Remove extra logs not needed anymore
krajorama Nov 14, 2023
e7334cf
Remove unused import
krajorama Nov 14, 2023
068fa0e
Add unit tests for initial point adjuster on exponential histograms
krajorama Nov 15, 2023
5ff25f4
Handle exponential histogram staleness marker
krajorama Nov 15, 2023
8b41107
Add unit test and fix for staleness marker
krajorama Nov 15, 2023
e8e2b8b
Fix heuristics around staleness marker
krajorama Nov 16, 2023
f259787
Drop unsupported gauge histograms
krajorama Nov 16, 2023
b10838a
Update readme
krajorama Nov 17, 2023
ec930f3
Linter induced test fixes
krajorama Nov 17, 2023
9618f5a
Apply suggestions from code review
krajorama Nov 21, 2023
06838f7
Handle float histograms in addExponentialHistogramSeries
krajorama Nov 21, 2023
42bc0c1
Fix consistency in toExponentialHistogramDataPoints
krajorama Nov 21, 2023
5e10890
Fix using Prometheus Config
krajorama Mar 1, 2024
1d36a8e
Add setting ZeroThreshold in exponential histogram
krajorama Mar 16, 2024
27c9f52
Fix: created timestamp was not correctly added for exponential histog…
krajorama Mar 16, 2024
86a73fa
Add feature gate receiver.prometheusreceiver.EnableNativeHistograms
krajorama Mar 17, 2024
5271dbf
Fix typo
krajorama Mar 17, 2024
a6b49f2
Merge branch 'main' into implement-appendhistogram
krajorama Mar 17, 2024
c4211f9
Run go mod tidy
krajorama Mar 17, 2024
d4daf7c
Fix linter issues
krajorama Mar 17, 2024
1e50b6a
Merge branch 'main' into implement-appendhistogram
krajorama Mar 20, 2024
92c2bcc
Enforce scraping classic histograms if native histograms feature is off
krajorama Mar 20, 2024
8d09788
Merge branch 'main' into implement-appendhistogram
krajorama Mar 25, 2024
a4695d9
Updates from review comments.
krajorama Mar 25, 2024
8bd610f
Merge branch 'main' into implement-appendhistogram
krajorama Mar 26, 2024
c3b2596
Followup merge from main
krajorama Mar 26, 2024
8d33a89
Tweak readmes
krajorama Mar 26, 2024
7ec9dc0
Add check on schema in metrics_receiver_protobuf_test.go
krajorama Mar 27, 2024
0ecd615
Merge branch 'main' into implement-appendhistogram
krajorama Mar 27, 2024
c399b73
Adopt 31908
krajorama Mar 27, 2024
90c2810
Fix linting
krajorama Mar 27, 2024
3c5ae66
Merge branch 'main' into implement-appendhistogram
krajorama Apr 4, 2024
27e61c5
Remove unsupported option enable_protobuf_negotiation
krajorama Apr 5, 2024
a67e214
Merge branch 'main' into implement-appendhistogram
krajorama Apr 5, 2024
58be349
Merge branch 'main' into implement-appendhistogram
krajorama Apr 8, 2024
79ec8c9
Merge branch 'main' into implement-appendhistogram
dashpole Apr 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .chloggen/prometheusreceiver-append-native-histogram.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: prometheusreceiver

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Allows receiving prometheus native histograms

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [26555]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: |
- Native histograms are compatible with OTEL exponential histograms.
- The feature can be enabled via the feature gate `receiver.prometheusreceiver.EnableNativeHistograms`.
Run the collector with the command line option `--feature-gates=receiver.prometheusreceiver.EnableNativeHistograms`.
- Currently the feature also requires that targets are scraped via the ProtoBuf format.
To start scraping native histograms, set
`config.global.scrape_protocols` to `[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]` in the
receiver configuration. This requirement will be lifted once Prometheus can scrape native histograms over text formats.
- For more up to date information see the README.md file of the receiver at
https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/receiver/prometheusreceiver/README.md#prometheus-native-histograms.

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: []
23 changes: 23 additions & 0 deletions receiver/prometheusreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ prometheus --config.file=prom.yaml
"--feature-gates=receiver.prometheusreceiver.UseCreatedMetric"
```

- `receiver.prometheusreceiver.EnableNativeHistograms`: process and turn native histogram metrics into OpenTelemetry exponential histograms. For more details consult the [Prometheus native histograms](#prometheus-native-histograms) section.

```shell
"--feature-gates=receiver.prometheusreceiver.EnableNativeHistograms"
```

- `report_extra_scrape_metrics`: Extra Prometheus scrape metrics can be reported by setting this parameter to `true`

You can copy and paste that same configuration under:
Expand Down Expand Up @@ -106,6 +112,7 @@ The prometheus receiver also supports additional top-level options:
- **trim_metric_suffixes**: [**Experimental**] When set to true, this enables trimming unit and some counter type suffixes from metric names. For example, it would cause `singing_duration_seconds_total` to be trimmed to `singing_duration`. This can be useful when trying to restore the original metric names used in OpenTelemetry instrumentation. Defaults to false.
- **use_start_time_metric**: When set to true, this enables retrieving the start time of all counter metrics from the process_start_time_seconds metric. This is only correct if all counters on that endpoint started after the process start time, and the process is the only actor exporting the metric after the process started. It should not be used in "exporters" which export counters that may have started before the process itself. Use only if you know what you are doing, as this may result in incorrect rate calculations. Defaults to false.
- **start_time_metric_regex**: The regular expression for the start time metric, and is only applied when use_start_time_metric is enabled. Defaults to process_start_time_seconds.
- **enable_protobuf_negotiation**: When set to true, Prometheus receiver will try to negotiate the protobuf format first for scraping the target. This is considered experimental.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this something we will eventually enable by default? Will we ever want it always-on? A feature-gate might be better if we want this to eventually always be enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Prometheus will enable two things by default eventually (I'd guess next year):

  • try Protobuf negotiation first (since it's more efficient than text)
  • allow native histograms (currently feature flag)

Actually the text format for native histograms is being worked on currently, so again eventually it will not matter what you set protobuf negotiation to.

Given the current code, to follow Prometheus, the default for enable_protobuf_negotiation option should be switched to true in otel collector as well when Prometheus makes the change.

The real "problem" will be native histograms being on by default. Since for a metric that has both classic and native buckets, we can only scrape one due to the name conflict (both result in the same metric name). So if we start enabling native histograms by default, suddenly the classic buckets would be gone, replaced by exponential buckets.

Copy link
Contributor

Choose a reason for hiding this comment

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

So, we would want our eventual configuration to only include a knob for switching between fixed-bucket and exponential histogram, right? Do we need to keep config for protobuf negotiation around long-term? It sounds like we essentially just want to keep the default histogram format as fixed-bucket, rather than exponential

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, we would want our eventual configuration to only include a knob for switching between fixed-bucket and exponential histogram, right? Do we need to keep config for protobuf negotiation around long-term? It sounds like we essentially just want to keep the default histogram format as fixed-bucket, rather than exponential

I guess so. The question that comes up for Prometheus is different, because you can have the classic histograms beside the native histograms (as names are different). Still the issue is similar in that if we enable native histograms by default, you suddenly have a breaking change by switching from classic buckets to native buckets. And maybe we don't need an extra setting since you can set scrape_classic_histograms in the scrape config. In Prometheus that gets you the classic buckets as well and in otel collector it would get you the classic buckets only. At least in the my current implementation.

The whole thing only affects cases where the metric has both classic and native defined.

cc @beorn7

Copy link
Contributor

Choose a reason for hiding this comment

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

Enabling protobuf negotiation can now be done using the prometheus scrape config. See #30934.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will wait for PR #30934 and rebase.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be removed now


For example,

Expand All @@ -115,6 +122,7 @@ receivers:
trim_metric_suffixes: true
use_start_time_metric: true
start_time_metric_regex: foo_bar_.*
enable_protobuf_negotiation: true
config:
scrape_configs:
- job_name: 'otel-collector'
Expand All @@ -123,6 +131,21 @@ receivers:
- targets: ['0.0.0.0:8888']
```

## Prometheus native histograms

Native histograms are an experimental [feature](https://prometheus.io/docs/prometheus/latest/feature_flags/#native-histograms) of Prometheus.

To start scraping native histograms, set `config.global.scrape_protocols` to `[ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]`
in the receiver configuration. This requirement will be lifted once Prometheus can scrape native histograms over text formats.

To enable converting native histograms to OpenTelemetry exponential histograms, enable the feature gate `receiver.prometheusreceiver.EnableNativeHistograms`.
The feature is considered experimental.

This feature applies to the most common integer counter histograms, gauge histograms are dropped.
In case a metric has both the conventional (aka classic) buckets and also native histogram buckets, only the native histogram buckets will be
taken into account to create the corresponding exponential histogram. To scrape the classic buckets instead use the
[scrape option](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) `scrape_classic_histograms`.

## OpenTelemetry Operator
Additional to this static job definitions this receiver allows to query a list of jobs from the
OpenTelemetryOperators TargetAllocator or a compatible endpoint.
Expand Down
8 changes: 8 additions & 0 deletions receiver/prometheusreceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ var useCreatedMetricGate = featuregate.GlobalRegistry().MustRegister(
" retrieve the start time for Summary, Histogram and Sum metrics from _created metric"),
)

var enableNativeHistogramsGate = featuregate.GlobalRegistry().MustRegister(
"receiver.prometheusreceiver.EnableNativeHistograms",
featuregate.StageAlpha,
featuregate.WithRegisterDescription("When enabled, the Prometheus receiver will convert"+
" Prometheus native histograms to OTEL exponential histograms and ignore"+
" those Prometheus classic histograms that have a native histogram alternative"),
)

// NewFactory creates a new Prometheus receiver factory.
func NewFactory() receiver.Factory {
return receiver.NewFactory(
Expand Down
33 changes: 18 additions & 15 deletions receiver/prometheusreceiver/internal/appendable.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import (

// appendable translates Prometheus scraping diffs into OpenTelemetry format.
type appendable struct {
sink consumer.Metrics
metricAdjuster MetricsAdjuster
useStartTimeMetric bool
trimSuffixes bool
startTimeMetricRegex *regexp.Regexp
externalLabels labels.Labels
sink consumer.Metrics
metricAdjuster MetricsAdjuster
useStartTimeMetric bool
enableNativeHistograms bool
trimSuffixes bool
startTimeMetricRegex *regexp.Regexp
externalLabels labels.Labels

settings receiver.CreateSettings
obsrecv *receiverhelper.ObsReport
Expand All @@ -36,6 +37,7 @@ func NewAppendable(
useStartTimeMetric bool,
startTimeMetricRegex *regexp.Regexp,
useCreatedMetric bool,
enableNativeHistograms bool,
externalLabels labels.Labels,
trimSuffixes bool) (storage.Appendable, error) {
var metricAdjuster MetricsAdjuster
Expand All @@ -51,17 +53,18 @@ func NewAppendable(
}

return &appendable{
sink: sink,
settings: set,
metricAdjuster: metricAdjuster,
useStartTimeMetric: useStartTimeMetric,
startTimeMetricRegex: startTimeMetricRegex,
externalLabels: externalLabels,
obsrecv: obsrecv,
trimSuffixes: trimSuffixes,
sink: sink,
settings: set,
metricAdjuster: metricAdjuster,
useStartTimeMetric: useStartTimeMetric,
enableNativeHistograms: enableNativeHistograms,
startTimeMetricRegex: startTimeMetricRegex,
externalLabels: externalLabels,
obsrecv: obsrecv,
trimSuffixes: trimSuffixes,
}, nil
}

func (o *appendable) Appender(ctx context.Context) storage.Appender {
return newTransaction(ctx, o.metricAdjuster, o.sink, o.externalLabels, o.settings, o.obsrecv, o.trimSuffixes)
return newTransaction(ctx, o.metricAdjuster, o.sink, o.externalLabels, o.settings, o.obsrecv, o.trimSuffixes, o.enableNativeHistograms)
}
163 changes: 161 additions & 2 deletions receiver/prometheusreceiver/internal/metricfamily.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/scrape"
Expand Down Expand Up @@ -49,6 +50,8 @@ type metricGroup struct {
hasSum bool
created float64
value float64
hValue *histogram.Histogram
fhValue *histogram.FloatHistogram
complexValue []*dataPoint
exemplars pmetric.ExemplarSlice
}
Expand Down Expand Up @@ -156,6 +159,118 @@ func (mg *metricGroup) toDistributionPoint(dest pmetric.HistogramDataPointSlice)
mg.setExemplars(point.Exemplars())
}

// toExponentialHistogramDataPoints is based on
// https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#exponential-histograms
func (mg *metricGroup) toExponentialHistogramDataPoints(dest pmetric.ExponentialHistogramDataPointSlice) {
if !mg.hasCount {
return
}
point := dest.AppendEmpty()
point.SetTimestamp(timestampFromMs(mg.ts))

// We do not set Min or Max as native histograms don't have that information.
switch {
case mg.fhValue != nil:
fh := mg.fhValue

if value.IsStaleNaN(fh.Sum) {
point.SetFlags(pmetric.DefaultDataPointFlags.WithNoRecordedValue(true))
// The count and sum are initialized to 0, so we don't need to set them.
} else {
point.SetScale(fh.Schema)
// Input is a float native histogram. This conversion will lose
// precision,but we don't actually expect float histograms in scrape,
// since these are typically the result of operations on integer
// native histograms in the database.
point.SetCount(uint64(fh.Count))
point.SetSum(fh.Sum)
point.SetZeroThreshold(fh.ZeroThreshold)
point.SetZeroCount(uint64(fh.ZeroCount))

if len(fh.PositiveSpans) > 0 {
point.Positive().SetOffset(fh.PositiveSpans[0].Offset - 1) // -1 because OTEL offset are for the lower bound, not the upper bound
convertAbsoluteBuckets(fh.PositiveSpans, fh.PositiveBuckets, point.Positive().BucketCounts())
}
if len(fh.NegativeSpans) > 0 {
point.Negative().SetOffset(fh.NegativeSpans[0].Offset - 1) // -1 because OTEL offset are for the lower bound, not the upper bound
convertAbsoluteBuckets(fh.NegativeSpans, fh.NegativeBuckets, point.Negative().BucketCounts())
}
}

case mg.hValue != nil:
h := mg.hValue

if value.IsStaleNaN(h.Sum) {
point.SetFlags(pmetric.DefaultDataPointFlags.WithNoRecordedValue(true))
// The count and sum are initialized to 0, so we don't need to set them.
} else {
point.SetScale(h.Schema)
point.SetCount(h.Count)
point.SetSum(h.Sum)
point.SetZeroThreshold(h.ZeroThreshold)
point.SetZeroCount(h.ZeroCount)

if len(h.PositiveSpans) > 0 {
point.Positive().SetOffset(h.PositiveSpans[0].Offset - 1) // -1 because OTEL offset are for the lower bound, not the upper bound
convertDeltaBuckets(h.PositiveSpans, h.PositiveBuckets, point.Positive().BucketCounts())
}
if len(h.NegativeSpans) > 0 {
point.Negative().SetOffset(h.NegativeSpans[0].Offset - 1) // -1 because OTEL offset are for the lower bound, not the upper bound
convertDeltaBuckets(h.NegativeSpans, h.NegativeBuckets, point.Negative().BucketCounts())
}
}

default:
// This should never happen.
return
}

tsNanos := timestampFromMs(mg.ts)
if mg.created != 0 {
point.SetStartTimestamp(timestampFromFloat64(mg.created))
} else {
// metrics_adjuster adjusts the startTimestamp to the initial scrape timestamp
point.SetStartTimestamp(tsNanos)
}
point.SetTimestamp(tsNanos)
populateAttributes(pmetric.MetricTypeHistogram, mg.ls, point.Attributes())
mg.setExemplars(point.Exemplars())
}

func convertDeltaBuckets(spans []histogram.Span, deltas []int64, buckets pcommon.UInt64Slice) {
dashpole marked this conversation as resolved.
Show resolved Hide resolved
buckets.EnsureCapacity(len(deltas))
bucketIdx := 0
bucketCount := int64(0)
for spanIdx, span := range spans {
if spanIdx > 0 {
for i := int32(0); i < span.Offset; i++ {
buckets.Append(uint64(0))
}
}
for i := uint32(0); i < span.Length; i++ {
bucketCount += deltas[bucketIdx]
bucketIdx++
buckets.Append(uint64(bucketCount))
}
}
}

func convertAbsoluteBuckets(spans []histogram.Span, counts []float64, buckets pcommon.UInt64Slice) {
buckets.EnsureCapacity(len(counts))
bucketIdx := 0
for spanIdx, span := range spans {
if spanIdx > 0 {
for i := int32(0); i < span.Offset; i++ {
buckets.Append(uint64(0))
}
}
for i := uint32(0); i < span.Length; i++ {
buckets.Append(uint64(counts[bucketIdx]))
bucketIdx++
}
}
}

func (mg *metricGroup) setExemplars(exemplars pmetric.ExemplarSlice) {
if mg == nil {
return
Expand Down Expand Up @@ -296,13 +411,17 @@ func (mf *metricFamily) addSeries(seriesRef uint64, metricName string, ls labels
}
mg.complexValue = append(mg.complexValue, &dataPoint{value: v, boundary: boundary})
}
case pmetric.MetricTypeExponentialHistogram:
if metricName == mf.metadata.Metric+metricSuffixCreated {
mg.created = v
}
case pmetric.MetricTypeSum:
if metricName == mf.metadata.Metric+metricSuffixCreated {
mg.created = v
} else {
mg.value = v
}
case pmetric.MetricTypeEmpty, pmetric.MetricTypeGauge, pmetric.MetricTypeExponentialHistogram:
case pmetric.MetricTypeEmpty, pmetric.MetricTypeGauge:
fallthrough
default:
mg.value = v
Expand All @@ -311,6 +430,37 @@ func (mf *metricFamily) addSeries(seriesRef uint64, metricName string, ls labels
return nil
}

func (mf *metricFamily) addExponentialHistogramSeries(seriesRef uint64, metricName string, ls labels.Labels, t int64, h *histogram.Histogram, fh *histogram.FloatHistogram) error {
mg := mf.loadMetricGroupOrCreate(seriesRef, ls, t)
if mg.ts != t {
return fmt.Errorf("inconsistent timestamps on metric points for metric %v", metricName)
}
if mg.mtype != pmetric.MetricTypeExponentialHistogram {
return fmt.Errorf("metric type mismatch for exponential histogram metric %v type %s", metricName, mg.mtype.String())
}
switch {
case fh != nil:
if mg.hValue != nil {
return fmt.Errorf("exponential histogram %v already has float counts", metricName)
}
mg.count = fh.Count
mg.sum = fh.Sum
mg.hasCount = true
mg.hasSum = true
mg.fhValue = fh
case h != nil:
if mg.fhValue != nil {
return fmt.Errorf("exponential histogram %v already has integer counts", metricName)
}
mg.count = float64(h.Count)
mg.sum = h.Sum
mg.hasCount = true
mg.hasSum = true
mg.hValue = h
}
return nil
}

func (mf *metricFamily) appendMetric(metrics pmetric.MetricSlice, trimSuffixes bool) {
metric := pmetric.NewMetric()
// Trims type and unit suffixes from metric name
Expand Down Expand Up @@ -352,7 +502,16 @@ func (mf *metricFamily) appendMetric(metrics pmetric.MetricSlice, trimSuffixes b
}
pointCount = sdpL.Len()

case pmetric.MetricTypeEmpty, pmetric.MetricTypeGauge, pmetric.MetricTypeExponentialHistogram:
case pmetric.MetricTypeExponentialHistogram:
histogram := metric.SetEmptyExponentialHistogram()
histogram.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
hdpL := histogram.DataPoints()
for _, mg := range mf.groupOrders {
mg.toExponentialHistogramDataPoints(hdpL)
}
pointCount = hdpL.Len()

case pmetric.MetricTypeEmpty, pmetric.MetricTypeGauge:
fallthrough
default: // Everything else should be set to a Gauge.
gauge := metric.SetEmptyGauge()
Expand Down
Loading