Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design document for otelhttpconv package #6859

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Prev Previous commit
Next Next commit
add a sample use in an http.Handler
  • Loading branch information
dmathieu committed Mar 11, 2025
commit 4084bc380f250a1830088437770f27a33e07c81d
82 changes: 77 additions & 5 deletions instrumentation/net/http/otelhttpconv/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ One for client. and one for servers. Implementations can use one, the other or b
// RequestWrapper provides a layer on top of `*http.Response` that tracks
// additional information such as bytes read etc.
type ResponseWrapper interface {
http.ResponseWriter

// Duration contains the duration of the HTTP request
Duration() time.Duration

Expand Down Expand Up @@ -86,8 +88,11 @@ type Client interface {
// This method does not create a new span. It retrieves the current one from
// the context.
// It remains the instrumentation's responsibility to start and end spans.
RecordSpan(ctx context.Context, req *http.Request, resp *http.Response)
RecordSpan(ctx context.Context, req *http.Request, w *ResponseWrapper, cfg ...ClientRecordSpanOption)
}

// ClientRecordSpanOption applies options to the RecordSpan method
type ClientRecordSpanOption interface{}
```

#### The Server
Expand All @@ -105,17 +110,19 @@ type Server interface {
// This method does not create a new span. It retrieves the current one from
// the context.
// It remains the instrumentation's responsibility to start and end spans.
RecordSpan(ctx context.Context, req *http.Request, resp *http.Response, ...ServerRecordSpanOptions)
RecordSpan(ctx context.Context, req *http.Request, w *ResponseWrapper, cfg ...ServerRecordSpanOption)
}

// ServerRecordSpanOption applies options to the RecordSpan method
type ServerRecordSpanOption interface{}
```

The `ServerRecordMetricsOption` functional option allows passing optional
parameters to be used within the `RecordSpan` method, such as the HTTP route.
The `ClientRecordSpanOption` and `ServerRecordSpanOption` functional options
allows passing optional parameters to be used within the `RecordSpan` method,
such as the HTTP route.

When the data those options provide is not specified, the related span attributes will not be set.
When the data those options provide is not specified, the related span
attributes will not be set.

#### Request and Response wrappers

Expand Down Expand Up @@ -143,6 +150,71 @@ We may provide additional implementations later on such as:
* An implementation that serves as a proxy to allow combining multiple implementations together.
* An implementation that covers unstable semantic conventions.

#### Example implementation

The implementation example here is kept simple for the purpose of
understandability.

The following code sample provides a simple `http.Handler` that implements the
provided interface to instrument HTTP applications.

Because both the client and server interface are very similar, a client
implementation would be similar too.

```golang
type middleware struct {
operation string

tracer trace.Tracer
propagators propagation.TextMapPropagator
meter metric.Meter
httpconv otelhttpconv.Server
}

// NewMiddleware returns a tracing and metrics instrumentation middleware.
// The handler returned by the middleware wraps a handler
// in a span named after the operation and enriches it with metrics.
func NewMiddleware(operation string) func(http.Handler) http.Handler {
m := middleware{
operation: operation,
tracer: otel.Tracer("http"),
propagators: otel.GetTextMapPropagator(),
meter: otel.Meter("http"),
httpconv: otelhttpconv.NewHTTPConv(otel.Tracer("httpconv"), otel.Meter("httpconv")),
}

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.serveHTTP(w, r, next)
})
}
}

func (m *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http.Handler) {
ctx := m.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))

// We keep creating the span here, as that is something we want to do before
// the middleware stack is run
ctx, span := m.tracer.Start(ctx, fmt.Sprintf("%s %s", r.Method, r.Pattern))
defer span.End()

// NewResponseWrapper wraps additional data into the http.ResponseWriter,
// such as duration, status code and quantity of bytes read.
rww := otelhttpconv.NewResponseWrapper(w)
next.ServeHTTP(rww, r.WithContext(ctx))

// RecordMetrics emits the proper semantic convention metrics
// With data based on the provided response wrapper
m.httpconv.RecordMetrics(ctx, rww)

// RecordSpan emits the proper semantic convention span attributes and events
// With data based on the provided response wrapper
// It must not create a new span. It retrieves the current one from the
// context.
m.httpconv.RecordSpan(ctx, r, rww)
}
```

### Usage

By default, instrumentations should use the official implementation mentioned
Expand Down
Loading