mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2024-12-24 20:14:40 +02:00
4e8d667f6e
* Remove resource from export.Record * fix bridge/opencensus * partial fixes in exporters/otlp/otlpmetric * Use go-cmp to simplify exporter_test * OTLP http/grpc * prometheus * fix stdout (pending PR 2015) * oc bridge cleanups * Lint and changelog * pr num * cleanup * revert * fix multiple * always merge the environment * lint * precommit * qualify package names in changelog
558 lines
16 KiB
Go
558 lines
16 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// 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
|
|
//
|
|
// http://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 transform provides translations for opentelemetry-go concepts and
|
|
// structures to otlp structures.
|
|
package metrictransform
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
commonpb "go.opentelemetry.io/proto/otlp/common/v1"
|
|
metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
|
|
|
|
"go.opentelemetry.io/otel/metric/number"
|
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
|
|
"go.opentelemetry.io/otel/sdk/instrumentation"
|
|
"go.opentelemetry.io/otel/sdk/resource"
|
|
)
|
|
|
|
var (
|
|
// ErrUnimplementedAgg is returned when a transformation of an unimplemented
|
|
// aggregator is attempted.
|
|
ErrUnimplementedAgg = errors.New("unimplemented aggregator")
|
|
|
|
// ErrIncompatibleAgg is returned when
|
|
// aggregation.Kind implies an interface conversion that has
|
|
// failed
|
|
ErrIncompatibleAgg = errors.New("incompatible aggregation type")
|
|
|
|
// ErrUnknownValueType is returned when a transformation of an unknown value
|
|
// is attempted.
|
|
ErrUnknownValueType = errors.New("invalid value type")
|
|
|
|
// ErrContextCanceled is returned when a context cancellation halts a
|
|
// transformation.
|
|
ErrContextCanceled = errors.New("context canceled")
|
|
|
|
// ErrTransforming is returned when an unexected error is encoutered transforming.
|
|
ErrTransforming = errors.New("transforming failed")
|
|
)
|
|
|
|
// result is the product of transforming Records into OTLP Metrics.
|
|
type result struct {
|
|
InstrumentationLibrary instrumentation.Library
|
|
Metric *metricpb.Metric
|
|
Err error
|
|
}
|
|
|
|
// toNanos returns the number of nanoseconds since the UNIX epoch.
|
|
func toNanos(t time.Time) uint64 {
|
|
if t.IsZero() {
|
|
return 0
|
|
}
|
|
return uint64(t.UnixNano())
|
|
}
|
|
|
|
// CheckpointSet transforms all records contained in a checkpoint into
|
|
// batched OTLP ResourceMetrics.
|
|
func CheckpointSet(ctx context.Context, exportSelector export.ExportKindSelector, res *resource.Resource, cps export.CheckpointSet, numWorkers uint) ([]*metricpb.ResourceMetrics, error) {
|
|
records, errc := source(ctx, exportSelector, cps)
|
|
|
|
// Start a fixed number of goroutines to transform records.
|
|
transformed := make(chan result)
|
|
var wg sync.WaitGroup
|
|
wg.Add(int(numWorkers))
|
|
for i := uint(0); i < numWorkers; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
transformer(ctx, exportSelector, records, transformed)
|
|
}()
|
|
}
|
|
go func() {
|
|
wg.Wait()
|
|
close(transformed)
|
|
}()
|
|
|
|
// Synchronously collect the transformed records and transmit.
|
|
rms, err := sink(ctx, res, transformed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// source is complete, check for any errors.
|
|
if err := <-errc; err != nil {
|
|
return nil, err
|
|
}
|
|
return rms, nil
|
|
}
|
|
|
|
// source starts a goroutine that sends each one of the Records yielded by
|
|
// the CheckpointSet on the returned chan. Any error encoutered will be sent
|
|
// on the returned error chan after seeding is complete.
|
|
func source(ctx context.Context, exportSelector export.ExportKindSelector, cps export.CheckpointSet) (<-chan export.Record, <-chan error) {
|
|
errc := make(chan error, 1)
|
|
out := make(chan export.Record)
|
|
// Seed records into process.
|
|
go func() {
|
|
defer close(out)
|
|
// No select is needed since errc is buffered.
|
|
errc <- cps.ForEach(exportSelector, func(r export.Record) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ErrContextCanceled
|
|
case out <- r:
|
|
}
|
|
return nil
|
|
})
|
|
}()
|
|
return out, errc
|
|
}
|
|
|
|
// transformer transforms records read from the passed in chan into
|
|
// OTLP Metrics which are sent on the out chan.
|
|
func transformer(ctx context.Context, exportSelector export.ExportKindSelector, in <-chan export.Record, out chan<- result) {
|
|
for r := range in {
|
|
m, err := Record(exportSelector, r)
|
|
// Propagate errors, but do not send empty results.
|
|
if err == nil && m == nil {
|
|
continue
|
|
}
|
|
res := result{
|
|
InstrumentationLibrary: instrumentation.Library{
|
|
Name: r.Descriptor().InstrumentationName(),
|
|
Version: r.Descriptor().InstrumentationVersion(),
|
|
},
|
|
Metric: m,
|
|
Err: err,
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case out <- res:
|
|
}
|
|
}
|
|
}
|
|
|
|
// sink collects transformed Records and batches them.
|
|
//
|
|
// Any errors encoutered transforming input will be reported with an
|
|
// ErrTransforming as well as the completed ResourceMetrics. It is up to the
|
|
// caller to handle any incorrect data in these ResourceMetrics.
|
|
func sink(ctx context.Context, res *resource.Resource, in <-chan result) ([]*metricpb.ResourceMetrics, error) {
|
|
var errStrings []string
|
|
|
|
// Group by instrumentation library name and then the MetricDescriptor.
|
|
grouped := map[instrumentation.Library]map[string]*metricpb.Metric{}
|
|
for res := range in {
|
|
if res.Err != nil {
|
|
errStrings = append(errStrings, res.Err.Error())
|
|
continue
|
|
}
|
|
|
|
mb, ok := grouped[res.InstrumentationLibrary]
|
|
if !ok {
|
|
mb = make(map[string]*metricpb.Metric)
|
|
grouped[res.InstrumentationLibrary] = mb
|
|
}
|
|
|
|
mID := res.Metric.GetName()
|
|
m, ok := mb[mID]
|
|
if !ok {
|
|
mb[mID] = res.Metric
|
|
continue
|
|
}
|
|
switch res.Metric.Data.(type) {
|
|
case *metricpb.Metric_Gauge:
|
|
m.GetGauge().DataPoints = append(m.GetGauge().DataPoints, res.Metric.GetGauge().DataPoints...)
|
|
case *metricpb.Metric_Sum:
|
|
m.GetSum().DataPoints = append(m.GetSum().DataPoints, res.Metric.GetSum().DataPoints...)
|
|
case *metricpb.Metric_Histogram:
|
|
m.GetHistogram().DataPoints = append(m.GetHistogram().DataPoints, res.Metric.GetHistogram().DataPoints...)
|
|
case *metricpb.Metric_Summary:
|
|
m.GetSummary().DataPoints = append(m.GetSummary().DataPoints, res.Metric.GetSummary().DataPoints...)
|
|
default:
|
|
err := fmt.Sprintf("unsupported metric type: %T", res.Metric.Data)
|
|
errStrings = append(errStrings, err)
|
|
}
|
|
}
|
|
|
|
if len(grouped) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
rm := &metricpb.ResourceMetrics{
|
|
Resource: Resource(res),
|
|
}
|
|
if res != nil {
|
|
rm.SchemaUrl = res.SchemaURL()
|
|
}
|
|
|
|
rms := []*metricpb.ResourceMetrics{rm}
|
|
for il, mb := range grouped {
|
|
ilm := &metricpb.InstrumentationLibraryMetrics{
|
|
Metrics: make([]*metricpb.Metric, 0, len(mb)),
|
|
InstrumentationLibrary: &commonpb.InstrumentationLibrary{
|
|
Name: il.Name,
|
|
Version: il.Version,
|
|
},
|
|
}
|
|
for _, m := range mb {
|
|
ilm.Metrics = append(ilm.Metrics, m)
|
|
}
|
|
rm.InstrumentationLibraryMetrics = append(rm.InstrumentationLibraryMetrics, ilm)
|
|
}
|
|
|
|
// Report any transform errors.
|
|
if len(errStrings) > 0 {
|
|
return rms, fmt.Errorf("%w:\n -%s", ErrTransforming, strings.Join(errStrings, "\n -"))
|
|
}
|
|
return rms, nil
|
|
}
|
|
|
|
// Record transforms a Record into an OTLP Metric. An ErrIncompatibleAgg
|
|
// error is returned if the Record Aggregator is not supported.
|
|
func Record(exportSelector export.ExportKindSelector, r export.Record) (*metricpb.Metric, error) {
|
|
agg := r.Aggregation()
|
|
switch agg.Kind() {
|
|
case aggregation.MinMaxSumCountKind:
|
|
mmsc, ok := agg.(aggregation.MinMaxSumCount)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %T", ErrIncompatibleAgg, agg)
|
|
}
|
|
return minMaxSumCount(r, mmsc)
|
|
|
|
case aggregation.HistogramKind:
|
|
h, ok := agg.(aggregation.Histogram)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %T", ErrIncompatibleAgg, agg)
|
|
}
|
|
return histogramPoint(r, exportSelector.ExportKindFor(r.Descriptor(), aggregation.HistogramKind), h)
|
|
|
|
case aggregation.SumKind:
|
|
s, ok := agg.(aggregation.Sum)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %T", ErrIncompatibleAgg, agg)
|
|
}
|
|
sum, err := s.Sum()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sumPoint(r, sum, r.StartTime(), r.EndTime(), exportSelector.ExportKindFor(r.Descriptor(), aggregation.SumKind), r.Descriptor().InstrumentKind().Monotonic())
|
|
|
|
case aggregation.LastValueKind:
|
|
lv, ok := agg.(aggregation.LastValue)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %T", ErrIncompatibleAgg, agg)
|
|
}
|
|
value, tm, err := lv.LastValue()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gaugePoint(r, value, time.Time{}, tm)
|
|
|
|
case aggregation.ExactKind:
|
|
e, ok := agg.(aggregation.Points)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %T", ErrIncompatibleAgg, agg)
|
|
}
|
|
pts, err := e.Points()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return gaugeArray(r, pts)
|
|
|
|
default:
|
|
return nil, fmt.Errorf("%w: %T", ErrUnimplementedAgg, agg)
|
|
}
|
|
}
|
|
|
|
func gaugeArray(record export.Record, points []aggregation.Point) (*metricpb.Metric, error) {
|
|
desc := record.Descriptor()
|
|
labels := record.Labels()
|
|
m := &metricpb.Metric{
|
|
Name: desc.Name(),
|
|
Description: desc.Description(),
|
|
Unit: string(desc.Unit()),
|
|
}
|
|
|
|
pbAttrs := Iterator(labels.Iter())
|
|
|
|
ndp := make([]*metricpb.NumberDataPoint, 0, len(points))
|
|
switch nk := desc.NumberKind(); nk {
|
|
case number.Int64Kind:
|
|
for _, p := range points {
|
|
ndp = append(ndp, &metricpb.NumberDataPoint{
|
|
Attributes: pbAttrs,
|
|
StartTimeUnixNano: toNanos(record.StartTime()),
|
|
TimeUnixNano: toNanos(record.EndTime()),
|
|
Value: &metricpb.NumberDataPoint_AsInt{
|
|
AsInt: p.Number.CoerceToInt64(nk),
|
|
},
|
|
})
|
|
}
|
|
case number.Float64Kind:
|
|
for _, p := range points {
|
|
ndp = append(ndp, &metricpb.NumberDataPoint{
|
|
Attributes: pbAttrs,
|
|
StartTimeUnixNano: toNanos(record.StartTime()),
|
|
TimeUnixNano: toNanos(record.EndTime()),
|
|
Value: &metricpb.NumberDataPoint_AsDouble{
|
|
AsDouble: p.Number.CoerceToFloat64(nk),
|
|
},
|
|
})
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%w: %v", ErrUnknownValueType, nk)
|
|
}
|
|
|
|
m.Data = &metricpb.Metric_Gauge{
|
|
Gauge: &metricpb.Gauge{
|
|
DataPoints: ndp,
|
|
},
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func gaugePoint(record export.Record, num number.Number, start, end time.Time) (*metricpb.Metric, error) {
|
|
desc := record.Descriptor()
|
|
labels := record.Labels()
|
|
|
|
m := &metricpb.Metric{
|
|
Name: desc.Name(),
|
|
Description: desc.Description(),
|
|
Unit: string(desc.Unit()),
|
|
}
|
|
|
|
switch n := desc.NumberKind(); n {
|
|
case number.Int64Kind:
|
|
m.Data = &metricpb.Metric_Gauge{
|
|
Gauge: &metricpb.Gauge{
|
|
DataPoints: []*metricpb.NumberDataPoint{
|
|
{
|
|
Value: &metricpb.NumberDataPoint_AsInt{
|
|
AsInt: num.CoerceToInt64(n),
|
|
},
|
|
Attributes: Iterator(labels.Iter()),
|
|
StartTimeUnixNano: toNanos(start),
|
|
TimeUnixNano: toNanos(end),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case number.Float64Kind:
|
|
m.Data = &metricpb.Metric_Gauge{
|
|
Gauge: &metricpb.Gauge{
|
|
DataPoints: []*metricpb.NumberDataPoint{
|
|
{
|
|
Value: &metricpb.NumberDataPoint_AsDouble{
|
|
AsDouble: num.CoerceToFloat64(n),
|
|
},
|
|
Attributes: Iterator(labels.Iter()),
|
|
StartTimeUnixNano: toNanos(start),
|
|
TimeUnixNano: toNanos(end),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%w: %v", ErrUnknownValueType, n)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func exportKindToTemporality(ek export.ExportKind) metricpb.AggregationTemporality {
|
|
switch ek {
|
|
case export.DeltaExportKind:
|
|
return metricpb.AggregationTemporality_AGGREGATION_TEMPORALITY_DELTA
|
|
case export.CumulativeExportKind:
|
|
return metricpb.AggregationTemporality_AGGREGATION_TEMPORALITY_CUMULATIVE
|
|
}
|
|
return metricpb.AggregationTemporality_AGGREGATION_TEMPORALITY_UNSPECIFIED
|
|
}
|
|
|
|
func sumPoint(record export.Record, num number.Number, start, end time.Time, ek export.ExportKind, monotonic bool) (*metricpb.Metric, error) {
|
|
desc := record.Descriptor()
|
|
labels := record.Labels()
|
|
|
|
m := &metricpb.Metric{
|
|
Name: desc.Name(),
|
|
Description: desc.Description(),
|
|
Unit: string(desc.Unit()),
|
|
}
|
|
|
|
switch n := desc.NumberKind(); n {
|
|
case number.Int64Kind:
|
|
m.Data = &metricpb.Metric_Sum{
|
|
Sum: &metricpb.Sum{
|
|
IsMonotonic: monotonic,
|
|
AggregationTemporality: exportKindToTemporality(ek),
|
|
DataPoints: []*metricpb.NumberDataPoint{
|
|
{
|
|
Value: &metricpb.NumberDataPoint_AsInt{
|
|
AsInt: num.CoerceToInt64(n),
|
|
},
|
|
Attributes: Iterator(labels.Iter()),
|
|
StartTimeUnixNano: toNanos(start),
|
|
TimeUnixNano: toNanos(end),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
case number.Float64Kind:
|
|
m.Data = &metricpb.Metric_Sum{
|
|
Sum: &metricpb.Sum{
|
|
IsMonotonic: monotonic,
|
|
AggregationTemporality: exportKindToTemporality(ek),
|
|
DataPoints: []*metricpb.NumberDataPoint{
|
|
{
|
|
Value: &metricpb.NumberDataPoint_AsDouble{
|
|
AsDouble: num.CoerceToFloat64(n),
|
|
},
|
|
Attributes: Iterator(labels.Iter()),
|
|
StartTimeUnixNano: toNanos(start),
|
|
TimeUnixNano: toNanos(end),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("%w: %v", ErrUnknownValueType, n)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// minMaxSumCountValue returns the values of the MinMaxSumCount Aggregator
|
|
// as discrete values.
|
|
func minMaxSumCountValues(a aggregation.MinMaxSumCount) (min, max, sum number.Number, count uint64, err error) {
|
|
if min, err = a.Min(); err != nil {
|
|
return
|
|
}
|
|
if max, err = a.Max(); err != nil {
|
|
return
|
|
}
|
|
if sum, err = a.Sum(); err != nil {
|
|
return
|
|
}
|
|
if count, err = a.Count(); err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// minMaxSumCount transforms a MinMaxSumCount Aggregator into an OTLP Metric.
|
|
func minMaxSumCount(record export.Record, a aggregation.MinMaxSumCount) (*metricpb.Metric, error) {
|
|
desc := record.Descriptor()
|
|
labels := record.Labels()
|
|
min, max, sum, count, err := minMaxSumCountValues(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := &metricpb.Metric{
|
|
Name: desc.Name(),
|
|
Description: desc.Description(),
|
|
Unit: string(desc.Unit()),
|
|
Data: &metricpb.Metric_Summary{
|
|
Summary: &metricpb.Summary{
|
|
DataPoints: []*metricpb.SummaryDataPoint{
|
|
{
|
|
Sum: sum.CoerceToFloat64(desc.NumberKind()),
|
|
Attributes: Iterator(labels.Iter()),
|
|
StartTimeUnixNano: toNanos(record.StartTime()),
|
|
TimeUnixNano: toNanos(record.EndTime()),
|
|
Count: uint64(count),
|
|
QuantileValues: []*metricpb.SummaryDataPoint_ValueAtQuantile{
|
|
{
|
|
Quantile: 0.0,
|
|
Value: min.CoerceToFloat64(desc.NumberKind()),
|
|
},
|
|
{
|
|
Quantile: 1.0,
|
|
Value: max.CoerceToFloat64(desc.NumberKind()),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func histogramValues(a aggregation.Histogram) (boundaries []float64, counts []uint64, err error) {
|
|
var buckets aggregation.Buckets
|
|
if buckets, err = a.Histogram(); err != nil {
|
|
return
|
|
}
|
|
boundaries, counts = buckets.Boundaries, buckets.Counts
|
|
if len(counts) != len(boundaries)+1 {
|
|
err = ErrTransforming
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
// histogram transforms a Histogram Aggregator into an OTLP Metric.
|
|
func histogramPoint(record export.Record, ek export.ExportKind, a aggregation.Histogram) (*metricpb.Metric, error) {
|
|
desc := record.Descriptor()
|
|
labels := record.Labels()
|
|
boundaries, counts, err := histogramValues(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
count, err := a.Count()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sum, err := a.Sum()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := &metricpb.Metric{
|
|
Name: desc.Name(),
|
|
Description: desc.Description(),
|
|
Unit: string(desc.Unit()),
|
|
Data: &metricpb.Metric_Histogram{
|
|
Histogram: &metricpb.Histogram{
|
|
AggregationTemporality: exportKindToTemporality(ek),
|
|
DataPoints: []*metricpb.HistogramDataPoint{
|
|
{
|
|
Sum: sum.CoerceToFloat64(desc.NumberKind()),
|
|
Attributes: Iterator(labels.Iter()),
|
|
StartTimeUnixNano: toNanos(record.StartTime()),
|
|
TimeUnixNano: toNanos(record.EndTime()),
|
|
Count: uint64(count),
|
|
BucketCounts: counts,
|
|
ExplicitBounds: boundaries,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return m, nil
|
|
}
|