Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.

Commit 0ac3701

Browse files
songy23rghetia
authored andcommitted
Exemplar: Add new record APIs that take exemplar attachments and SpanContext key. (#1123)
* Exemplar: Add SpanContext Attachment key. * Exemplar: Add new record APIs that take exemplar attachments. * Use RetrieveData instead of fake exporter to fix race. * Change map[string]interface to metricdata.Attachments * Use nil instead of empty map. * Update to use options for recording attachments.
1 parent 18733e4 commit 0ac3701

3 files changed

Lines changed: 168 additions & 20 deletions

File tree

metric/metricdata/exemplar.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import (
1818
"time"
1919
)
2020

21+
// Exemplars keys.
22+
const (
23+
AttachmentKeySpanContext = "SpanContext"
24+
)
25+
2126
// Exemplar is an example data point associated with each bucket of a
2227
// distribution type aggregation.
2328
//

stats/record.go

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package stats
1818
import (
1919
"context"
2020

21+
"go.opencensus.io/metric/metricdata"
2122
"go.opencensus.io/stats/internal"
2223
"go.opencensus.io/tag"
2324
)
@@ -30,40 +31,87 @@ func init() {
3031
}
3132
}
3233

34+
type recordOptions struct {
35+
attachments metricdata.Attachments
36+
mutators []tag.Mutator
37+
measurements []Measurement
38+
}
39+
40+
// WithAttachments applies provided exemplar attachments.
41+
func WithAttachments(attachments metricdata.Attachments) Options {
42+
return func(ro *recordOptions) {
43+
ro.attachments = attachments
44+
}
45+
}
46+
47+
// WithTags applies provided tag mutators.
48+
func WithTags(mutators ...tag.Mutator) Options {
49+
return func(ro *recordOptions) {
50+
ro.mutators = mutators
51+
}
52+
}
53+
54+
// WithMeasurements applies provided measurements.
55+
func WithMeasurements(measurements ...Measurement) Options {
56+
return func(ro *recordOptions) {
57+
ro.measurements = measurements
58+
}
59+
}
60+
61+
// Options apply changes to recordOptions.
62+
type Options func(*recordOptions)
63+
64+
func createRecordOption(ros ...Options) *recordOptions {
65+
o := &recordOptions{}
66+
for _, ro := range ros {
67+
ro(o)
68+
}
69+
return o
70+
}
71+
3372
// Record records one or multiple measurements with the same context at once.
3473
// If there are any tags in the context, measurements will be tagged with them.
3574
func Record(ctx context.Context, ms ...Measurement) {
75+
RecordWithOptions(ctx, WithMeasurements(ms...))
76+
}
77+
78+
// RecordWithTags records one or multiple measurements at once.
79+
//
80+
// Measurements will be tagged with the tags in the context mutated by the mutators.
81+
// RecordWithTags is useful if you want to record with tag mutations but don't want
82+
// to propagate the mutations in the context.
83+
func RecordWithTags(ctx context.Context, mutators []tag.Mutator, ms ...Measurement) error {
84+
return RecordWithOptions(ctx, WithTags(mutators...), WithMeasurements(ms...))
85+
}
86+
87+
// RecordWithOptions records measurements from the given options (if any) against context
88+
// and tags and attachments in the options (if any).
89+
// If there are any tags in the context, measurements will be tagged with them.
90+
func RecordWithOptions(ctx context.Context, ros ...Options) error {
91+
o := createRecordOption(ros...)
92+
if len(o.measurements) == 0 {
93+
return nil
94+
}
3695
recorder := internal.DefaultRecorder
3796
if recorder == nil {
38-
return
39-
}
40-
if len(ms) == 0 {
41-
return
97+
return nil
4298
}
4399
record := false
44-
for _, m := range ms {
100+
for _, m := range o.measurements {
45101
if m.desc.subscribed() {
46102
record = true
47103
break
48104
}
49105
}
50106
if !record {
51-
return
107+
return nil
52108
}
53-
// TODO(songy23): fix attachments.
54-
recorder(tag.FromContext(ctx), ms, map[string]interface{}{})
55-
}
56-
57-
// RecordWithTags records one or multiple measurements at once.
58-
//
59-
// Measurements will be tagged with the tags in the context mutated by the mutators.
60-
// RecordWithTags is useful if you want to record with tag mutations but don't want
61-
// to propagate the mutations in the context.
62-
func RecordWithTags(ctx context.Context, mutators []tag.Mutator, ms ...Measurement) error {
63-
ctx, err := tag.New(ctx, mutators...)
64-
if err != nil {
65-
return err
109+
if len(o.mutators) > 0 {
110+
var err error
111+
if ctx, err = tag.New(ctx, o.mutators...); err != nil {
112+
return err
113+
}
66114
}
67-
Record(ctx, ms...)
115+
recorder(tag.FromContext(ctx), o.measurements, o.attachments)
68116
return nil
69117
}

stats/record_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright 2019, OpenCensus 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 stats_test
16+
17+
import (
18+
"context"
19+
"log"
20+
"reflect"
21+
"testing"
22+
"time"
23+
24+
"github.com/google/go-cmp/cmp"
25+
"github.com/google/go-cmp/cmp/cmpopts"
26+
27+
"go.opencensus.io/metric/metricdata"
28+
"go.opencensus.io/stats"
29+
"go.opencensus.io/stats/view"
30+
"go.opencensus.io/tag"
31+
"go.opencensus.io/trace"
32+
)
33+
34+
var (
35+
tid = trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 4, 8, 16, 32, 64, 128}
36+
sid = trace.SpanID{1, 2, 4, 8, 16, 32, 64, 128}
37+
spanCtx = trace.SpanContext{
38+
TraceID: tid,
39+
SpanID: sid,
40+
TraceOptions: 1,
41+
}
42+
)
43+
44+
func TestRecordWithAttachments(t *testing.T) {
45+
k1, _ := tag.NewKey("k1")
46+
k2, _ := tag.NewKey("k2")
47+
distribution := view.Distribution(5, 10)
48+
m := stats.Int64("TestRecordWithAttachments/m1", "", stats.UnitDimensionless)
49+
v := &view.View{
50+
Name: "test_view",
51+
TagKeys: []tag.Key{k1, k2},
52+
Measure: m,
53+
Aggregation: distribution,
54+
}
55+
view.SetReportingPeriod(100 * time.Millisecond)
56+
if err := view.Register(v); err != nil {
57+
log.Fatalf("Failed to register views: %v", err)
58+
}
59+
60+
attachments := map[string]interface{}{metricdata.AttachmentKeySpanContext: spanCtx}
61+
stats.RecordWithOptions(context.Background(), stats.WithAttachments(attachments), stats.WithMeasurements(m.M(12)))
62+
rows, err := view.RetrieveData("test_view")
63+
if err != nil {
64+
t.Errorf("Failed to retrieve data %v", err)
65+
}
66+
if len(rows) == 0 {
67+
t.Errorf("No data was recorded.")
68+
}
69+
data := rows[0].Data
70+
dis, ok := data.(*view.DistributionData)
71+
if !ok {
72+
t.Errorf("want DistributionData, got %+v", data)
73+
}
74+
wantBuckets := []int64{0, 0, 1}
75+
if !reflect.DeepEqual(dis.CountPerBucket, wantBuckets) {
76+
t.Errorf("want buckets %v, got %v", wantBuckets, dis.CountPerBucket)
77+
}
78+
for i, e := range dis.ExemplarsPerBucket {
79+
// Exemplar slice should be [nil, nil, exemplar]
80+
if i != 2 && e != nil {
81+
t.Errorf("want nil exemplar, got %v", e)
82+
}
83+
if i == 2 {
84+
wantExemplar := &metricdata.Exemplar{Value: 12, Attachments: attachments}
85+
if diff := cmpExemplar(e, wantExemplar); diff != "" {
86+
t.Fatalf("Unexpected Exemplar -got +want: %s", diff)
87+
}
88+
}
89+
}
90+
}
91+
92+
// Compare exemplars while ignoring exemplar timestamp, since timestamp is non-deterministic.
93+
func cmpExemplar(got, want *metricdata.Exemplar) string {
94+
return cmp.Diff(got, want, cmpopts.IgnoreFields(metricdata.Exemplar{}, "Timestamp"), cmpopts.IgnoreUnexported(metricdata.Exemplar{}))
95+
}

0 commit comments

Comments
 (0)