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

Logging Errors in Huma Middleware #679

Open
bioform opened this issue Dec 12, 2024 · 2 comments
Open

Logging Errors in Huma Middleware #679

bioform opened this issue Dec 12, 2024 · 2 comments
Labels
question Further information is requested

Comments

@bioform
Copy link

bioform commented Dec 12, 2024

I am currently working on a Huma middleware and need assistance with logging errors. Specifically, I would like to log the following:

  1. Input structure
  2. Output errors or the entire output structure

Although I managed to log HTTP status responses, I have not found a proper way to log input parameters and output errors. Here is the current code snippet I am using:

package middleware

import (
	"my/logging"
	"github.com/danielgtaylor/huma/v2"
)

func HumaTracing(ctx huma.Context, next func(huma.Context)) {
	// Call the next middleware or handler
	next(ctx)

	// Inspect the response for errors
	if ctx.Status() >= 400 {
		log := logging.Logger(ctx.Context())
		log.Error("Handler error", "status", ctx.Status())
	}
}

Is there a general way to configure comprehensive logging in a Huma application, including input parameters and output errors?

@danielgtaylor danielgtaylor added the question Further information is requested label Dec 18, 2024
@danielgtaylor
Copy link
Owner

@bioform I see two options you could use here:

  1. Middleware where you use a custom huma.Context implementation that loads the request and response into memory before passing it on to Huma or writing it to the client as a response. At the end of the request you can dump the request/response stored in memory to your log. You'll need to override various methods for getting params and reading/writing the body in the context. This option gives you access to the bytes of the request/response.
  2. You can provide your own operation registration function that wraps the given handler with your logging code. Instead of a middleware you are essentially writing a wrapper handler so you'll have access to the parsed input/output and the error instance and can log them. For example: https://go.dev/play/p/EqErXbEUQl6

Hope that helps!

@timruffles
Copy link

timruffles commented Dec 19, 2024

@danielgtaylor I've also wanted the ability to get the error returned from the handler for middleware. I'm new to huma so apologies if this is silly, but would it make sense to add a HandlerError() error method to huma.Context for middleware that want to observe errors? I hacked in something similar using an Transformer that wrote into a mutable field in context, which post-request code in middleware could read from. Works fine, but icky :)

One thing to note: the error returned from handler is the thing I'd like, rather than the error response sent to the client. Having the actual error value will provide access to things like the stack-trace, and errors.Is/errors.As to extract more structured data from the error value.

humaCfg.Transformers = append(humaCfg.Transformers, RecordErrorTransformer)
api := huma.New(humaCfg)
api.Use(RecordErrorMiddleware)
api.Use(ErrorMetricsMiddleware)

// hacky transformer that is called by huma's operation handler, storing errors in the context
func RecordErrorTransformer(ctx huma.Context, status string, v any) (any, error) {
	errResponse, ok := v.(error)
	if !ok {
		return v, nil
	}
	writeHandlerError(ctx.Context().Value(handlerErrCtxKey{}), errResponse)
  	return v, nil
}

// adds a key to ctx with a stateful value we can update with error
func RecordErrorMiddleware(ctx huma.Context, next func(huma.Context)) {
	next(huma.WithValue(ctx, handlerErrCtxKey{}, &handlerErrState{}))
}

// this is an example of how this is used. If we had `ctx.HandlerError()` it'd be used in the same way
func ErrorMetricsMiddleware(ctx huma.Context, next func(huma.Context)) {
	defer () {
		err := getHandlerError(ctx.Context().Value(handlerErrCtxKey{}))
		if err != nil {
			updateErrorMetrics(err)
		}
	}
	next(ctx)
}

// these methods read/write from the mutable value stored in the req context by RecordErrorMiddleware
func getHandlerError(handlerState any) error {}
func writeHandlerError(handlerState any, err error) {}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants