We have a situation where we call from one of our front end lambda into another one which does some work for us. This work doesn't need to be waited for, and as it can take some time (0.5 - 10s), we'd rather the user didn't have to wait for it either.

All well and good, this is pretty easy to do with 2 lambda, just call them with the InvocationType of Event

const params: Lambda.InvocationRequest = {
  FunctionName: config.lambdaSmsProviderFunctionName,
  InvocationType: 'Event',
  Payload: JSON.stringify({
    message,
    phoneNumber,
  }),
};

const result = await o11y.withEvent('sms.lambda', {phoneNumber, message}, async span => {
  return await lambda.invoke(params).promise();
});

The problem is, we end up with 2 distinct traces in Honeycomb. This is also "fine", but I'd rather have them as one.

The main lambda
The SMS sending lambda

Hooking them up, it turns out, is fairly easy. In the calling lambda, you extract the traceId and patentSpanId which are in the trace context:

const context = beeline.marshalTraceContext(o11y.bee.getTraceContext());

const params: Lambda.InvocationRequest = {
  FunctionName: config.lambdaSmsProviderFunctionName,
  InvocationType: 'Event',
  Payload: JSON.stringify({
    message,
    phoneNumber,
    honeycombContext: context,
  }),
};

I'm putting it into the Payload because lambda, for some reason, doesn't pass over the ClientContext when you use the Event invocation type.

On the other end, you pull it out, and when starting your initial trace, pass it in as the initial ids:

const {context, event} = handler;

if (event.honeycombContext) {
  const {traceId, parentSpanId} = beeline.unmarshalTraceContext(event.honeycombContext);
  span = beeline.startTrace(
    {name: 'lambda', awsRequestId: context.awsRequestId, resource: event.resource},
    traceId,
    parentSpanId,
  );
} else {
  span = beeline.startTrace({name: 'lambda', awsRequestId: context.awsRequestId, resource: event.resource});
}

And... we have embedded traces:

Nice.