Skip to content

Commit 18f28ed

Browse files
committed
feat: adding structured logging helper
1 parent 6252f73 commit 18f28ed

3 files changed

Lines changed: 512 additions & 0 deletions

File tree

lambdacontext/context.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import (
1515
"strconv"
1616
)
1717

18+
// LogFormatName is the name of the Log Format, either TEXT or JSON
19+
var LogFormatName string
20+
21+
// LogLevelName is the name of the Log Levels for structured logging. Only available when LogFormatName is JSON
22+
var LogLevelName string
23+
1824
// LogGroupName is the name of the log group that contains the log streams of the current Lambda Function
1925
var LogGroupName string
2026

@@ -33,6 +39,8 @@ var FunctionVersion string
3339
var maxConcurrency int
3440

3541
func init() {
42+
LogFormatName = os.Getenv("AWS_LAMBDA_LOG_FORMAT")
43+
LogLevelName = os.Getenv("AWS_LAMBDA_LOG_LEVEL")
3644
LogGroupName = os.Getenv("AWS_LAMBDA_LOG_GROUP_NAME")
3745
LogStreamName = os.Getenv("AWS_LAMBDA_LOG_STREAM_NAME")
3846
FunctionName = os.Getenv("AWS_LAMBDA_FUNCTION_NAME")

lambdacontext/logger.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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+
// Field represents an optional field to include in log records.
15+
type Field struct {
16+
key string
17+
value func(*LambdaContext) string
18+
}
19+
20+
// FunctionArn includes the invoked function ARN in log records.
21+
var FunctionArn = Field{"functionArn", func(lc *LambdaContext) string { return lc.InvokedFunctionArn }} //nolint: staticcheck
22+
23+
// TenantId includes the tenant ID in log records (for multi-tenant functions).
24+
var TenantId = Field{"tenantId", func(lc *LambdaContext) string { return lc.TenantID }} //nolint: staticcheck
25+
26+
// Handler returns a [slog.Handler] for AWS Lambda structured logging.
27+
// It reads AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL from environment,
28+
// and injects requestId from Lambda context into each log record.
29+
//
30+
// By default, only requestId is injected. Pass optional fields to include more:
31+
//
32+
// // Default: only requestId
33+
// slog.SetDefault(slog.New(lambdacontext.Handler()))
34+
//
35+
// // With functionArn and tenantId
36+
// slog.SetDefault(slog.New(lambdacontext.Handler(lambdacontext.FunctionArn, lambdacontext.TenantId)))
37+
func Handler(fields ...Field) slog.Handler {
38+
level := parseLogLevel()
39+
opts := &slog.HandlerOptions{
40+
Level: level,
41+
ReplaceAttr: ReplaceAttr,
42+
}
43+
44+
var h slog.Handler
45+
if LogFormatName == "JSON" {
46+
h = slog.NewJSONHandler(os.Stdout, opts)
47+
} else {
48+
h = slog.NewTextHandler(os.Stdout, opts)
49+
}
50+
51+
return &lambdaHandler{handler: h, fields: fields}
52+
}
53+
54+
// ReplaceAttr maps slog's default keys to AWS Lambda's log format (time->timestamp, msg->message).
55+
func ReplaceAttr(groups []string, attr slog.Attr) slog.Attr {
56+
if len(groups) > 0 {
57+
return attr
58+
}
59+
60+
switch attr.Key {
61+
case slog.TimeKey:
62+
attr.Key = "timestamp"
63+
case slog.MessageKey:
64+
attr.Key = "message"
65+
}
66+
return attr
67+
}
68+
69+
// Attrs returns Lambda context fields as slog-compatible key-value pairs.
70+
// For most use cases, using [Handler] with slog.InfoContext is preferred.
71+
func (lc *LambdaContext) Attrs() []any {
72+
return []any{"requestId", lc.AwsRequestID}
73+
}
74+
75+
// lambdaHandler wraps a slog.Handler to inject Lambda context fields.
76+
type lambdaHandler struct {
77+
handler slog.Handler
78+
fields []Field
79+
}
80+
81+
// Enabled implements slog.Handler.
82+
func (h *lambdaHandler) Enabled(ctx context.Context, level slog.Level) bool {
83+
return h.handler.Enabled(ctx, level)
84+
}
85+
86+
// Handle implements slog.Handler.
87+
func (h *lambdaHandler) Handle(ctx context.Context, r slog.Record) error {
88+
if lc, ok := FromContext(ctx); ok {
89+
r.AddAttrs(slog.String("requestId", lc.AwsRequestID))
90+
91+
for _, f := range h.fields {
92+
if v := f.value(lc); v != "" {
93+
r.AddAttrs(slog.String(f.key, v))
94+
}
95+
}
96+
}
97+
return h.handler.Handle(ctx, r)
98+
}
99+
100+
// WithAttrs implements slog.Handler.
101+
func (h *lambdaHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
102+
return &lambdaHandler{
103+
handler: h.handler.WithAttrs(attrs),
104+
fields: h.fields,
105+
}
106+
}
107+
108+
// WithGroup implements slog.Handler.
109+
func (h *lambdaHandler) WithGroup(name string) slog.Handler {
110+
return &lambdaHandler{
111+
handler: h.handler.WithGroup(name),
112+
fields: h.fields,
113+
}
114+
}
115+
116+
func parseLogLevel() slog.Level {
117+
switch LogLevelName {
118+
case "DEBUG":
119+
return slog.LevelDebug
120+
case "INFO":
121+
return slog.LevelInfo
122+
case "WARN":
123+
return slog.LevelWarn
124+
case "ERROR":
125+
return slog.LevelError
126+
default:
127+
return slog.LevelInfo
128+
}
129+
}

0 commit comments

Comments
 (0)