diff --git a/CHANGELOG.md b/CHANGELOG.md index ab91768f636..1b5c995c081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Transform raw (`slog.KindAny`) attribute values to matching `log.Value` types. For example, `[]string{"foo", "bar"}` attribute value is now transformed to `log.SliceValue(log.StringValue("foo"), log.StringValue("bar"))` instead of `log.String("[foo bar"])`. (#6254) - Add the `WithSource` option to the `go.opentelemetry.io/contrib/bridges/otelslog` log bridge to set the `code.*` attributes in the log record that includes the source location where the record was emitted. (#6253) +- Add `ContextWithStartTime` and `StartTimeFromContext` to `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`, which allows setting the start time using go context. (#6137) ### Fixed diff --git a/instrumentation/net/http/otelhttp/handler.go b/instrumentation/net/http/otelhttp/handler.go index e4236ab398c..e555a475f13 100644 --- a/instrumentation/net/http/otelhttp/handler.go +++ b/instrumentation/net/http/otelhttp/handler.go @@ -117,6 +117,11 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http } } + if startTime := StartTimeFromContext(ctx); !startTime.IsZero() { + opts = append(opts, trace.WithTimestamp(startTime)) + requestStartTime = startTime + } + ctx, span := tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...) defer span.End() diff --git a/instrumentation/net/http/otelhttp/start_time_context.go b/instrumentation/net/http/otelhttp/start_time_context.go new file mode 100644 index 00000000000..9476ef01b01 --- /dev/null +++ b/instrumentation/net/http/otelhttp/start_time_context.go @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + +import ( + "context" + "time" +) + +type startTimeContextKeyType int + +const startTimeContextKey startTimeContextKeyType = 0 + +// ContextWithStartTime returns a new context with the provided start time. The +// start time will be used for metrics and traces emitted by the +// instrumentation. Only one labeller can be injected into the context. +// Injecting it multiple times will override the previous calls. +func ContextWithStartTime(parent context.Context, start time.Time) context.Context { + return context.WithValue(parent, startTimeContextKey, start) +} + +// StartTimeFromContext retrieves a time.Time from the provided context if one +// is available. If no start time was found in the provided context, a new, +// zero start time is returned and the second return value is false. +func StartTimeFromContext(ctx context.Context) time.Time { + t, _ := ctx.Value(startTimeContextKey).(time.Time) + return t +} diff --git a/instrumentation/net/http/otelhttp/start_time_context_test.go b/instrumentation/net/http/otelhttp/start_time_context_test.go new file mode 100644 index 00000000000..fd821fbd0a0 --- /dev/null +++ b/instrumentation/net/http/otelhttp/start_time_context_test.go @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package otelhttp + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestStartTimeFromContext(t *testing.T) { + ctx := context.Background() + startTime := StartTimeFromContext(ctx) + assert.True(t, startTime.IsZero()) + + now := time.Now() + ctx = ContextWithStartTime(ctx, now) + startTime = StartTimeFromContext(ctx) + assert.True(t, startTime.Equal(now)) +} diff --git a/instrumentation/net/http/otelhttp/test/handler_test.go b/instrumentation/net/http/otelhttp/test/handler_test.go index 0f56f8af959..8caec99917e 100644 --- a/instrumentation/net/http/otelhttp/test/handler_test.go +++ b/instrumentation/net/http/otelhttp/test/handler_test.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,6 +73,9 @@ func assertScopeMetrics(t *testing.T, sm metricdata.ScopeMetrics, attrs attribut }, } metricdatatest.AssertEqual(t, want, sm.Metrics[2], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue()) + + // verify that the custom start time, which is 10 minutes in the past, is respected. + assert.GreaterOrEqual(t, sm.Metrics[2].Data.(metricdata.Histogram[float64]).DataPoints[0].Sum, float64(10*time.Minute/time.Millisecond)) } func TestHandlerBasics(t *testing.T) { @@ -102,6 +106,9 @@ func TestHandlerBasics(t *testing.T) { if err != nil { t.Fatal(err) } + // set a custom start time 10 minutes in the past. + startTime := time.Now().Add(-10 * time.Minute) + r = r.WithContext(otelhttp.ContextWithStartTime(r.Context(), startTime)) h.ServeHTTP(rr, r) rm := metricdata.ResourceMetrics{} @@ -138,6 +145,7 @@ func TestHandlerBasics(t *testing.T) { if got, expected := string(d), "hello world"; got != expected { t.Fatalf("got %q, expected %q", got, expected) } + assert.Equal(t, startTime, spans[0].StartTime()) } func TestHandlerEmittedAttributes(t *testing.T) {