Skip to content

Logger

Write JSON logs with searchable fields, service names and Lambda request context.

Install

Use Go 1.27 or newer:

CGO_ENABLED=0 go get github.com/rambow-cloud/powertools-lambda-go/logger@v1.2.0

Write your first log

This program writes an order ID and amount as JSON fields. The timestamp in the output is an example; your run uses the current time.

Input

{
  "order_id": "ORD-123",
  "amount": 42
}

Code

package main

import (
    "github.com/rambow-cloud/powertools-lambda-go/logger"
)

func main() {
    appLog := logger.New(logger.WithServiceName("orders"))

    if err := appLog.Info("Processing order", logger.Fields{
        "order_id": "ORD-123",
        "amount":   42,
    }); err != nil {
        panic(err)
    }
}

Output

{
  "level": "INFO",
  "message": "Processing order",
  "timestamp": "2026-10-01T00:00:00.000Z",
  "service": "orders",
  "sampling_rate": 0,
  "amount": 42,
  "order_id": "ORD-123"
}

Add fields to later messages

Use AppendKeys for fields shared by several log calls. Per-call fields apply only to that message.

appLog.AppendKeys(logger.Fields{"order_id": "ORD-123"})
if err := appLog.Info("Order accepted", logger.Fields{"items": 3}); err != nil {
    panic(err)
}

The next record contains "order_id":"ORD-123" and "items":3. Use RemoveKeys("order_id") to remove a temporary field.

Log an error

Pass the business error after the message; handle a returned logging error separately. These standalone snippets panic if Logger itself cannot serialize or write. In a handler, return that logging error to the caller rather than trying to report it through the same failed logger.

paymentErr := errors.New("card declined")
if err := appLog.Error("Payment failed", paymentErr); err != nil {
    panic(err)
}

The record has level: "ERROR", message: "Payment failed" and error.message: "card declined". It is written to stderr. Logging it does not return the business error from your handler.

This example targets stable v1.2.0, which does not automatically collect error stacks or log handler panics. The following Preview has different defaults.

Error stacks and panic logging (Preview)

This Preview is available only from main and is not included in v1.2.0.

Install it explicitly in your application:

CGO_ENABLED=0 go get github.com/rambow-cloud/powertools-lambda-go/logger@main

Omitting a version does not select main. Go resolves a branch to a pseudo-version; use @<commit-SHA> instead of @main to pin a preview. See Go version queries. If a module proxy still resolves main to an older commit, select the intended main commit SHA explicitly. The stable installation above and its examples continue to target v1.2.0.

Attach the error's source

Use WithStack where an error is created or first observed. Ordinary errors do not need a panic to carry a stack.

func loadOrder() error {
    return logger.WithStack(errors.New("order not found"))
}

// appLog := logger.New(logger.WithServiceName("orders"))
if logErr := appLog.Error("Cannot load order", loadOrder()); logErr != nil {
    return logErr
}

Output (error excerpt; example path and line numbers):

{"error":{"name":"*errors.errorString","message":"order not found","location":"/app/main.go:11","stack":"main.loadOrder\n\t/app/main.go:11\nmain.main\n\t/app/main.go:16","stack_source":"attached"}}

The internal wrapper adds no error name or extra cause layer. %w, errors.Is and errors.As keep working. Repeated wrapping retains the existing stack along a single error chain; joined branches keep their own origins.

Get a stack at the log call

With the Preview defaults, an error without an attached stack gets a fallback:

paymentErr := errors.New("card declined")
if logErr := appLog.Error("Payment failed", paymentErr); logErr != nil {
    return logErr
}

Output (error excerpt):

{"error":{"name":"*errors.errorString","message":"card declined","location":"/app/main.go:19","stack":"main.main\n\t/app/main.go:19","stack_source":"logging"}}

This identifies where the error was logged, which may differ from where it occurred. location is the first application frame in the selected stack. Stacks contain at most 64 frames; a longer stack adds stack_truncated: true. Filtered records and records without errors do not collect a stack.

Record an unhandled handler panic

The outermost Logger wrapper logs one ERROR before cleaning up the request scope, then rethrows the original panic value. Typed and raw handlers both support it.

handler := func(ctx context.Context, event map[string]any) (string, error) {
    appLog.WithContext(ctx).SetCorrelationID(event["order_id"])
    panic("unexpected payment state")
}
lambda.Start(logger.WrapHandler(appLog, handler))

Input: {"order_id":"ORD-123"}

Output (Logger record excerpt):

{"level":"ERROR","message":"Unhandled panic","service":"orders","function_request_id":"request-123","correlation_id":"ORD-123","error":{"name":"string","message":"unexpected payment state","location":"/app/main.go:30","stack":"main.main.func1\n\t/app/main.go:30\n...","stack_source":"panic"}}

The ... above abbreviates caller frames. Nested Logger wrappers do not repeat this record. The invocation still fails; the Lambda SDK can emit a separate error record. Ordinary returned handler errors are logged explicitly by your application; the existing FlushBufferOnError opt-in still applies.

Compact JSON escapes stack newlines, so the Logger record occupies one physical line. Development pretty printing, custom formatters and SDK-generated records are outside that guarantee. Formatter/replacer hooks still apply. Initialization panics, other goroutines and runtime fatal errors are outside the wrapper's scope.

Turn the Preview behavior off

appLog := logger.New(
    logger.WithStackTrace(false),
    logger.WithPanicLogging(false),
)

Both options default to true on main and can be overridden on child loggers. WithStackTrace(false) suppresses attached, fallback and panic stack fields; WithPanicLogging(false) suppresses the new automatic panic record. Disable both to retain the previous logging behavior. These options do not exist in v1.2.0.

Run the complete Preview example for source and logging-site stacks; see the detailed contract for joins, buffering and diagnostic failures.

Add Lambda request context

Create the logger once. Use the bound logger inside a wrapped handler:

handler := func(ctx context.Context, event map[string]any) (map[string]any, error) {
    requestLog := appLog.WithContext(ctx)
    if err := requestLog.Info("Order received", logger.Fields{"order_id": event["order_id"]}); err != nil {
        return nil, err
    }
    return map[string]any{"status": "accepted"}, nil
}
lambda.Start(logger.WrapHandler(appLog, handler))

Logs gain fields such as function_request_id and cold_start. An input of {"order_id":"ORD-123"} returns {"status":"accepted"}. See the complete Lambda for imports and deployment.

Choose a log level

Setting Example Effect
Service logger.WithServiceName("orders") Adds service to every record
Level logger.WithLevel(logger.DebugLevel) Includes debug records
Environment POWERTOOLS_LOG_LEVEL=INFO Sets the default threshold
Event logging POWERTOOLS_LOGGER_LOG_EVENT=true Opts into logging incoming events
Sampling logger.WithSampleRate(0.1) Enables debug logs for a sample of invocations

Log the incoming event

For a typed handler, use WrapRawHandler when the log should retain fields that are not declared in your event struct:

type Greeting struct { Name string `json:"name"` }
greet := func(ctx context.Context, event Greeting) (string, error) {
    return "Hello, " + event.Name, nil
}
enabled := true
lambda.Start(logger.WrapRawHandler(appLog, greet, logger.HandlerOptions{LogEvent: &enabled}))

With input {"name":"Ada","debug":true}, the extra log record includes:

{
  "message": "Lambda invocation event",
  "event": {"name": "Ada", "debug": true}
}

This is a log excerpt; the typed handler still receives only its declared fields. Event logging is off by default because it can include sensitive payload data.

Things to know

  • Use named fields rather than printf placeholders: Info("Order %s", id) does not format the message.
  • Use WrapRawHandler when event logs must retain JSON fields absent from your typed event. Event logging can expose payload data; it is off by default.
  • Request fields belong to the invocation. Join background work before returning; do not retain a bound logger for later requests.
  • Advanced Lambda Logging Controls can override the configured threshold.

Next steps

See the API and behavior notes for advanced options, and module installation for dependencies.