event: semi-automatic trace duration metric
If the call to Builder.End includes a DurationMetric label, then emit
a Metric measuring the time between the parent Start call and the End.
Also, fix a bug in Builder.Start where the synthesized End event
didn't have the same labels as the Start event.
Change-Id: If5e0598e69628e6300b8590a0c607426237aa2b7
Reviewed-on: https://go-review.googlesource.com/c/exp/+/326673
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/event/builder.go b/event/builder.go
index 6966078..f90804f 100644
--- a/event/builder.go
+++ b/event/builder.go
@@ -33,10 +33,11 @@
const preallocateLabels = 6
type builder struct {
- exporter *Exporter
- Event Event
- labels [preallocateLabels]Label
- id uint64
+ exporter *Exporter
+ Event Event
+ labels [preallocateLabels]Label
+ id uint64
+ parentStart time.Time
}
var builderPool = sync.Pool{New: func() interface{} { return &builder{} }}
@@ -54,12 +55,13 @@
var builderID uint64 // atomic
func newBuilder(ctx context.Context) *builder {
- exporter, parent := FromContext(ctx)
+ exporter, parent, parentStart := FromContext(ctx)
if exporter == nil {
return nil
}
b := allocBuilder()
b.exporter = exporter
+ b.parentStart = parentStart
b.Event.Labels = b.labels[:0]
b.Event.Parent = parent
return b
@@ -196,7 +198,7 @@
if b.data.Event.Namespace == "" {
b.data.Event.Namespace = mv.m.Descriptor().Namespace()
}
- b.data.Event.Labels = append(b.data.Event.Labels, MetricVal.Of(mv.v), MetricKey.Of(ValueOf(mv.m)))
+ b.data.Event.Labels = append(b.data.Event.Labels, MetricVal.Of(mv.v), MetricKey.Of(mv.m))
b.data.exporter.prepare(&b.data.Event)
b.data.exporter.handler.Metric(b.ctx, &b.data.Event)
}
@@ -225,6 +227,12 @@
}
checkValid(b.data, b.builderID)
if b.data.exporter.tracingEnabled() {
+ // If there is a DurationMetric label, emit a Metric event
+ // with the time since Start was called.
+ if v, ok := DurationMetric.Find(&b.data.Event); ok {
+ m := v.(*Duration)
+ b.Clone().Metric(m.Record(time.Since(b.data.parentStart)))
+ }
b.data.exporter.mu.Lock()
defer b.data.exporter.mu.Unlock()
b.data.Event.Labels = append(b.data.Event.Labels, End.Value())
@@ -267,18 +275,18 @@
defer b.data.exporter.mu.Unlock()
b.data.exporter.lastEvent++
traceID := b.data.exporter.lastEvent
+ // create the end builder
+ eb := b.Clone()
+ eb.data.Event.Parent = traceID
+
b.data.Event.Labels = append(b.data.Event.Labels, Trace.Of(traceID))
b.data.exporter.prepare(&b.data.Event)
- // create the end builder
- eb := Builder{}
- eb.data = allocBuilder()
- eb.builderID = eb.data.id
- eb.data.exporter = b.data.exporter
- eb.data.Event.Parent = traceID
// and now deliver the start event
b.data.Event.Labels = append(b.data.Event.Labels, Name.Of(name))
- ctx = newContext(ctx, b.data.exporter, traceID)
+ now := time.Now()
+ ctx = newContext(ctx, b.data.exporter, traceID, now)
ctx = b.data.exporter.handler.Start(ctx, &b.data.Event)
+ eb.data.parentStart = now
eb.ctx = ctx
end = eb.End
}
diff --git a/event/builder_test.go b/event/builder_test.go
index ad1185c..5e0174f 100644
--- a/event/builder_test.go
+++ b/event/builder_test.go
@@ -11,6 +11,7 @@
"fmt"
"strings"
"testing"
+ "time"
"github.com/google/go-cmp/cmp"
"golang.org/x/exp/event"
@@ -66,7 +67,7 @@
func TestTraceBuilder(t *testing.T) {
// Verify that the context returned from the handler is also returned from Start,
// and is the context passed to End.
- ctx := event.WithExporter(context.Background(), event.NewExporter(&testTraceHandler{t}, nil))
+ ctx := event.WithExporter(context.Background(), event.NewExporter(&testTraceHandler{t: t}, nil))
ctx, end := event.To(ctx).Start("s")
val := ctx.Value("x")
if val != 1 {
@@ -76,13 +77,10 @@
}
type testTraceHandler struct {
+ event.NopHandler
t *testing.T
}
-func (*testTraceHandler) Log(ctx context.Context, _ *event.Event) {}
-func (*testTraceHandler) Annotate(ctx context.Context, _ *event.Event) {}
-func (*testTraceHandler) Metric(ctx context.Context, _ *event.Event) {}
-
func (*testTraceHandler) Start(ctx context.Context, _ *event.Event) context.Context {
return context.WithValue(ctx, "x", 1)
}
@@ -129,6 +127,48 @@
})
}
+func TestTraceDuration(t *testing.T) {
+ // Verify that a trace can can emit a latency metric.
+ dur := event.NewDuration("test")
+ want := 200 * time.Millisecond
+
+ check := func(t *testing.T, h *testTraceDurationHandler) {
+ if !h.got.HasValue() {
+ t.Fatal("no metric value")
+ }
+ got := h.got.Duration().Round(50 * time.Millisecond)
+ if got != want {
+ t.Fatalf("got %s, want %s", got, want)
+ }
+ }
+
+ t.Run("end function", func(t *testing.T) {
+ h := &testTraceDurationHandler{}
+ ctx := event.WithExporter(context.Background(), event.NewExporter(h, nil))
+ ctx, end := event.To(ctx).With(event.DurationMetric.Of(dur)).Start("s")
+ time.Sleep(want)
+ end()
+ check(t, h)
+ })
+ t.Run("End method", func(t *testing.T) {
+ h := &testTraceDurationHandler{}
+ ctx := event.WithExporter(context.Background(), event.NewExporter(h, nil))
+ ctx, _ = event.To(ctx).Start("s")
+ time.Sleep(want)
+ event.To(ctx).With(event.DurationMetric.Of(dur)).End()
+ check(t, h)
+ })
+}
+
+type testTraceDurationHandler struct {
+ event.NopHandler
+ got event.Value
+}
+
+func (t *testTraceDurationHandler) Metric(ctx context.Context, e *event.Event) {
+ t.got, _ = event.MetricVal.Find(e)
+}
+
func BenchmarkBuildContext(b *testing.B) {
// How long does it take to deliver an event from a nested context?
c := event.NewCounter("c")
diff --git a/event/common.go b/event/common.go
index e9c9704..b7be86c 100644
--- a/event/common.go
+++ b/event/common.go
@@ -4,19 +4,25 @@
package event
-const Message = stringKey("msg")
-const Name = stringKey("name")
-const Trace = traceKey("trace")
-const End = tagKey("end")
-const MetricKey = valueKey("metric")
-const MetricVal = valueKey("metricValue")
-const Error = errorKey("error")
+const (
+ Message = stringKey("msg")
+ Name = stringKey("name")
+ Trace = traceKey("trace")
+ End = tagKey("end")
+ MetricKey = interfaceKey("metric")
+ MetricVal = valueKey("metricValue")
+ DurationMetric = interfaceKey("durationMetric")
+ Error = errorKey("error")
+)
-type stringKey string
-type traceKey string
-type tagKey string
-type valueKey string
-type errorKey string
+type (
+ stringKey string
+ traceKey string
+ tagKey string
+ valueKey string
+ interfaceKey string
+ errorKey string
+)
// Of creates a new message Label.
func (k stringKey) Of(msg string) Label {
@@ -75,6 +81,23 @@
func (k valueKey) Find(ev *Event) (Value, bool) {
return lookupValue(string(k), ev.Labels)
+}
+
+func (k interfaceKey) Of(v interface{}) Label {
+ return Label{Name: string(k), Value: ValueOf(v)}
+}
+
+func (k interfaceKey) Matches(ev *Event) bool {
+ _, found := k.Find(ev)
+ return found
+}
+
+func (k interfaceKey) Find(ev *Event) (interface{}, bool) {
+ v, ok := lookupValue(string(k), ev.Labels)
+ if !ok {
+ return nil, false
+ }
+ return v.Interface(), true
}
diff --git a/event/event.go b/event/event.go
index 221d0ce..18a737d 100644
--- a/event/event.go
+++ b/event/event.go
@@ -42,7 +42,7 @@
// The exporter is called synchronously from the event call site, so it should
// return quickly so as not to hold up user code.
func WithExporter(ctx context.Context, e *Exporter) context.Context {
- return newContext(ctx, e, 0)
+ return newContext(ctx, e, 0, time.Time{})
}
// SetDefaultExporter sets an exporter that is used if no exporter can be
diff --git a/event/export.go b/event/export.go
index 84af70c..7464015 100644
--- a/event/export.go
+++ b/event/export.go
@@ -49,8 +49,9 @@
// contextValue is stored by value in the context to track the exporter and
// current parent event.
type contextValue struct {
- exporter *Exporter
- parent uint64
+ exporter *Exporter
+ parent uint64
+ startTime time.Time // for trace latency
}
var (
@@ -81,16 +82,16 @@
return (*Exporter)(atomic.LoadPointer(&defaultExporter))
}
-func newContext(ctx context.Context, exporter *Exporter, parent uint64) context.Context {
- return context.WithValue(ctx, contextKey, contextValue{exporter: exporter, parent: parent})
+func newContext(ctx context.Context, exporter *Exporter, parent uint64, t time.Time) context.Context {
+ return context.WithValue(ctx, contextKey, contextValue{exporter: exporter, parent: parent, startTime: t})
}
-// FromContext returns the exporter and current trace for the supplied context.
-func FromContext(ctx context.Context) (*Exporter, uint64) {
+// FromContext returns the exporter, parentID and parent start time for the supplied context.
+func FromContext(ctx context.Context) (*Exporter, uint64, time.Time) {
if v, ok := ctx.Value(contextKey).(contextValue); ok {
- return v.exporter, v.parent
+ return v.exporter, v.parent, v.startTime
}
- return getDefaultExporter(), 0
+ return getDefaultExporter(), 0, time.Time{}
}
// prepare events before delivering to the underlying handler.