We use cookies or similar technologies to personalize your online experience and tailor marketing to you. Many of our product features require cookies to function properly. Your use of this site and online product constitutes your consent to these personalization technologies. Read our Privacy Policy to find out more.

X

Instrumenting AWS Lambda Functions with Honeycomb

For serverless apps based on Lambda and similar platforms, observability can be challenging. With no server to log in to, clunky logging interfaces, and short, event-driven process runtimes, making sense of what’s going on is often difficult. Fortunately, with libraries for NodeJS, Python and Go, it’s easy to instrument your Lambda functions and gain quick insight into your serverless apps.

Using libhoney inside your Lambda functions is similar to using it in other applications, but with a few important caveats documented below.

Alternatively, you can emit structured logs to Cloudwatch, and use our Cloudwatch Logs Integration to scoop up your structured logs into a Honeycomb dataset.

Go

In Go-based Lambda implementations, initialization is done in your main function. This is where you’ll want to initialize libhoney. Libhoney initialization is somewhat heavy - doing so on every function invocation (inside your handler) would add significant overhead. Since Lambda freezes and re-uses function instances after main is called, calling Init inside your main function will improve performance.

import (
  // ...
  libhoney "github.com/honeycombio/libhoney-go"
)

func main() {
  libhoney.Init(libhoney.Config{
		WriteKey:   "<write key>",
		Dataset:    "my dataset",
  })

  // ...

  lambda.Start(Handler)
}

Inside your Lambda handler, you can use libhoney normally to send events. Note the call to Flush, explained below.

func Handler(ctx context.Context) error {
  // Ensure events are sent before returning
  defer libhoney.Flush()

  // Measure execution time
  startTime := time.Now()

  // ...

  // Get the Lambda context object
  lc, _ := lambdacontext.FromContext(ctx)

  ev := libhoney.NewEvent()
  ev.Add(map[string]interface{}{
    "message": "Hello World from Lambda!",
    "function_name": lambdacontext.FunctionName,
    "function_version": lambdacontext.FunctionVersion,
    "request_id": lc.AwsRequestID,
    "duration_ms": time.Since(startTime)
    // other fields of interest
  })
  ev.Send()
}

Normally, libhoney events are enqueued and sent as batches. By default, this occurs every 100ms or whenever the queue is full. However, because Lambda freezes the function instance between invocations, the goroutine responsible for sending this batch is not guaranteed to execute. To ensure that events are sent, call Flush before your function returns. Alternatively, if you can live with some events going missing, you can leave this out.

Python

You should initialize a libhoney Client object outside of your handler function. Client initialization is relatively heavy, and since Lambda freezes and re-uses function instances, doing heavy initialization outside of the handler will significantly improve performance on subsequent invocations.

import libhoney
client = libhoney.Client(
    writekey="<write key>",
    dataset="my dataset",
)

def handler(event, context):
    # measure execution time
    start_time = datetime.datetime.now()

    # ...

    ev = client.new_event()
    ev.add({
        "message": "Hello World from Lambda!",
        "function_name": context.function_name,
        "function_version": context.function_version,
        "request_id": context.aws_request_id,
        "duration_ms": (datetime.datetime.now() - start_time).total_seconds() * 1000,
        # other fields of interest
    })
    ev.send()

    # ...

    # ensure events are sent before the handler returns
    client.flush()

Note the call to flush. In libhoney, events are queued and then sent in batches for efficiency. This is problematic in Lambda, as the function instance is frozen between handler invocations, which can prevent some batches from being sent. To ensure events are always sent after each invocation, you can flush all currently enqueued events before returning.

Javascript

In the .js file for your handler, initialize the libhoney library. You’ll want to do this outside your handler function. Lambda will freeze the function after it is called, and may re-use the same function instance multiple times. By initializing libhoney outside your handler, you can avoid doing initialization work on every invocation.

const Libhoney = require('libhoney').default;

let hny = new Libhoney({
  writeKey: "<your Honeycomb API Key>",
  dataset: "my-dataset",
});

Inside your handler, instrument your code as usual.

module.exports.handler = (event, context, callback) => {
  // Measure execution time
  let startTime = Date.now();

  // ...

  let ev = hny.newEvent();
  ev.add({
      message: "Hello World from Lambda!",
      // Some useful fields from the Lambda context object
      functionName: context.functionName,
      functionVersion: context.functionVersion,
      requestId: context.awsRequestId,
      latencyMs: Date.now() - startTime,
      // Example fields - send anything that seems relevant!
      userId: event.UserId,
      userAction: event.UserAction,
      // ...
    });
    ev.send();
  }
}

libhoney will queue up events to be sent, so that it can efficiently batch them. Sending events will not block your function, but event batches will be added as callbacks on the Node event loop. When your function returns, the remaining callbacks on the event loop will be executed before results are returned to the client.

If this is undesirable, you can toggle the callbackWaitsForEmptyEventLoop option in the Lambda Context object.

module.exports.handler = (event, context, callback) => {
  context.callbackWaitsForEmptyEventLoop = false;
  /// ...
}

Doing this can result in some events being lost, as libhoney does not get a chance to execute its batches.

Cloudwatch Logs Integration

You might prefer to combine structured logging with our integration for Cloudwatch Logs.

Some reasons you might want to do this:

How it works

Logging in Lambda is non-blocking, and log messages are forwarded to Cloudwatch Logs. Our Cloudwatch Logs integration listens to your Cloudwatch Log Groups, parses them, and sends events to Honeycomb. As long as the log messages are in JSON format, they can be easily ingested into Honeycomb.

Installation

See the install steps in our Cloudwatch Logs Integration docs.

Examples

Below are some structured logging examples using some libraries we are familiar with at Honeycomb. Feel free to use your own!

Go

import (
  log "github.com/sirupsen/logrus"
)

log.SetFormatter(&log.JSONFormatter{})

func Handler(ctx context.Context) error {
  // Measure execution time
  startTime := time.Now()

  // ...

  // Get the Lambda context object
  lc, _ := lambdacontext.FromContext(ctx)

  log.WithFields(log.Fields{
      "function_name": lambdacontext.FunctionName,
      "function_version": lambdacontext.FunctionVersion,
      "request_id": lc.AwsRequestID,
      "duration_ms": time.Since(startTime),
      // other fields of interest
  }).Info("Hello World from Lambda!")
}

Python

import structlog
structlog.configure(processors=[structlog.processors.JSONRenderer()])
log = structlog.get_logger()

def handler(event, context):
    # measure execution time
    start_time = datetime.datetime.now()

    # ...

    log.msg(
        "Hello World from Lambda!",
        function_name=context.function_name,
        function_version=context.function_version,
        request_id=context.aws_request_id,
        duration_ms=(datetime.datetime.now() - start_time).total_seconds() * 1000,
        # other fields of interest
    )

JavaScript

var bunyan = require('bunyan');
var log = bunyan.createLogger();


module.exports.handler = (event, context, callback) => {
  // Measure execution time
  let startTime = Date.now();

  // ...

  log.info({
    functionName: context.functionName,
    functionVersion: context.functionVersion,
    requestId: context.awsRequestId,
    // Example fields - send anything that seems relevant!
    userId: event.UserId,
    userAction: event.UserAction,
    latencyMs: Date.now() - startTime,
  },
  'Hello World from Lambda!');
}

Note: Do not use console.log to write structured log lines in Lambda. Lambda uses a patched version of console.log, injecting extra information with each line that does not work correctly with Honeycomb Cloudwatch Logs integration.