adapters/apigateway/v2/gateway.go

// 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"},
	}
}