feat: get operation info from context

This commit is contained in:
Daniel G. Taylor 2021-05-03 22:31:01 +02:00
parent 8ddda3bb3e
commit fbeb18f3cf
No known key found for this signature in database
GPG key ID: 74AE195C5112E534
4 changed files with 96 additions and 1 deletions

View file

@ -682,6 +682,24 @@ middleware.NewLogger = func() (*zap.Logger, error) {
}
```
### Getting Operation Info
When setting up logging (or metrics, or auditing) you may want to have access to some additional information like the ID of the current operation. You can fetch this from the context **after** the handler has run.
```go
app.Middleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// First, make sure the handler function runs!
next.ServeHTTP(w, r)
// After that, you can get the operation info.
opInfo := GetOperationInfo(r.Context())
fmt.Println(opInfo.ID)
fmt.Println(opInfo.URITemplate)
})
})
```
## Lazy-loading at Server Startup
You can register functions to run before the server starts, allowing for things like lazy-loading dependencies. It is safe to call this method multiple times.

View file

@ -1,6 +1,7 @@
package huma
import (
"context"
"fmt"
"net/http"
"reflect"
@ -11,6 +12,29 @@ import (
"github.com/danielgtaylor/huma/schema"
)
// OperationInfo describes an operation. It contains useful information for
// logging, metrics, auditing, etc.
type OperationInfo struct {
ID string
URITemplate string
Summary string
Tags []string
}
// GetOperationInfo returns information about the current Huma operation. This
// will only be populated *after* routing has been handled, meaning *after*
// `next.ServeHTTP(w, r)` has been called in your middleware.
func GetOperationInfo(ctx context.Context) *OperationInfo {
if oi := ctx.Value(opIDContextKey); oi != nil {
return oi.(*OperationInfo)
}
return &OperationInfo{
ID: "unknown",
Tags: []string{},
}
}
// Operation represents an operation (an HTTP verb, e.g. GET / PUT) against
// a resource attached to a router.
type Operation struct {
@ -236,6 +260,13 @@ func (o *Operation) Run(handler interface{}) {
}
}
// Update the operation info for loggers/metrics/etc middlware to use later.
opInfo := GetOperationInfo(r.Context())
opInfo.ID = o.id
opInfo.URITemplate = o.resource.path
opInfo.Summary = o.summary
opInfo.Tags = append([]string{}, o.resource.tags...)
ctx := &hcontext{
Context: r.Context(),
ResponseWriter: w,

View file

@ -19,6 +19,10 @@ type contextKey string
// context value.
var connContextKey contextKey = "huma-request-conn"
// opIDContextKey is used to get the operation name after request routing
// has finished.
var opIDContextKey contextKey = "huma-operation-id"
// GetConn gets the underlying `net.Conn` from a context.
func GetConn(ctx context.Context) net.Conn {
conn := ctx.Value(connContextKey)
@ -351,11 +355,15 @@ func New(docs, version string) *Router {
ctx.WriteError(http.StatusMethodNotAllowed, fmt.Sprintf("No handler for method %s", r.Method))
}))
// Automatically add links to OpenAPI and docs.
r.Middleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Inject the operation info before other middleware so that the later
// middleware will have access to it.
req = req.WithContext(context.WithValue(req.Context(), opIDContextKey, &OperationInfo{}))
next.ServeHTTP(w, req)
// Automatically add links to OpenAPI and docs.
if req.URL.Path == "/" {
link := w.Header().Get("link")
if link != "" {

View file

@ -2,6 +2,7 @@ package huma
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
@ -362,3 +363,40 @@ func TestCustomRequestSchema(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
}
func TestGetOperationName(t *testing.T) {
app := newTestRouter()
var opInfo *OperationInfo
app.Middleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
opInfo = GetOperationInfo(r.Context())
})
})
app.Resource("/").Get("test-id", "doc",
NewResponse(http.StatusOK, "ok"),
).Run(func(ctx Context) {
// Do nothing!
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/", nil)
app.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Result().StatusCode)
assert.Equal(t, "test-id", opInfo.ID)
assert.Equal(t, "/", opInfo.URITemplate)
assert.Equal(t, "doc", opInfo.Summary)
assert.Equal(t, []string{}, opInfo.Tags)
}
func TestGetOperationDoesNotCrash(t *testing.T) {
ctx := context.Background()
assert.NotPanics(t, func() {
info := GetOperationInfo(ctx)
assert.NotNil(t, info)
})
}