diff --git a/beeline.go b/beeline.go index 9cda6c0f..ddb1a555 100644 --- a/beeline.go +++ b/beeline.go @@ -272,15 +272,20 @@ func Close() { // a Handler, feel free to call AddField freely within your code. Pass it the // context from the request (`r.Context()`) and the key and value you wish to // add.This function is good for span-level data, eg timers or the arguments to -// a specific function call, etc. Fields added here are prefixed with `app.` +// a specific function call, etc. +// +// Field keys added will be prefixed with 'app.' if the 'app.' prefix is not +// already present on the key name. If you provide a key that starts with +// 'app.', both speed and memory allocations are improved, especially within hot +// paths of your application. // // Errors are treated as a special case for convenience: if `val` is of type -// `error` then the key is set to the error's message in the span. +// `error` then the field's value is set to the error's message. func AddField(ctx context.Context, key string, val interface{}) { span := trace.GetSpanFromContext(ctx) if span != nil { - if val != nil { - namespacedKey := "app." + key // Avoid excess parsing/allocation work + if val != nil { // TODO: move this to first check to save looking up the current span when there is no value? + namespacedKey := getNamespacedKey(key) if valErr, ok := val.(error); ok { // treat errors specially because it's a pain to have to // remember to stringify them @@ -297,16 +302,31 @@ func AddField(ctx context.Context, key string, val interface{}) { // Additionally, these fields are packaged up and passed along to downstream // processes if they are also using a beeline. This function is good for adding // context that is better scoped to the request than this specific unit of work, -// eg user IDs, globally relevant feature flags, errors, etc. Fields added here -// are prefixed with `app.` +// eg user IDs, globally relevant feature flags, errors, etc. +// +// Field keys added will be prefixed with 'app.' if the 'app.' prefix is not +// already present on the key name. If you provide a key that starts with +// 'app.', both speed and memory allocations are improved, especially within hot +// paths of your application. func AddFieldToTrace(ctx context.Context, key string, val interface{}) { - namespacedKey := "app." + key // Avoid excess parsing/allocation work tr := trace.GetTraceFromContext(ctx) if tr != nil { + namespacedKey := getNamespacedKey(key) tr.AddField(namespacedKey, val) } } +// getNamespacedKey ensures a key name is prefixed with "app." if the prefix +// isn't already present. If the key is already namespaced, this reduces the +// number of memory allocations needed to add a field to a span. +func getNamespacedKey(key string) string { + const prefix = "app." + if strings.HasPrefix(key, prefix) { + return key + } + return prefix + key +} + // StartSpan lets you start a new span as a child of an already instrumented // handler. If there isn't an existing wrapped handler in the context when this // is called, it will start a new trace. Spans automatically get a `duration_ms` diff --git a/beeline_test.go b/beeline_test.go index 262585d1..58e946e2 100644 --- a/beeline_test.go +++ b/beeline_test.go @@ -105,9 +105,26 @@ func BenchmarkBeelineAddField(b *testing.B) { setupLibhoney(b) ctx, _ := StartSpan(context.Background(), "parent") - for n := 0; n < b.N; n++ { - AddField(ctx, "foo", 1) - } + + b.Run("AddField/no-prefix", func(b *testing.B) { + for n := 0; n < b.N; n++ { + AddField(ctx, "foo", 1) + } + }) + b.Run("AddField/half-prefixed", func(b *testing.B) { + for n := 0; n < b.N; n++ { + if n%2 == 0 { + AddField(ctx, "app.foo", 1) + } else { + AddField(ctx, "foo", 1) + } + } + }) + b.Run("AddField/all-prefixed", func(b *testing.B) { + for n := 0; n < b.N; n++ { + AddField(ctx, "app.foo", 1) + } + }) } func setupLibhoney(t testing.TB) *transmission.MockSender { diff --git a/trace/trace.go b/trace/trace.go index bd102d95..2aa90715 100644 --- a/trace/trace.go +++ b/trace/trace.go @@ -260,7 +260,7 @@ func newSpan() *Span { // AddField adds a key/value pair to this span // // Errors are treated as a special case for convenience: if `val` is of type -// `error` then the key is set to the error's message in the span. +// `error` then the field's value is set to the error's message. func (s *Span) AddField(key string, val interface{}) { // The call to event's AddField is protected by a lock, but this is not always sufficient // See send for why this lock exists