Skip to content

Commit a1539d4

Browse files
dashpolesirianniMrAlias
authored
OpenCensus metric exporter bridge (#1444)
* add OpenCensus metric exporter bridge * Update bridge/opencensus/README.md Co-authored-by: Eric Sirianni <[email protected]> Co-authored-by: Eric Sirianni <[email protected]> Co-authored-by: Tyler Yahn <[email protected]>
1 parent 77aa218 commit a1539d4

File tree

10 files changed

+1319
-9
lines changed

10 files changed

+1319
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
133133
- Added codeql worfklow to GitHub Actions (#1428)
134134
- Added Gosec workflow to GitHub Actions (#1429)
135135
- Add new HTTP driver for OTLP exporter in `exporters/otlp/otlphttp`. Currently it only supports the binary protobuf payloads. (#1420)
136+
- Add an OpenCensus exporter bridge. (#1444)
136137

137138
### Changed
138139

bridge/opencensus/README.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
The OpenCensus Bridge helps facilitate the migration of an application from OpenCensus to OpenTelemetry.
44

5-
## The Problem: Mixing OpenCensus and OpenTelemetry libraries
5+
## Tracing
6+
7+
### The Problem: Mixing OpenCensus and OpenTelemetry libraries
68

79
In a perfect world, one would simply migrate their entire go application --including custom instrumentation, libraries, and exporters-- from OpenCensus to OpenTelemetry all at once. In the real world, dependency constraints, third-party ownership of libraries, or other reasons may require mixing OpenCensus and OpenTelemetry libraries in a single application.
810

@@ -44,10 +46,12 @@ The bridge implements the OpenCensus trace API using OpenTelemetry. This would
4446

4547
### User Journey
4648

49+
Starting from an application using entirely OpenCensus APIs:
50+
4751
1. Instantiate OpenTelemetry SDK and Exporters
4852
2. Override OpenCensus' DefaultTracer with the bridge
49-
3. Migrate libraries from OpenCensus to OpenTelemetry
50-
4. Remove OpenCensus Exporters
53+
3. Migrate libraries individually from OpenCensus to OpenTelemetry
54+
4. Remove OpenCensus exporters and configuration
5155

5256
To override OpenCensus' DefaultTracer with the bridge:
5357
```golang
@@ -63,10 +67,56 @@ octrace.DefaultTracer = opencensus.NewTracer(tracer)
6367

6468
Be sure to set the `Tracer` name to your instrumentation package name instead of `"bridge"`.
6569

66-
### Incompatibilities
70+
#### Incompatibilities
6771

6872
OpenCensus and OpenTelemetry APIs are not entirely compatible. If the bridge finds any incompatibilities, it will log them. Incompatibilities include:
6973

7074
* Custom OpenCensus Samplers specified during StartSpan are ignored.
7175
* Links cannot be added to OpenCensus spans.
7276
* OpenTelemetry Debug or Deferred trace flags are dropped after an OpenCensus span is created.
77+
78+
## Metrics
79+
80+
### The problem: mixing libraries without mixing pipelines
81+
82+
The problem for monitoring is simpler than the problem for tracing, since there
83+
are no context propagation issues to deal with. However, it still is difficult
84+
for users to migrate an entire applications' monitoring at once. It
85+
should be possible to send metrics generated by OpenCensus libraries to an
86+
OpenTelemetry pipeline so that migrating a metric does not require maintaining
87+
separate export pipelines for OpenCensus and OpenTelemetry.
88+
89+
### The Exporter "wrapper" solution
90+
91+
The solution we use here is to allow wrapping an OpenTelemetry exporter such
92+
that it implements the OpenCensus exporter interfaces. This allows a single
93+
exporter to be used for metrics from *both* OpenCensus and OpenTelemetry.
94+
95+
### User Journey
96+
97+
Starting from an application using entirely OpenCensus APIs:
98+
99+
1. Instantiate OpenTelemetry SDK and Exporters.
100+
2. Replace OpenCensus exporters with a wrapped OpenTelemetry exporter from step 1.
101+
3. Migrate libraries individually from OpenCensus to OpenTelemetry
102+
4. Remove OpenCensus Exporters and configuration.
103+
104+
For example, to swap out the OpenCensus logging exporter for the OpenTelemetry stdout exporter:
105+
```golang
106+
import (
107+
"go.opencensus.io/metric/metricexport"
108+
"go.opentelemetry.io/otel/bridge/opencensus"
109+
"go.opentelemetry.io/otel/exporters/stdout"
110+
"go.opentelemetry.io/otel"
111+
)
112+
// With OpenCensus, you could have previously configured the logging exporter like this:
113+
// import logexporter "go.opencensus.io/examples/exporter"
114+
// exporter, _ := logexporter.NewLogExporter(logexporter.Options{})
115+
// Instead, we can create an equivalent using the OpenTelemetry stdout exporter:
116+
openTelemetryExporter, _ := stdout.NewExporter(stdout.WithPrettyPrint())
117+
exporter := opencensus.NewMetricExporter(openTelemetryExporter)
118+
119+
// Use the wrapped OpenTelemetry exporter like you normally would with OpenCensus
120+
intervalReader, _ := metricexport.NewIntervalReader(&metricexport.Reader{}, exporter)
121+
intervalReader.Start()
122+
```

bridge/opencensus/aggregation.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package opencensus
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"time"
21+
22+
"go.opencensus.io/metric/metricdata"
23+
24+
"go.opentelemetry.io/otel/metric/number"
25+
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
26+
)
27+
28+
var (
29+
errIncompatibleType = errors.New("incompatible type for aggregation")
30+
errEmpty = errors.New("points may not be empty")
31+
errBadPoint = errors.New("point cannot be converted")
32+
)
33+
34+
// aggregationWithEndTime is an aggregation that can also provide the timestamp
35+
// of the last recorded point.
36+
type aggregationWithEndTime interface {
37+
aggregation.Aggregation
38+
end() time.Time
39+
}
40+
41+
// newAggregationFromPoints creates an OpenTelemetry aggregation from
42+
// OpenCensus points. Points may not be empty and must be either
43+
// all (int|float)64 or all *metricdata.Distribution.
44+
func newAggregationFromPoints(points []metricdata.Point) (aggregationWithEndTime, error) {
45+
if len(points) == 0 {
46+
return nil, errEmpty
47+
}
48+
switch t := points[0].Value.(type) {
49+
case int64:
50+
return newExactAggregator(points)
51+
case float64:
52+
return newExactAggregator(points)
53+
case *metricdata.Distribution:
54+
return newDistributionAggregator(points)
55+
default:
56+
// TODO add *metricdata.Summary support
57+
return nil, fmt.Errorf("%w: %v", errIncompatibleType, t)
58+
}
59+
}
60+
61+
var _ aggregation.Aggregation = &ocExactAggregator{}
62+
var _ aggregation.LastValue = &ocExactAggregator{}
63+
var _ aggregation.Points = &ocExactAggregator{}
64+
65+
// newExactAggregator creates an OpenTelemetry aggreation from OpenCensus points.
66+
// Points may not be empty, and must only contain integers or floats.
67+
func newExactAggregator(pts []metricdata.Point) (aggregationWithEndTime, error) {
68+
points := make([]aggregation.Point, len(pts))
69+
for i, pt := range pts {
70+
switch t := pt.Value.(type) {
71+
case int64:
72+
points[i] = aggregation.Point{
73+
Number: number.NewInt64Number(pt.Value.(int64)),
74+
Time: pt.Time,
75+
}
76+
case float64:
77+
points[i] = aggregation.Point{
78+
Number: number.NewFloat64Number(pt.Value.(float64)),
79+
Time: pt.Time,
80+
}
81+
default:
82+
return nil, fmt.Errorf("%w: %v", errIncompatibleType, t)
83+
}
84+
}
85+
return &ocExactAggregator{
86+
points: points,
87+
}, nil
88+
}
89+
90+
type ocExactAggregator struct {
91+
points []aggregation.Point
92+
}
93+
94+
// Kind returns the kind of aggregation this is.
95+
func (o *ocExactAggregator) Kind() aggregation.Kind {
96+
return aggregation.ExactKind
97+
}
98+
99+
// Points returns access to the raw data set.
100+
func (o *ocExactAggregator) Points() ([]aggregation.Point, error) {
101+
return o.points, nil
102+
}
103+
104+
// LastValue returns the last point.
105+
func (o *ocExactAggregator) LastValue() (number.Number, time.Time, error) {
106+
last := o.points[len(o.points)-1]
107+
return last.Number, last.Time, nil
108+
}
109+
110+
// end returns the timestamp of the last point
111+
func (o *ocExactAggregator) end() time.Time {
112+
_, t, _ := o.LastValue()
113+
return t
114+
}
115+
116+
var _ aggregation.Aggregation = &ocDistAggregator{}
117+
var _ aggregation.Histogram = &ocDistAggregator{}
118+
119+
// newDistributionAggregator creates an OpenTelemetry aggreation from
120+
// OpenCensus points. Points may not be empty, and must only contain
121+
// Distributions. The most recent disribution will be used in the aggregation.
122+
func newDistributionAggregator(pts []metricdata.Point) (aggregationWithEndTime, error) {
123+
// only use the most recent datapoint for now.
124+
pt := pts[len(pts)-1]
125+
val, ok := pt.Value.(*metricdata.Distribution)
126+
if !ok {
127+
return nil, fmt.Errorf("%w: %v", errBadPoint, pt.Value)
128+
}
129+
bucketCounts := make([]uint64, len(val.Buckets))
130+
for i, bucket := range val.Buckets {
131+
if bucket.Count < 0 {
132+
return nil, fmt.Errorf("%w: bucket count may not be negative", errBadPoint)
133+
}
134+
bucketCounts[i] = uint64(bucket.Count)
135+
}
136+
if val.Count < 0 {
137+
return nil, fmt.Errorf("%w: count may not be negative", errBadPoint)
138+
}
139+
return &ocDistAggregator{
140+
sum: number.NewFloat64Number(val.Sum),
141+
count: uint64(val.Count),
142+
buckets: aggregation.Buckets{
143+
Boundaries: val.BucketOptions.Bounds,
144+
Counts: bucketCounts,
145+
},
146+
endTime: pts[len(pts)-1].Time,
147+
}, nil
148+
}
149+
150+
type ocDistAggregator struct {
151+
sum number.Number
152+
count uint64
153+
buckets aggregation.Buckets
154+
endTime time.Time
155+
}
156+
157+
// Kind returns the kind of aggregation this is.
158+
func (o *ocDistAggregator) Kind() aggregation.Kind {
159+
return aggregation.HistogramKind
160+
}
161+
162+
// Sum returns the sum of values.
163+
func (o *ocDistAggregator) Sum() (number.Number, error) {
164+
return o.sum, nil
165+
}
166+
167+
// Count returns the number of values.
168+
func (o *ocDistAggregator) Count() (uint64, error) {
169+
return o.count, nil
170+
}
171+
172+
// Histogram returns the count of events in pre-determined buckets.
173+
func (o *ocDistAggregator) Histogram() (aggregation.Buckets, error) {
174+
return o.buckets, nil
175+
}
176+
177+
// end returns the time the histogram was measured.
178+
func (o *ocDistAggregator) end() time.Time {
179+
return o.endTime
180+
}

0 commit comments

Comments
 (0)