Skip to content

Commit 9dac8a5

Browse files
authored
Add structured logging helper (#614)
1 parent 6252f73 commit 9dac8a5

File tree

3 files changed

+615
-0
lines changed

3 files changed

+615
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
package lambdacontext_test
5+
6+
import (
7+
"context"
8+
"log/slog"
9+
10+
"github.com/aws/aws-lambda-go/lambda"
11+
"github.com/aws/aws-lambda-go/lambdacontext"
12+
)
13+
14+
// ExampleNewLogger demonstrates the simplest usage of NewLogger for structured logging.
15+
// The logger automatically injects requestId from Lambda context into each log record.
16+
func ExampleNewLogger() {
17+
// Set up the Lambda-aware slog logger
18+
slog.SetDefault(lambdacontext.NewLogger())
19+
20+
lambda.Start(func(ctx context.Context) (string, error) {
21+
// Use slog.InfoContext to include Lambda context in logs
22+
slog.InfoContext(ctx, "processing request", "action", "example")
23+
return "success", nil
24+
})
25+
}
26+
27+
// ExampleNewLogHandler demonstrates using NewLogHandler for more control.
28+
func ExampleNewLogHandler() {
29+
// Set up the Lambda-aware slog handler
30+
slog.SetDefault(slog.New(lambdacontext.NewLogHandler()))
31+
32+
lambda.Start(func(ctx context.Context) (string, error) {
33+
slog.InfoContext(ctx, "processing request", "action", "example")
34+
return "success", nil
35+
})
36+
}
37+
38+
// ExampleNewLogHandler_withOptions demonstrates NewLogHandler with additional fields.
39+
// Use WithFunctionARN() and WithTenantID() to include extra context.
40+
func ExampleNewLogHandler_withOptions() {
41+
// Set up handler with function ARN and tenant ID fields
42+
slog.SetDefault(slog.New(lambdacontext.NewLogHandler(
43+
lambdacontext.WithFunctionARN(),
44+
lambdacontext.WithTenantID(),
45+
)))
46+
47+
lambda.Start(func(ctx context.Context) (string, error) {
48+
slog.InfoContext(ctx, "multi-tenant request", "tenant", "acme-corp")
49+
return "success", nil
50+
})
51+
}
52+
53+
// ExampleWithFunctionARN demonstrates using WithFunctionARN to include the function ARN.
54+
func ExampleWithFunctionARN() {
55+
// Include only function ARN
56+
slog.SetDefault(lambdacontext.NewLogger(
57+
lambdacontext.WithFunctionARN(),
58+
))
59+
60+
lambda.Start(func(ctx context.Context) (string, error) {
61+
// Log output will include "functionArn" field
62+
slog.InfoContext(ctx, "function invoked")
63+
return "success", nil
64+
})
65+
}

lambdacontext/logger.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
6+
package lambdacontext
7+
8+
import (
9+
"context"
10+
"log/slog"
11+
"os"
12+
)
13+
14+
// logFormat is the log format from AWS_LAMBDA_LOG_FORMAT (TEXT or JSON)
15+
var logFormat = os.Getenv("AWS_LAMBDA_LOG_FORMAT")
16+
17+
// logLevel is the log level from AWS_LAMBDA_LOG_LEVEL
18+
var logLevel = os.Getenv("AWS_LAMBDA_LOG_LEVEL")
19+
20+
// field represents a Lambda context field to include in log records.
21+
type field struct {
22+
key string
23+
value func(*LambdaContext) string
24+
}
25+
26+
// logOptions holds configuration for the Lambda log handler.
27+
type logOptions struct {
28+
fields []field
29+
}
30+
31+
// LogOption is a functional option for configuring the Lambda log handler.
32+
type LogOption func(*logOptions)
33+
34+
// WithFunctionARN includes the invoked function ARN in log records.
35+
func WithFunctionARN() LogOption {
36+
return func(o *logOptions) {
37+
o.fields = append(o.fields, field{"functionArn", func(lc *LambdaContext) string { return lc.InvokedFunctionArn }})
38+
}
39+
}
40+
41+
// WithTenantID includes the tenant ID in log records (for multi-tenant functions).
42+
func WithTenantID() LogOption {
43+
return func(o *logOptions) {
44+
o.fields = append(o.fields, field{"tenantId", func(lc *LambdaContext) string { return lc.TenantID }})
45+
}
46+
}
47+
48+
// NewLogHandler returns a [slog.Handler] for AWS Lambda structured logging.
49+
// It reads AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL from environment,
50+
// and injects requestId from Lambda context into each log record.
51+
//
52+
// By default, only requestId is injected. Use WithFunctionARN or WithTenantID to include more.
53+
// See the package examples for usage.
54+
func NewLogHandler(opts ...LogOption) slog.Handler {
55+
options := &logOptions{}
56+
for _, opt := range opts {
57+
opt(options)
58+
}
59+
60+
level := parseLogLevel()
61+
handlerOpts := &slog.HandlerOptions{
62+
Level: level,
63+
ReplaceAttr: ReplaceAttr,
64+
}
65+
66+
var h slog.Handler
67+
if logFormat == "JSON" {
68+
h = slog.NewJSONHandler(os.Stdout, handlerOpts)
69+
} else {
70+
h = slog.NewTextHandler(os.Stdout, handlerOpts)
71+
}
72+
73+
return &lambdaHandler{handler: h, fields: options.fields}
74+
}
75+
76+
// NewLogger returns a [*slog.Logger] configured for AWS Lambda structured logging.
77+
// This is a convenience function equivalent to slog.New(NewLogHandler(opts...)).
78+
func NewLogger(opts ...LogOption) *slog.Logger {
79+
return slog.New(NewLogHandler(opts...))
80+
}
81+
82+
// ReplaceAttr maps slog's default keys to AWS Lambda's log format (time->timestamp, msg->message).
83+
func ReplaceAttr(groups []string, attr slog.Attr) slog.Attr {
84+
if len(groups) > 0 {
85+
return attr
86+
}
87+
88+
switch attr.Key {
89+
case slog.TimeKey:
90+
attr.Key = "timestamp"
91+
case slog.MessageKey:
92+
attr.Key = "message"
93+
}
94+
return attr
95+
}
96+
97+
// lambdaHandler wraps a slog.Handler to inject Lambda context fields.
98+
type lambdaHandler struct {
99+
handler slog.Handler
100+
fields []field
101+
}
102+
103+
// Enabled implements slog.Handler.
104+
func (h *lambdaHandler) Enabled(ctx context.Context, level slog.Level) bool {
105+
return h.handler.Enabled(ctx, level)
106+
}
107+
108+
// Handle implements slog.Handler.
109+
func (h *lambdaHandler) Handle(ctx context.Context, r slog.Record) error {
110+
if lc, ok := FromContext(ctx); ok {
111+
r.AddAttrs(slog.String("requestId", lc.AwsRequestID))
112+
113+
for _, field := range h.fields {
114+
if v := field.value(lc); v != "" {
115+
r.AddAttrs(slog.String(field.key, v))
116+
}
117+
}
118+
}
119+
return h.handler.Handle(ctx, r)
120+
}
121+
122+
// WithAttrs implements slog.Handler.
123+
func (h *lambdaHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
124+
return &lambdaHandler{
125+
handler: h.handler.WithAttrs(attrs),
126+
fields: h.fields,
127+
}
128+
}
129+
130+
// WithGroup implements slog.Handler.
131+
func (h *lambdaHandler) WithGroup(name string) slog.Handler {
132+
return &lambdaHandler{
133+
handler: h.handler.WithGroup(name),
134+
fields: h.fields,
135+
}
136+
}
137+
138+
func parseLogLevel() slog.Level {
139+
switch logLevel {
140+
case "DEBUG":
141+
return slog.LevelDebug
142+
case "INFO":
143+
return slog.LevelInfo
144+
case "WARN":
145+
return slog.LevelWarn
146+
case "ERROR":
147+
return slog.LevelError
148+
default:
149+
return slog.LevelInfo
150+
}
151+
}

0 commit comments

Comments
 (0)