// Package v2 adapts lambdalog to API Gateway HTTP API (v2) events.
//
// See mercemay.top/src/lambdalog/adapters/apigateway/v2/.
package v2
import (
"context"
"time"
"github.com/aws/aws-lambda-go/events"
)
// Logger is the narrow interface consumed by the adapter.
type Logger interface {
Info(msg string, fields ...Field)
Error(msg string, fields ...Field)
With(fields ...Field) Logger
}
// Field mirrors encoder.Field.
type Field struct {
Key string
Value any
}
// Handler is the shape of a user handler for HTTP API v2.
type Handler func(context.Context, events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error)
// Wrap returns a Handler that logs around next. The v2 event format
// simplifies the request data significantly: method and path live in
// RequestContext.HTTP, and there is no Resource field.
func Wrap(logger Logger, next Handler) Handler {
return func(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
start := time.Now()
http := req.RequestContext.HTTP
l := logger.With(
Field{Key: "method", Value: http.Method},
Field{Key: "path", Value: http.Path},
Field{Key: "route", Value: req.RouteKey},
)
l.Info("http.request.start")
resp, err := next(ctx, req)
dur := time.Since(start)
status := resp.StatusCode
if status == 0 {
status = 500
}
if err != nil {
l.Error("http.request.end",
Field{Key: "status", Value: status},
Field{Key: "duration_ms", Value: dur.Milliseconds()},
Field{Key: "error", Value: err.Error()})
} else {
l.Info("http.request.end",
Field{Key: "status", Value: status},
Field{Key: "duration_ms", Value: dur.Milliseconds()})
}
return resp, err
}
}
// ExtractFields reads the common set of request fields for custom adapters.
func ExtractFields(req events.APIGatewayV2HTTPRequest) []Field {
fs := []Field{
{Key: "method", Value: req.RequestContext.HTTP.Method},
{Key: "path", Value: req.RequestContext.HTTP.Path},
{Key: "route", Value: req.RouteKey},
}
if id := req.RequestContext.RequestID; id != "" {
fs = append(fs, Field{Key: "api_request_id", Value: id})
}
if ip := req.RequestContext.HTTP.SourceIP; ip != "" {
fs = append(fs, Field{Key: "source_ip", Value: ip})
}
return fs
}
// ErrorResponse returns a simple response usable by panic recovery paths.
func ErrorResponse(status int, body string) events.APIGatewayV2HTTPResponse {
return events.APIGatewayV2HTTPResponse{
StatusCode: status,
Body: body,
Headers: map[string]string{"Content-Type": "text/plain"},
}
}
// JSONResponse is a convenience constructor for JSON payloads. It assumes
// body is already a valid JSON-encoded string.
func JSONResponse(status int, body string) events.APIGatewayV2HTTPResponse {
return events.APIGatewayV2HTTPResponse{
StatusCode: status,
Body: body,
Headers: map[string]string{"Content-Type": "application/json"},
}
}