Skip to content

hotsock/voker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Voker

A minimal, modern AWS Lambda runtime for Go that focuses on performance, simplicity, and type safety.

Overview

Voker is a simplified alternative to aws-lambda-go that maintains full compatibility with the AWS Lambda Runtime API. It uses Go generics to provide compile-time type safety with a clean, single-function-signature design. It supports structured logging with slog and proper log levels for errors, including an optional vokerslog handler tuned for AWS Lambda.

Installation

go get github.com/hotsock/voker

Usage

Basic Handler

package main

import (
    "context"
    "github.com/hotsock/voker"
)

type MyEvent struct {
    Name string `json:"name"`
}

type MyResponse struct {
    Message string `json:"message"`
}

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
    return MyResponse{
        Message: "Hello, " + event.Name,
    }, nil
}

func main() {
    voker.Start(handler)
}

Accessing Lambda Context

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
    lc, ok := voker.FromContext(ctx)
    if ok {
        log.Printf("Request ID: %s", lc.AwsRequestID)
        log.Printf("Function ARN: %s", lc.InvokedFunctionArn)
    }

    deadline, _ := ctx.Deadline()
    log.Printf("Function deadline: %s", deadline)

    return MyResponse{Message: "success"}, nil
}

Error Handling

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
    if event.Name == "" {
        return MyResponse{}, fmt.Errorf("name is required")
    }

    return MyResponse{Message: "Hello, " + event.Name}, nil
}

Simple Event Types

// Handle API Gateway events
func handler(ctx context.Context, event map[string]any) (map[string]any, error) {
    return map[string]any{
        "statusCode": 200,
        "body":       "Hello from Lambda!",
    }, nil
}

// Handle SQS events
type SQSEvent struct {
    Records []SQSRecord `json:"Records"`
}

type SQSRecord struct {
    Body string `json:"body"`
}

func handler(ctx context.Context, event SQSEvent) (string, error) {
    for _, record := range event.Records {
        log.Printf("Processing: %s", record.Body)
    }
    return "ok", nil
}

Handler Signature

Voker supports only one handler signature:

func(context.Context, TIn) (TOut, error)

Where:

  • context.Context is required (provides deadline, cancellation, Lambda metadata)
  • TIn is your input type (must be JSON-deserializable)
  • TOut is your output type (must be JSON-serializable)
  • error is required for error handling

Raw payloads

Declare TIn as json.RawMessage to receive the invocation payload verbatim. Voker skips unmarshaling — and JSON validation — and hands the raw bytes straight to your handler, which is then responsible for decoding them:

func handler(ctx context.Context, payload json.RawMessage) (Response, error) {
    // payload is the raw request bytes, aliased (not copied) from the
    // invocation buffer. Decode it yourself however you like.
    var event MyEvent
    if err := json.Unmarshal(payload, &event); err != nil {
        return Response{}, err
    }
    // ...
}

This is useful for handlers that work with large payloads and want to measure or control their own decoding rather than paying for an unmarshal up front. Because validation is skipped, the handler also sees empty or malformed payloads as-is instead of voker rejecting them.

Lambda Context

The LambdaContext type contains metadata about the invocation:

type LambdaContext struct {
    AwsRequestID       string          // Unique request ID
    InvokedFunctionArn string          // ARN of the invoked function
    Identity           CognitoIdentity // Cognito identity (if present)
    ClientContext      ClientContext   // Client context (if present)
}

Access it using voker.FromContext(ctx).

Logging

Voker logs with the standard library's log/slog. By default it creates a logger from AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL using slog's built-in JSON or text handlers. Provide your own with voker.WithLogger.

For ideal Lambda logging behavior, the optional vokerslog subpackage offers a slog.Handler tuned for AWS Lambda advanced logging controls. It is opt-in (import it only when you want it) and adds no extra dependency — the request ID is read from voker.FromContext rather than aws-lambda-go.

import (
    "log/slog"
    "os"

    "github.com/hotsock/voker"
    "github.com/hotsock/voker/vokerslog"
)

func main() {
    logger := slog.New(vokerslog.NewHandler(os.Stdout))
    slog.SetDefault(logger)

    voker.Start(handler, voker.WithLogger(logger))
}

NewHandler auto-configures format (JSON or text) and level from AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL, and enriches every record with Lambda metadata (function name, version, and the request ID from the invocation context). Options override the environment values:

Option Description
WithJSON() Output in JSON format
WithText() Output in text format
WithLevel(slog.Leveler) Set the minimum log level
WithSource() Include source file, function, and line number
WithType(string) Set the type field (default: "app.log")
WithoutTime() Omit the timestamp

In addition to the standard slog levels, the handler maps Lambda's TRACE (slog.LevelDebug - 4) and FATAL (slog.LevelError + 4) levels. A JSON record looks like:

{"level":"INFO","msg":"Lambda Invoked","record":{"functionName":"my-func","version":"$LATEST","requestId":"abc-123"},"type":"app.log"}

The type field

The type + record envelope mirrors the shape of AWS Lambda Telemetry API events, so type works best as a low-cardinality category for filtering and routing logs (e.g. filter type = "app.request" in CloudWatch Logs Insights) rather than for per-request data. AWS reserves function, extension, and platform.*; a dotted app.<category> namespace avoids collisions and matches AWS's style — for example app.log (the default), app.request, or app.audit.

The default comes from WithType (or "app.log"). A single record can override it with a top-level string attribute keyed vokerslog.TypeKey ("type"); the attribute sets the record's type instead of being emitted normally. Set it via With to tag every record from a logger, or per call:

// All records from this logger are tagged "app.request".
requests := slog.New(handler).With(vokerslog.TypeKey, "app.request")

// Or override a single record (this wins over any With value):
slog.InfoContext(ctx, "audit event", vokerslog.TypeKey, "app.audit")

Setting the type to "" omits the field for that record. An attribute keyed type inside a group is left untouched and emitted normally.

Error Handling

Voker automatically handles errors and panics:

Regular Errors

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
    return MyResponse{}, errors.New("something went wrong")
}
// Returns: {"errorMessage":"something went wrong","errorType":"errorString"}

Panics

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
	a := []string{"hey"}
	fmt.Println(a[1]) // panic

    // ...
}

Returns the following (and process exits after panic):

{
  "errorType": "Runtime.Panic.boundsError",
  "errorMessage": "runtime error: index out of range [1] with length 1",
  "stackTrace": [
    {
      "label": "gopanic",
      "line": 783,
      "path": "/usr/local/go/src/runtime/panic.go"
    },
    {
      "label": "goPanicIndex",
      "line": 115,
      "path": "/usr/local/go/src/runtime/panic.go"
    },
    {
      "label": "handler",
      "line": 30,
      "path": "/Users/me/Code/voker/examples/error/main.go"
    },
    {
      "label": "callHandler[...]",
      "line": 198,
      "path": "Code/voker/voker.go"
    },
    {
      "label": "handleInvocation[...]",
      "line": 170,
      "path": "Code/voker/voker.go"
    },
    {
      "label": "Start[...]",
      "line": 120,
      "path": "Code/voker/voker.go"
    },
    {
      "label": "main",
      "line": 21,
      "path": "/Users/me/Code/voker/examples/error/main.go"
    },
    {
      "label": "main",
      "line": 285,
      "path": "/usr/local/go/src/runtime/proc.go"
    },
    {
      "label": "goexit",
      "line": 1268,
      "path": "/usr/local/go/src/runtime/asm_arm64.s"
    }
  ]
}

Testing Your Handler

func TestHandler(t *testing.T) {
    event := MyEvent{Name: "World"}
    response, err := handler(context.Background(), event)

    assert.NoError(t, err)
    assert.Equal(t, "Hello, World", response.Message)
}

No mocking required - your handler is just a function!

Building and Deploying

Build for Lambda

GOOS=linux GOARCH=arm64 go build -o bootstrap main.go
zip function.zip bootstrap

Using with AWS SAM

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures: [arm64]

Using with AWS CDK

lambda.NewFunction(stack, jsii.String("MyFunction"), &lambda.FunctionProps{
    Runtime: lambda.Runtime_PROVIDED_AL2023(),
    Handler: jsii.String("bootstrap"),
    Code:    lambda.Code_FromAsset(jsii.String("./function.zip"), nil),
    Architecture: lambda.Architecture_ARM_64(),
})

Migration from aws-lambda-go

Before (aws-lambda-go)

import "github.com/aws/aws-lambda-go/lambda"

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
    // ...
}

func main() {
    lambda.StartHandlerFunc(handler)
}

After (Voker)

import "github.com/hotsock/voker"

func handler(ctx context.Context, event MyEvent) (MyResponse, error) {
    // ...
}

func main() {
    voker.Start(handler)
}

That's it! If you were using the standard func(context.Context, TIn) (TOut, error) signature, it's a drop-in replacement.

If you were using lambdacontext.LambdaContext (most likely lambdacontext.FromContext(ctx) in your code, switch those to voker.FromContext(ctx)).

License

See LICENSE.

About

AWS Lambda runtime for Go applications

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages