Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lambdacontext/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import (
"strconv"
)

// LogFormat is the log format, either TEXT or JSON (from AWS_LAMBDA_LOG_FORMAT)
var LogFormat string

// LogLevel is the log level for structured logging (from AWS_LAMBDA_LOG_LEVEL). Only available when LogFormat is JSON
var LogLevel string

// LogGroupName is the name of the log group that contains the log streams of the current Lambda Function
var LogGroupName string

Expand All @@ -33,6 +39,8 @@ var FunctionVersion string
var maxConcurrency int

func init() {
LogFormat = os.Getenv("AWS_LAMBDA_LOG_FORMAT")
Comment thread
anzheyazzz marked this conversation as resolved.
Outdated
LogLevel = os.Getenv("AWS_LAMBDA_LOG_LEVEL")
LogGroupName = os.Getenv("AWS_LAMBDA_LOG_GROUP_NAME")
LogStreamName = os.Getenv("AWS_LAMBDA_LOG_STREAM_NAME")
FunctionName = os.Getenv("AWS_LAMBDA_FUNCTION_NAME")
Expand Down
54 changes: 54 additions & 0 deletions lambdacontext/example_logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//go:build go1.21
// +build go1.21

package lambdacontext_test

import (
"context"
"log/slog"

"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-lambda-go/lambdacontext"
)

// ExampleLogHandler demonstrates basic usage of LogHandler for structured logging.
// The handler automatically injects requestId from Lambda context into each log record.
func ExampleLogHandler() {
// Set up the Lambda-aware slog handler
slog.SetDefault(slog.New(lambdacontext.LogHandler()))
Comment thread
anzheyazzz marked this conversation as resolved.
Outdated

lambda.Start(func(ctx context.Context) (string, error) {
// Use slog.InfoContext to include Lambda context in logs
slog.InfoContext(ctx, "processing request", "action", "example")
return "success", nil
})
}

// ExampleLogHandler_withFields demonstrates LogHandler with additional fields.
// Use WithFields with FieldFunctionARN() and FieldTenantID() to include extra context.
func ExampleLogHandler_withFields() {
// Set up handler with function ARN and tenant ID fields
slog.SetDefault(slog.New(lambdacontext.LogHandler(
lambdacontext.WithFields(lambdacontext.FieldFunctionARN(), lambdacontext.FieldTenantID()),
)))
Comment thread
anzheyazzz marked this conversation as resolved.
Outdated

lambda.Start(func(ctx context.Context) (string, error) {
slog.InfoContext(ctx, "multi-tenant request", "tenant", "acme-corp")
return "success", nil
})
}

// ExampleWithFields demonstrates using WithFields to include specific Lambda context fields.
func ExampleWithFields() {
// Include only function ARN
handler := lambdacontext.LogHandler(
lambdacontext.WithFields(lambdacontext.FieldFunctionARN()),
)
slog.SetDefault(slog.New(handler))

lambda.Start(func(ctx context.Context) (string, error) {
// Log output will include "functionArn" field
slog.InfoContext(ctx, "function invoked")
return "success", nil
})
}
148 changes: 148 additions & 0 deletions lambdacontext/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//go:build go1.21
// +build go1.21

// Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.

package lambdacontext

import (
"context"
"log/slog"
"os"
)

// Field represents a Lambda context field to include in log records.
type Field struct {
key string
value func(*LambdaContext) string
}

// FieldFunctionARN returns a Field that includes the invoked function ARN in log records.
func FieldFunctionARN() Field {
return Field{"functionArn", func(lc *LambdaContext) string { return lc.InvokedFunctionArn }}
}

// FieldTenantID returns a Field that includes the tenant ID in log records (for multi-tenant functions).
func FieldTenantID() Field {
return Field{"tenantId", func(lc *LambdaContext) string { return lc.TenantID }}
}

// logOptions holds configuration for the Lambda log handler.
type logOptions struct {
fields []Field
}

// LogOption is a functional option for configuring the Lambda log handler.
type LogOption func(*logOptions)

// WithFields includes the specified fields in log records.
func WithFields(fields ...Field) LogOption {
return func(o *logOptions) {
o.fields = append(o.fields, fields...)
}
}

// LogHandler returns a [slog.Handler] for AWS Lambda structured logging.
// It reads AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL from environment,
// and injects requestId from Lambda context into each log record.
//
// By default, only requestId is injected. Use WithFields to include more.
// See the package examples for usage.
func LogHandler(opts ...LogOption) slog.Handler {
Comment thread
anzheyazzz marked this conversation as resolved.
Outdated
options := &logOptions{}
for _, opt := range opts {
opt(options)
}

level := parseLogLevel()
handlerOpts := &slog.HandlerOptions{
Level: level,
ReplaceAttr: ReplaceAttr,
}

var h slog.Handler
if LogFormat == "JSON" {
h = slog.NewJSONHandler(os.Stdout, handlerOpts)
} else {
h = slog.NewTextHandler(os.Stdout, handlerOpts)
}

return &lambdaHandler{handler: h, fields: options.fields}
}

// ReplaceAttr maps slog's default keys to AWS Lambda's log format (time->timestamp, msg->message).
func ReplaceAttr(groups []string, attr slog.Attr) slog.Attr {
if len(groups) > 0 {
return attr
}

switch attr.Key {
case slog.TimeKey:
attr.Key = "timestamp"
case slog.MessageKey:
attr.Key = "message"
}
return attr
}

// Attrs returns Lambda context fields as slog-compatible key-value pairs.
// For most use cases, using [LogHandler] with slog.InfoContext is preferred.
func (lc *LambdaContext) Attrs() []any {
return []any{"requestId", lc.AwsRequestID}
}
Comment thread
anzheyazzz marked this conversation as resolved.
Outdated

// lambdaHandler wraps a slog.Handler to inject Lambda context fields.
type lambdaHandler struct {
handler slog.Handler
fields []Field
}

// Enabled implements slog.Handler.
func (h *lambdaHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.handler.Enabled(ctx, level)
}

// Handle implements slog.Handler.
func (h *lambdaHandler) Handle(ctx context.Context, r slog.Record) error {
Comment thread
anzheyazzz marked this conversation as resolved.
if lc, ok := FromContext(ctx); ok {
r.AddAttrs(slog.String("requestId", lc.AwsRequestID))
Comment thread
anzheyazzz marked this conversation as resolved.

for _, field := range h.fields {
if v := field.value(lc); v != "" {
r.AddAttrs(slog.String(field.key, v))
}
}
}
return h.handler.Handle(ctx, r)
}

// WithAttrs implements slog.Handler.
func (h *lambdaHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &lambdaHandler{
handler: h.handler.WithAttrs(attrs),
fields: h.fields,
}
}

// WithGroup implements slog.Handler.
func (h *lambdaHandler) WithGroup(name string) slog.Handler {
return &lambdaHandler{
handler: h.handler.WithGroup(name),
fields: h.fields,
}
}

func parseLogLevel() slog.Level {
switch LogLevel {
case "DEBUG":
return slog.LevelDebug
case "INFO":
return slog.LevelInfo
case "WARN":
return slog.LevelWarn
case "ERROR":
return slog.LevelError
default:
return slog.LevelInfo
}
}
Loading
Loading