Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
88 changes: 58 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,71 @@
# Powerslog

A slog handler that captures key fields from an AWS Lambda context and produces
structured logs with the same fields as the Powertools loggers for Python and
TypeScript.
[![CI](https://github.com/unfunco/powerslog/actions/workflows/ci.yaml/badge.svg)](https://github.com/unfunco/powerslog/actions/workflows/ci.yaml)
[![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT)

A [slog] handler that enriches structured logs with key fields captured from an
AWS Lambda context, to produce logs equivalent to those produced by the
AWS Lambda Powertools packages for [Python], [TypeScript], and [.NET].

## Getting started

### Requirements

- [AWS Command Line Interface] 2+
- [Go] 1.22+
- [SAM Command Line Interface] 1.115+

### Installation and usage

Powerslog is compatible with modern Go releases in module mode.
With Go installed, the following command will resolve and add the package to the
current development module, along with its dependencies.

```bash
go get github.com/unfunco/powerslog
```

Alternatively, the same can be achieved using import in a package and running
the `go get` command without arguments.

```go
package main

import (
"context"
"log/slog"
"net/http"
"os"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/unfunco/powerslog"
)

func handler(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
logger := ctx.Value("logger").(*slog.Logger)
logger.Info("Request received", slog.Any("event", event))

return events.APIGatewayProxyResponse{
StatusCode: http.StatusNoContent,
}, nil
}
import "github.com/unfunco/powerslog"
```

func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
powerslogHandler := powerslog.NewHandler(jsonHandler)
logger := slog.New(powerslogHandler)
```go
powerslogHandler := powerslog.NewHandler(os.Stdout, nil)
logger := slog.New(powerslogHandler)
```

ctx := context.WithValue(context.Background(), "logger", logger)
lambda.StartWithOptions(handler, lambda.WithContext(ctx))
```go
ctx := context.WithValue(context.Background(), "logger", logger)
lambda.StartWithOptions(handler, lambda.WithContext(ctx))
```

```go
logger := ctx.Value("logger").(*slog.Logger)
logger.Info("Hello, world!")
```

```json
{
"level": "INFO",
"timestamp": "2024-04-20T16:20:56.666902747Z",
"message": "Hello, world!",
"function_name": "powerslog-test-TestFunction-yjKUabwI3fNE",
"function_memory_size": 128
}
```

#### AWS Lambda Advanced Logging Controls

With [AWS Lambda Advanced Logging Controls], the output format can be set to
either TEXT or JSON and the minimum accepted log level can be specified.
Regardless of the output format setting in Lambda, log messages will always be
emitted as JSON.

#### Environment variables

### Development and testing

```bash
Expand All @@ -58,6 +79,13 @@ sam deploy --guided
© 2024 [Daniel Morris]\
Made available under the terms of the [MIT License].

[.net]: https://docs.powertools.aws.dev/lambda/dotnet/
[aws command line interface]: https://aws.amazon.com/cli/
[aws lambda advanced logging controls]: https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced
[daniel morris]: https://unfun.co
[go]: https://go.dev
[mit license]: LICENSE.md
[python]: https://docs.powertools.aws.dev/lambda/python/latest/
[sam command line interface]: https://aws.amazon.com/serverless/sam/
[slog]: https://pkg.go.dev/log/slog
[typescript]: https://docs.powertools.aws.dev/lambda/typescript/latest/
52 changes: 6 additions & 46 deletions examples/powerslog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,13 @@ import (
"github.com/unfunco/powerslog"
)

func Example_withTextHandler() {
_ = os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "example-text-logging-function")
_ = os.Setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128")
_ = os.Setenv("POWERTOOLS_SERVICE_NAME", "example-text-logging-service")

// Create a new TextHandler that writes to stdout but does not include the
// time, this makes it easier to test the output.
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: removeTimeAttr(),
})

powerslogHandler := powerslog.NewHandler(textHandler)
logger := slog.New(powerslogHandler)

logger.Debug("This will not appear since the default level is INFO")
logger.Info("This is informational!")
logger.Warn("This is a warning!")
logger.Error("This is an error!")

// Output:
// level=INFO msg="This is informational!" service=example-text-logging-service function_name=example-text-logging-function function_memory_size=128
// level=WARN msg="This is a warning!" service=example-text-logging-service function_name=example-text-logging-function function_memory_size=128
// level=ERROR msg="This is an error!" service=example-text-logging-service function_name=example-text-logging-function function_memory_size=128
}

func Example_withJSONHandler() {
_ = os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "example-json-logging-function")
_ = os.Setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "256")
_ = os.Setenv("POWERTOOLS_LOG_LEVEL", "DEBUG")
_ = os.Setenv("POWERTOOLS_SERVICE_NAME", "example-json-logging-service")

// Create a new JSONHandler that writes to stdout but does not include the
// time, this makes it easier to test the output.
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
ReplaceAttr: removeTimeAttr(),
})

powerslogHandler := powerslog.NewHandler(jsonHandler)
powerslogHandler := powerslog.NewHandler(os.Stdout, nil)
logger := slog.New(powerslogHandler)

logger.Debug("This is a debug message!")
Expand All @@ -53,17 +22,8 @@ func Example_withJSONHandler() {
logger.Error("This is an error message!")

// Output:
// {"level":"DEBUG","msg":"This is a debug message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
// {"level":"INFO","msg":"This is an informational message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
// {"level":"WARN","msg":"This is a warning message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
// {"level":"ERROR","msg":"This is an error message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
}

func removeTimeAttr() func(groups []string, a slog.Attr) slog.Attr {
return func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 {
return slog.Attr{}
}
return a
}
// {"level":"DEBUG","message":"This is a debug message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
// {"level":"INFO","message":"This is an informational message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
// {"level":"WARN","message":"This is a warning message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
// {"level":"ERROR","message":"This is an error message!","service":"example-json-logging-service","function_name":"example-json-logging-function","function_memory_size":256}
}
63 changes: 63 additions & 0 deletions internal/buffer/buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package buffer provides a pool-allocated byte buffer.
package buffer

import "sync"

// buffer adapted from go/src/fmt/print.go
type Buffer []byte

// Having an initial size gives a dramatic speedup.
var bufPool = sync.Pool{
New: func() any {
b := make([]byte, 0, 1024)
return (*Buffer)(&b)
},
}

func New() *Buffer {
return bufPool.Get().(*Buffer)
}

func (b *Buffer) Free() {
// To reduce peak allocation, return only smaller buffers to the pool.
const maxBufferSize = 16 << 10
if cap(*b) <= maxBufferSize {
*b = (*b)[:0]
bufPool.Put(b)
}
}

func (b *Buffer) Reset() {
b.SetLen(0)
}

func (b *Buffer) Write(p []byte) (int, error) {
*b = append(*b, p...)
return len(p), nil
}

func (b *Buffer) WriteString(s string) (int, error) {
*b = append(*b, s...)
return len(s), nil
}

func (b *Buffer) WriteByte(c byte) error {
*b = append(*b, c)
return nil
}

func (b *Buffer) String() string {
return string(*b)
}

func (b *Buffer) Len() int {
return len(*b)
}

func (b *Buffer) SetLen(n int) {
*b = (*b)[:n]
}
21 changes: 21 additions & 0 deletions internal/buffer/buffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package buffer

import "testing"

func Test(t *testing.T) {
b := New()
defer b.Free()
_, _ = b.WriteString("hello")
_ = b.WriteByte(',')
_, _ = b.Write([]byte(" world"))

got := b.String()
want := "hello, world"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
2 changes: 2 additions & 0 deletions lambda/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ require (
github.com/aws/aws-lambda-go v1.47.0
github.com/unfunco/powerslog v0.1.0
)

replace github.com/unfunco/powerslog => ../
3 changes: 1 addition & 2 deletions lambda/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ func handler(ctx context.Context, event events.APIGatewayProxyRequest) (events.A
}

func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
powerslogHandler := powerslog.NewHandler(jsonHandler)
powerslogHandler := powerslog.NewHandler(os.Stdout, &powerslog.HandlerOptions{})
logger := slog.New(powerslogHandler)

ctx := context.WithValue(context.Background(), "logger", logger)
Expand Down
Loading