// Package v1 adapts lambdalog to API Gateway REST API (v1) events. The
// shape of the event differs from HTTP API v2 enough that a separate
// adapter is simpler than a parameterised one.
//
// See mercemay.top/src/lambdalog/adapters/apigateway/v1/.
package v1
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 REST API v1.
type Handler func(context.Context, events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
// Wrap returns a Handler that logs around next. Each call produces a single
// "http.request" record with the status, path, and elapsed milliseconds.
func Wrap(logger Logger, next Handler) Handler {
return func(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
start := time.Now()
l := logger.With(
Field{Key: "method", Value: req.HTTPMethod},
Field{Key: "path", Value: req.Path},
Field{Key: "stage", Value: req.RequestContext.Stage},
)
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 common identifying fields out of req. It is exported
// so callers building their own wrappers can reuse the logic.
func ExtractFields(req events.APIGatewayProxyRequest) []Field {
fs := []Field{
{Key: "method", Value: req.HTTPMethod},
{Key: "path", Value: req.Path},
{Key: "stage", Value: req.RequestContext.Stage},
}
if id := req.RequestContext.RequestID; id != "" {
fs = append(fs, Field{Key: "api_request_id", Value: id})
}
if ip := req.RequestContext.Identity.SourceIP; ip != "" {
fs = append(fs, Field{Key: "source_ip", Value: ip})
}
return fs
}
// ResponseSize approximates the emitted body length. It is intentionally a
// best-effort estimate: base64 payloads are counted as their decoded
// length.
func ResponseSize(resp events.APIGatewayProxyResponse) int {
if resp.IsBase64Encoded {
return (len(resp.Body) * 3) / 4
}
return len(resp.Body)
}
// ErrorResponse returns a well-formed response body for the given code.
// The adapter uses it when the wrapped handler panics so clients do not
// receive a raw 502 from the Lambda runtime.
func ErrorResponse(status int, body string) events.APIGatewayProxyResponse {
return events.APIGatewayProxyResponse{
StatusCode: status,
Body: body,
Headers: map[string]string{"Content-Type": "text/plain"},
}
}