File size: 5,620 Bytes
fea99b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | package httptransport
import (
"context"
"errors"
"net/http"
"sync/atomic"
"go.opentelemetry.io/otel"
semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
"go.opentelemetry.io/otel/trace"
"github.com/openmeterio/openmeter/pkg/contextx"
"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
"github.com/openmeterio/openmeter/pkg/framework/operation"
"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport/encoder"
"github.com/openmeterio/openmeter/pkg/models"
)
var defaultHandlerOptions = []HandlerOption{
WithErrorEncoder(commonhttp.GenericErrorEncoder()),
}
// tracer reads the globally configured TracerProvider (set during telemetry init).
// Used to start an application-level span named after the handler operation, as a
// child of the otelhttp server span.
var tracer = otel.Tracer("github.com/openmeterio/openmeter/pkg/framework/transport/httptransport")
// operationSpansEnabled is a global toggle for the per-operation child span. It is off
// by default — the span is added to every operation request, so enabling it across the
// whole API surface meaningfully increases trace-span volume. Set once at startup via
// EnableOperationSpans.
var operationSpansEnabled atomic.Bool
// EnableOperationSpans globally enables or disables the application-level per-operation
// child span. Intended to be called once during startup from telemetry configuration.
func EnableOperationSpans(enabled bool) {
operationSpansEnabled.Store(enabled)
}
type Handler[Request any, Response any] interface {
ServeHTTP(w http.ResponseWriter, r *http.Request)
Chain(outer operation.Middleware[Request, Response], others ...operation.Middleware[Request, Response]) Handler[Request, Response]
}
// NewHandler returns a new HTTP handler that wraps the given [operation.Operation].
func NewHandler[Request any, Response any](
requestDecoder RequestDecoder[Request],
op operation.Operation[Request, Response],
responseEncoder encoder.ResponseEncoder[Response],
options ...HandlerOption,
) Handler[Request, Response] {
return newHandler(requestDecoder, op, responseEncoder, options...)
}
func newHandler[Request any, Response any](
requestDecoder RequestDecoder[Request],
op operation.Operation[Request, Response],
responseEncoder encoder.ResponseEncoder[Response],
options ...HandlerOption,
) handler[Request, Response] {
h := handler[Request, Response]{
operation: op,
decodeRequest: requestDecoder,
encodeResponse: responseEncoder,
}
options = append(options, defaultHandlerOptions...)
h.apply(options)
return h
}
type handler[Request any, Response any] struct {
operation operation.Operation[Request, Response]
operationNameFunc func(ctx context.Context) string
decodeRequest RequestDecoder[Request]
encodeResponse encoder.ResponseEncoder[Response]
errorEncoders []encoder.ErrorEncoder
errorHandler ErrorHandler
}
type RequestDecoder[Request any] func(ctx context.Context, r *http.Request) (Request, error)
// ErrorHandler receives a transport error to be processed for diagnostic purposes.
// Usually this means logging the error.
type ErrorHandler interface {
HandleContext(ctx context.Context, err error)
}
func (h handler[Request, Response]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// TODO: rewrite this as a generic hook
if h.operationNameFunc != nil {
name := h.operationNameFunc(ctx)
ctx = contextx.WithAttr(ctx, string(semconv.HTTPRouteKey), name)
// When enabled globally (EnableOperationSpans), start an application-level span
// named after the operation, as a child of the otelhttp server span. The server
// span stays route-named; this one carries the operation identity (e.g.
// "query-governance-access") and exposes the handler-vs-middleware timing split.
// Off by default: it adds a span to every operation request API-wide, so it is
// gated by a single startup toggle rather than enabled unconditionally.
if operationSpansEnabled.Load() {
var span trace.Span
ctx, span = tracer.Start(ctx, name)
defer span.End()
}
}
request, err := h.decodeRequest(ctx, r)
if err != nil {
// Might be a client error (can be encoded, non-terminal)
// Might be a server error (terminal)
handled := h.encodeError(ctx, err, w, r)
if !handled {
h.errorHandler.HandleContext(ctx, err)
}
return
}
response, err := h.operation(ctx, request)
if err != nil {
// Might be a client error (can be encoded, non-terminal)
// Might be a server error (terminal)
handled := h.encodeError(ctx, err, w, r)
if !handled {
h.errorHandler.HandleContext(ctx, err)
}
return
}
if err := h.encodeResponse(ctx, w, r, response); err != nil {
// Always a server error (terminal)?
h.errorHandler.HandleContext(ctx, err)
return
}
}
func (h handler[Request, Response]) encodeError(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) bool {
for _, errorEncoder := range h.errorEncoders {
if errorEncoder(ctx, err, w, r) {
return true
}
}
if encoder, ok := err.(SelfEncodingError); ok {
if encoder.EncodeError(ctx, w) {
return true
}
}
models.NewStatusProblem(ctx, errors.New("internal server error"), http.StatusInternalServerError).Respond(w)
return false
}
func (h handler[Request, Response]) Chain(outer operation.Middleware[Request, Response], others ...operation.Middleware[Request, Response]) Handler[Request, Response] {
h.operation = operation.Chain(outer, others...)(h.operation)
return h
}
type SelfEncodingError interface {
EncodeError(ctx context.Context, w http.ResponseWriter) bool
}
|