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

Prometheus receiver spec compliance tracker #25865

Closed
dashpole opened this issue Aug 17, 2023 · 18 comments
Closed

Prometheus receiver spec compliance tracker #25865

dashpole opened this issue Aug 17, 2023 · 18 comments
Assignees
Labels
needs triage New item requiring triage receiver/prometheus Prometheus receiver

Comments

@dashpole dashpole added the needs triage New item requiring triage label Aug 17, 2023
@dashpole dashpole self-assigned this Aug 17, 2023
@dashpole
Copy link
Contributor Author

@open-telemetry/wg-prometheus

@dashpole
Copy link
Contributor Author

dashpole commented Aug 17, 2023

Metric Name

The OpenMetrics MetricFamily Name MUST be added as the Name of the OTLP metric. By default, the name MUST be unaltered, but translation SHOULD provide configuration which, when enabled, removes type (e.g. _total) and unit (e.g. _seconds) suffixes.

Configuration to trim suffixes:

TrimMetricSuffixes bool `mapstructure:"trim_metric_suffixes"`

Name is unaltered by default, unless configured to trim suffixes:

name := mf.name
if trimSuffixes {
name = prometheus.TrimPromSuffixes(name, mf.mtype, mf.metadata.Unit)
}
metric.SetName(name)

This is compliant with the specification

@dashpole
Copy link
Contributor Author

dashpole commented Aug 17, 2023

Metric Unit

The OpenMetrics UNIT metadata, if present, MUST be converted to the unit of the OTLP metric. The unit SHOULD be translated from Prometheus conventions to OpenTelemetry conventions by:

  • Converting from full words to abbreviations (e.g. "milliseconds" to "ms").
  • Special case: Converting "ratio" to "1".
  • Converting "foo_per_bar" to "foo/bar".

Unit is converted to the unit of the OTLP:

The unit is NOT translated from Prometheus conventions to OTel conventions

Tracked by #23208

@dashpole
Copy link
Contributor Author

Metric Description

The OpenMetrics HELP metadata, if present, MUST be added as the description of the OTLP metric.

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Metric Type

The OpenMetrics TYPE metadata, if present, MUST be used to determine the OTLP data type, and dictates type-specific conversion rules listed below. Metric families without type metadata follow rules for unknown-typed metrics below.

The type is used to determine the OTel metric type. The individual types will be verified below:

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Counters

A Prometheus Counter MUST be converted to an OTLP Sum with is_monotonic equal to true.

Counters become monotonic sums:

case textparse.MetricTypeCounter:
// always use float64, as it's the internal data type used in prometheus
return pmetric.MetricTypeSum, true

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Gauges

A Prometheus Gauge MUST be converted to an OTLP Gauge.

case textparse.MetricTypeGauge, textparse.MetricTypeUnknown:
return pmetric.MetricTypeGauge, false

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Info and StateSet

An OpenMetrics Info metric MUST be converted to an OTLP Non-Monotonic Sum unless it is the target_info metric, which is used to populate resource attributes. An OpenMetrics Info can be thought of as a special-case of the OpenMetrics Gauge which has a value of 1, and whose labels generally stays constant over the life of the process. It is converted to a Non-Monotonic Sum, rather than a Gauge, because the value of 1 is intended to be viewed as a count, which should be summed together when aggregating away labels.

An OpenMetrics StateSet metric MUST be converted to an OTLP Non-Monotonic Sum. An OpenMetrics StateSet can be thought of as a special-case of the OpenMetrics Gauge which has a 0 or 1 value, and has one metric point for every possible state. It is converted to a Non-Monotonic Sum, rather than a Gauge, because the value of 1 is intended to be viewed as a count, which should be summed together when aggregating away labels.

Info and StateSet become non-monotonic sums:

case textparse.MetricTypeInfo, textparse.MetricTypeStateset:
return pmetric.MetricTypeSum, false

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Unknown-typed

A Prometheus Unknown MUST be converted to an OTLP Gauge.

case textparse.MetricTypeGauge, textparse.MetricTypeUnknown:
return pmetric.MetricTypeGauge, false

This is compliant with the specification

@dashpole
Copy link
Contributor Author

dashpole commented Aug 17, 2023

Histograms

A Prometheus Histogram MUST be converted to an OTLP Histogram.

case textparse.MetricTypeHistogram:
return pmetric.MetricTypeHistogram, true

Multiple Prometheus histogram metrics MUST be merged together into a single OTLP Histogram:

  • The le label on the _bucket-suffixed metric is used to identify and order histogram bucket boundaries. Each Prometheus line produces one bucket count on the resulting histogram. Each value for the le label except +Inf produces one bucket boundary.

We do not validate that the histogram bucket series ends in _bucket. We assume that any series that isn't a sum, count, or created is the _bucket series. I think this is compliant with the specification.

le label is used for bucket boundary:

func getBoundary(metricType pmetric.MetricType, labels labels.Labels) (float64, error) {
val := ""
switch metricType {
case pmetric.MetricTypeHistogram:
val = labels.Get(model.BucketLabel)
if val == "" {
return 0, errEmptyLeLabel
}

The +inf bucket is ignored:

  • Lines with _count and _sum suffixes are used to determine the histogram's count and sum.

case pmetric.MetricTypeHistogram, pmetric.MetricTypeSummary:
switch {
case strings.HasSuffix(metricName, metricsSuffixSum):
mg.sum = v
mg.hasSum = true
case strings.HasSuffix(metricName, metricsSuffixCount):
// always use the timestamp from count, because is the only required field for histograms and summaries.
mg.ts = t
mg.count = v
mg.hasCount = true
case strings.HasSuffix(metricName, metricSuffixCreated):
mg.created = v
default:
boundary, err := getBoundary(mf.mtype, ls)
if err != nil {
return err
}
mg.complexValue = append(mg.complexValue, &dataPoint{value: v, boundary: boundary})
}

  • If _count is not present, the metric MUST be dropped.

func (mg *metricGroup) toDistributionPoint(dest pmetric.HistogramDataPointSlice) {
if !mg.hasCount {
return
}

  • If _sum is not present, the histogram's sum MUST be unset.

This is compliant with the specification

@dashpole
Copy link
Contributor Author

dashpole commented Aug 17, 2023

Summaries

Prometheus Summary MUST be converted to an OTLP Summary.

case textparse.MetricTypeSummary:
return pmetric.MetricTypeSummary, true

Multiple Prometheus metrics are merged together into a single OTLP Summary:

  • The quantile label on non-suffixed metrics is used to identify quantile points in summary metrics. Each Prometheus line produces one quantile on the resulting summary.

We use the quantile label:

case pmetric.MetricTypeSummary:
val = labels.Get(model.QuantileLabel)
if val == "" {
return 0, errEmptyQuantileLabel
}

It is used to populate the quantile of the resulting summary:

We do not validate that the quantile has no suffix. We do ensure it does not have other relevant suffixes (sum, count, created). I believe this still complies with the spec, as non-suffixed metrics are used to identify quantiles.

  • Lines with _count and _sum suffixes are used to determine the summary's count and sum.

case pmetric.MetricTypeHistogram, pmetric.MetricTypeSummary:
switch {
case strings.HasSuffix(metricName, metricsSuffixSum):
mg.sum = v
mg.hasSum = true
case strings.HasSuffix(metricName, metricsSuffixCount):
// always use the timestamp from count, because is the only required field for histograms and summaries.
mg.ts = t
mg.count = v
mg.hasCount = true
case strings.HasSuffix(metricName, metricSuffixCreated):
mg.created = v
default:
boundary, err := getBoundary(mf.mtype, ls)
if err != nil {
return err
}
mg.complexValue = append(mg.complexValue, &dataPoint{value: v, boundary: boundary})
}

  • If _count is not present, the metric MUST be dropped.

func (mg *metricGroup) toSummaryPoint(dest pmetric.SummaryDataPointSlice) {
// expecting count to be provided, however, in the following two cases, they can be missed.
// 1. data is corrupted
// 2. ignored by startValue evaluation
if !mg.hasCount {
return
}

  • If _sum is not present, the summary's sum MUST be set to zero.

The sum is unset, which is the same as setting to zero.

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Dropped Types

The following Prometheus types MUST be dropped:

case textparse.MetricTypeGaugeHistogram:
fallthrough
default:
// including: textparse.MetricTypeGaugeHistogram
return pmetric.MetricTypeEmpty, false
}

This is compliant with the specification

@dashpole
Copy link
Contributor Author

Start Time

Prometheus Cumulative metrics can include the start time using the _created metric as specified in OpenMetrics. When converting Prometheus Counters to OTLP, conversion SHOULD use _created where available.

Histogram + Summary:

case strings.HasSuffix(metricName, metricSuffixCreated):
mg.created = v

Counter:

if strings.HasSuffix(metricName, metricSuffixCreated) {
mg.created = v

When no _created metric is available, conversion MUST follow Cumulative streams: handling unknown start time by default.

this logic is complex, and is implemented in metrics_adjuster.go.

Conversion MAY offer configuration, disabled by default, which allows using the process_start_time_seconds metric to provide the start time. Using process_start_time_seconds is only correct when all counters on the target start after the process and are not reset while the process is running.

// UseStartTimeMetric 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.
UseStartTimeMetric bool `mapstructure:"use_start_time_metric"`

This is compliant with the specification

@dashpole
Copy link
Contributor Author

dashpole commented Aug 17, 2023

Exemplars

OpenMetrics Exemplars can be attached to Prometheus Histogram bucket metric points and counter metric points. Exemplars on histogram buckets SHOULD be converted to exemplars on OpenTelemetry histograms. Exemplars on counter metric points SHOULD be converted to exemplars on OpenTelemetry sums.

We implement the AppendExemplar function to get exemplars provided on series.

func (t *transaction) AppendExemplar(_ storage.SeriesRef, l labels.Labels, e exemplar.Exemplar) (storage.SeriesRef, error) {

Exemplars are set on histograms:

Exemplars are set on counters:

If present, the timestamp MUST be added to the OpenTelemetry exemplar.

The Trace ID and Span ID SHOULD be retrieved from the trace_id and span_id label keys, respectively. All labels not used for the trace and span ids MUST be added to the OpenTelemetry exemplar as attributes.

traceIDKey = "trace_id"
spanIDKey = "span_id"

for _, lb := range pe.Labels {
switch strings.ToLower(lb.Name) {
case traceIDKey:
var tid [16]byte
err := decodeAndCopyToLowerBytes(tid[:], []byte(lb.Value))
if err == nil {
e.SetTraceID(tid)
} else {
e.FilteredAttributes().PutStr(lb.Name, lb.Value)
}
case spanIDKey:
var sid [8]byte
err := decodeAndCopyToLowerBytes(sid[:], []byte(lb.Value))
if err == nil {
e.SetSpanID(sid)
} else {
e.FilteredAttributes().PutStr(lb.Name, lb.Value)
}
default:
e.FilteredAttributes().PutStr(lb.Name, lb.Value)
}

This is compliant with the specification

@dashpole
Copy link
Contributor Author

dashpole commented Aug 17, 2023

Instrumentation Scope

Each otel_scope_info metric point present in a batch of metrics SHOULD be dropped from the incoming scrape, and converted to an instrumentation scope. The otel_scope_name and otel_scope_version labels, if present, MUST be converted to the Name and Version of the Instrumentation Scope. Additional labels MUST be added as scope attributes, with keys and values unaltered. Other metrics in the batch which have otel_scope_name and otel_scope_version labels that match an instrumentation scope MUST be placed within the matching instrumentation scope, and MUST remove those labels.

Metrics which are not found to be associated with an instrumentation scope MUST all be placed within an empty instrumentation scope, and MUST not have any labels removed.

Related: open-telemetry/opentelemetry-specification#3660

This is not implemented

@dashpole
Copy link
Contributor Author

Resource Attributes

When scraping a Prometheus endpoint, resource attributes MUST be added to the scraped metrics to distinguish them from metrics from other Prometheus endpoints. In particular, service.name and service.instance.id, are needed to ensure Prometheus exporters can disambiguate metrics using job and instance labels as described below.

The following attributes MUST be associated with scraped metrics as resource attributes, and MUST NOT be added as metric attributes:

OTLP Resource Attribute Description
service.name The configured name of the service that the target belongs to
service.instance.id A unique identifier of the target. By default, it should be the <host>:<port> of the scraped URL

The following attributes SHOULD be associated with scraped metrics as resource
attributes, and MUST NOT be added as metric attributes:

OTLP Resource Attribute Description
server.address The <host> portion of the target's URL that was scraped
server.port The <port> portion of the target's URL that was scraped
url.scheme http or https

func CreateResource(job, instance string, serviceDiscoveryLabels labels.Labels) pcommon.Resource {
host, port, err := net.SplitHostPort(instance)
if err != nil {
host = instance
}
resource := pcommon.NewResource()
attrs := resource.Attributes()
attrs.PutStr(conventions.AttributeServiceName, job)
if isDiscernibleHost(host) {
attrs.PutStr(conventions.AttributeNetHostName, host)
}
attrs.PutStr(conventions.AttributeServiceInstanceID, instance)
attrs.PutStr(conventions.AttributeNetHostPort, port)
attrs.PutStr(conventions.AttributeHTTPScheme, serviceDiscoveryLabels.Get(model.SchemeLabel))

In addition to the attributes above, the target_info metric is used to supply additional resource attributes. If present, target_info MUST be dropped from the batch of metrics, and all labels from the target_info metric MUST be converted to resource attributes attached to all other metrics which are part of the scrape. By default, label keys and values MUST NOT be altered (such as replacing _ with . characters in keys).

func (t *transaction) AddTargetInfo(labels labels.Labels) error {
attrs := t.nodeResource.Attributes()
for _, lbl := range labels {
if lbl.Name == model.JobLabel || lbl.Name == model.InstanceLabel || lbl.Name == model.MetricNameLabel {
continue
}
attrs.PutStr(lbl.Name, lbl.Value)
}
return nil

This is compliant with the specification

@dashpole dashpole added the receiver/prometheus Prometheus receiver label Aug 17, 2023
@dashpole dashpole changed the title Prometheus receiver spec compliance Prometheus receiver spec compliance tracker Aug 17, 2023
@dashpole
Copy link
Contributor Author

I realized we actually do drop summaries and histograms without a count. I've updated the summary and histogram comments above with the reference. open-telemetry/opentelemetry-specification#3666 is no longer needed

@dashpole
Copy link
Contributor Author

The prometheus receiver is now fully compliant with the prometheus specification

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs triage New item requiring triage receiver/prometheus Prometheus receiver
Projects
None yet
Development

No branches or pull requests

1 participant