exporter/collector/googlemanagedprometheus/extra_metrics.go (307 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package googlemanagedprometheus
import (
"time"
"github.com/prometheus/common/model"
"go.opentelemetry.io/collector/featuregate"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/pmetric"
semconv "go.opentelemetry.io/collector/semconv/v1.18.0"
)
var intToDoubleFeatureGate = featuregate.GlobalRegistry().MustRegister(
"exporter.googlemanagedprometheus.intToDouble",
featuregate.StageAlpha,
featuregate.WithRegisterFromVersion("v0.100.0"),
featuregate.WithRegisterDescription("Convert all int metrics to double metrics to avoid incompatible value types."),
featuregate.WithRegisterReferenceURL("https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/issues/798"))
const prometheusMetricMetadataTypeKey = "prometheus.type"
func (c Config) ExtraMetrics(m pmetric.Metrics) {
addUntypedMetrics(m)
c.addTargetInfoMetric(m)
c.addScopeInfoMetric(m)
convertIntToDouble(m)
}
// convertIntToDouble converts all counter and gauge int values to double.
func convertIntToDouble(m pmetric.Metrics) {
if !intToDoubleFeatureGate.IsEnabled() {
return
}
rms := m.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
rm := rms.At(i)
for j := 0; j < rm.ScopeMetrics().Len(); j++ {
sm := rm.ScopeMetrics().At(j)
for k := 0; k < sm.Metrics().Len(); k++ {
metric := sm.Metrics().At(k)
var points pmetric.NumberDataPointSlice
switch metric.Type() {
case pmetric.MetricTypeSum:
points = metric.Sum().DataPoints()
case pmetric.MetricTypeGauge:
points = metric.Gauge().DataPoints()
default:
continue
}
for x := 0; x < points.Len(); x++ {
point := points.At(x)
if point.ValueType() == pmetric.NumberDataPointValueTypeInt {
point.SetDoubleValue(float64(point.IntValue()))
}
}
}
}
}
}
// addUntypedMetrics looks for any Gauge data point with the special Ops Agent untyped metric
// attribute and duplicates that data point to a matching Sum.
func addUntypedMetrics(m pmetric.Metrics) {
rms := m.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
rm := rms.At(i)
sms := rm.ScopeMetrics()
for j := 0; j < sms.Len(); j++ {
sm := sms.At(j)
mes := sm.Metrics()
// only iterate up to the current length. new untyped sum metrics
// will be added to this slice inline.
mLen := mes.Len()
for k := 0; k < mLen; k++ {
metric := mes.At(k)
// only applies to gauges
if metric.Type() != pmetric.MetricTypeGauge {
continue
}
if !isUnknown(metric) {
continue
}
// attribute is set on the data point
gauge := metric.Gauge()
points := gauge.DataPoints()
for l := 0; l < points.Len(); l++ {
// Add the new Sum metric and copy values over from this Gauge
point := points.At(l)
newMetric := mes.AppendEmpty()
newMetric.SetName(metric.Name())
newMetric.SetDescription(metric.Description())
newMetric.SetUnit(metric.Unit())
newMetric.Metadata().PutStr(prometheusMetricMetadataTypeKey, string(model.MetricTypeUnknown))
newSum := newMetric.SetEmptySum()
newSum.SetIsMonotonic(true)
newSum.SetAggregationTemporality(pmetric.AggregationTemporalityCumulative)
newDataPoint := newSum.DataPoints().AppendEmpty()
point.Attributes().CopyTo(newDataPoint.Attributes())
if point.ValueType() == pmetric.NumberDataPointValueTypeInt {
newDataPoint.SetIntValue(point.IntValue())
} else if point.ValueType() == pmetric.NumberDataPointValueTypeDouble {
newDataPoint.SetDoubleValue(point.DoubleValue())
}
newDataPoint.SetFlags(point.Flags())
newDataPoint.SetTimestamp(point.Timestamp())
newDataPoint.SetStartTimestamp(point.StartTimestamp())
}
}
}
}
}
func isUnknown(metric pmetric.Metric) bool {
originalType, ok := metric.Metadata().Get(prometheusMetricMetadataTypeKey)
return ok && originalType.Str() == string(model.MetricTypeUnknown)
}
// resourceID identifies a resource. We only send one target info for each
// resourceID.
type resourceID struct {
serviceName, serviceInstanceID, serviceNamespace string
}
// addTargetInfoMetric inserts target_info for each resource.
// First, it extracts the target_info metric from each ResourceMetric associated with the input pmetric.Metrics
// and inserts it into a new ScopeMetric for that resource, as specified in
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.16.0/specification/compatibility/prometheus_and_openmetrics.md#resource-attributes-1
func (c Config) addTargetInfoMetric(m pmetric.Metrics) {
if !c.ExtraMetricsConfig.EnableTargetInfo {
return
}
ids := make(map[resourceID]struct{})
rms := m.ResourceMetrics()
// loop over input (original) resource metrics
for i := 0; i < rms.Len(); i++ {
rm := rms.At(i)
getResourceAttr := func(attr string) string {
if v, ok := rm.Resource().Attributes().Get(attr); ok {
return v.AsString()
}
return ""
}
// Keep track of the most recent time in this resource's metrics
// Use that time for the timestamp of the new metric
latestTime := time.Time{}
for j := 0; j < rm.ScopeMetrics().Len(); j++ {
for k := 0; k < rm.ScopeMetrics().At(j).Metrics().Len(); k++ {
metric := rm.ScopeMetrics().At(j).Metrics().At(k)
switch metric.Type() {
case pmetric.MetricTypeSum:
sum := metric.Sum()
points := sum.DataPoints()
for x := 0; x < points.Len(); x++ {
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeGauge:
gauge := metric.Gauge()
points := gauge.DataPoints()
for x := 0; x < points.Len(); x++ {
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeSummary:
summary := metric.Summary()
points := summary.DataPoints()
for x := 0; x < points.Len(); x++ {
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeHistogram:
hist := metric.Histogram()
points := hist.DataPoints()
for x := 0; x < points.Len(); x++ {
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeExponentialHistogram:
eh := metric.ExponentialHistogram()
points := eh.DataPoints()
for x := 0; x < points.Len(); x++ {
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
}
}
}
id := resourceID{
serviceName: getResourceAttr(semconv.AttributeServiceName),
serviceNamespace: getResourceAttr(semconv.AttributeServiceNamespace),
serviceInstanceID: getResourceAttr(semconv.AttributeServiceInstanceID),
}
if _, ok := ids[id]; ok {
// We've already added a resource with the same ID before, so skip this one.
continue
}
ids[id] = struct{}{}
// create the target_info metric as a Gauge with value 1
targetInfoMetric := rm.ScopeMetrics().AppendEmpty().Metrics().AppendEmpty()
targetInfoMetric.SetName("target_info")
dataPoint := targetInfoMetric.SetEmptyGauge().DataPoints().AppendEmpty()
dataPoint.SetIntValue(1)
dataPoint.SetTimestamp(pcommon.NewTimestampFromTime(latestTime))
// copy Resource attributes to the metric except for service.name, service.namespace, and service.instance.id
// because those three attributes (if present) will be copied to resource attributes.
// See https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#resource-attributes-1
// Also drop reserved GMP labels (location, cluster, namespace, job, service_namespace, instance).
// Other "fallback" attributes which could become `job` or `instance` in the absence of those three
// (such as k8s.pod.name or faas.name) will be duplicated as a resource attribute and metric label.
rm.Resource().Attributes().Range(func(k string, v pcommon.Value) bool {
if k != semconv.AttributeServiceName &&
k != semconv.AttributeServiceNamespace &&
k != semconv.AttributeServiceInstanceID &&
k != locationLabel &&
k != clusterLabel &&
k != namespaceLabel &&
k != jobLabel &&
k != serviceNamespaceLabel &&
k != instanceLabel {
dataPoint.Attributes().PutStr(k, v.AsString())
}
return true
})
}
}
// scopeID identifies a scope. We only send one scope info for each scopeID within a unique resource.
type scopeID struct {
resource resourceID
name, version string
}
// addScopeInfoMetric adds the otel_scope_info metric to a Metrics slice as specified in
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.16.0/specification/compatibility/prometheus_and_openmetrics.md#instrumentation-scope-1
// It also updates all other metrics with the corresponding scope_name and scope_version attributes, if they are present.
func (c Config) addScopeInfoMetric(m pmetric.Metrics) {
if !c.ExtraMetricsConfig.EnableScopeInfo {
return
}
ids := make(map[scopeID]struct{})
rms := m.ResourceMetrics()
for i := 0; i < rms.Len(); i++ {
rm := rms.At(i)
getResourceAttr := func(attr string) string {
if v, ok := rm.Resource().Attributes().Get(attr); ok {
return v.AsString()
}
return ""
}
sms := rm.ScopeMetrics()
for j := 0; j < sms.Len(); j++ {
sm := sms.At(j)
// If not present, skip this scope
if len(sm.Scope().Name()) == 0 && len(sm.Scope().Version()) == 0 {
continue
}
// Keep track of the most recent time in this scope's metrics
// Use that time for the timestamp of the new metric
latestTime := time.Time{}
for k := 0; k < sm.Metrics().Len(); k++ {
metric := sm.Metrics().At(k)
switch metric.Type() {
case pmetric.MetricTypeSum:
sum := metric.Sum()
points := sum.DataPoints()
for x := 0; x < points.Len(); x++ {
point := points.At(x)
point.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
point.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeGauge:
gauge := metric.Gauge()
points := gauge.DataPoints()
for x := 0; x < points.Len(); x++ {
point := points.At(x)
point.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
point.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeSummary:
summary := metric.Summary()
points := summary.DataPoints()
for x := 0; x < points.Len(); x++ {
point := points.At(x)
point.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
point.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeHistogram:
hist := metric.Histogram()
points := hist.DataPoints()
for x := 0; x < points.Len(); x++ {
point := points.At(x)
point.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
point.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
case pmetric.MetricTypeExponentialHistogram:
eh := metric.ExponentialHistogram()
points := eh.DataPoints()
for x := 0; x < points.Len(); x++ {
point := points.At(x)
point.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
point.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
if latestTime.Before(points.At(x).Timestamp().AsTime()) {
latestTime = points.At(x).Timestamp().AsTime()
}
}
}
}
id := scopeID{
resource: resourceID{
serviceName: getResourceAttr(semconv.AttributeServiceName),
serviceNamespace: getResourceAttr(semconv.AttributeServiceNamespace),
serviceInstanceID: getResourceAttr(semconv.AttributeServiceInstanceID),
},
name: sm.Scope().Name(),
version: sm.Scope().Version(),
}
if _, ok := ids[id]; ok {
// We've already added a scope with the same ID before, so skip this one.
continue
}
ids[id] = struct{}{}
// Add otel_scope_info metric
scopeInfoMetric := sm.Metrics().AppendEmpty()
scopeInfoMetric.SetName("otel_scope_info")
dataPoint := scopeInfoMetric.SetEmptyGauge().DataPoints().AppendEmpty()
dataPoint.SetIntValue(1)
sm.Scope().Attributes().Range(func(k string, v pcommon.Value) bool {
dataPoint.Attributes().PutStr(k, v.AsString())
return true
})
dataPoint.Attributes().PutStr("otel_scope_name", sm.Scope().Name())
dataPoint.Attributes().PutStr("otel_scope_version", sm.Scope().Version())
dataPoint.SetTimestamp(pcommon.NewTimestampFromTime(latestTime))
}
}
}