logger icon indicating copy to clipboard operation
logger copied to clipboard

Request ID logging

Open mnapoli opened this issue 5 years ago • 6 comments

Here are some notes about logging with the AWS request ID.

The goal

Be able to see all logs of a single invocation.

How? By filtering using the "request ID".

How? By storing that request ID in each log record.

Is it not there already? It's there when logging from a JS lambda for example.

When logging from PHP (by writing to stdout/stderr), only the START, END and REPORT log lines contain it (the lines written by Lambda itself).

NodeJS runtime

exports.handler = async () => {
    console.log('Hello from Lambda!');
};

This is what we see in CloudWatch:

Capture d’écran 2020-03-13 à 22 38 05

This is the same thing in Insights:

Capture d’écran 2020-03-13 à 22 39 49

Analysis: the request ID is added in a non-structured text line. Values are separated by tabs (\t). For example:

2020-03-13T21:21:56.341Z 0879de55-0817-4d5c-9f37-7ed2755a72e1 INFO Hello from Lambda! 

Current Bref PHP runtime

echo 'Hello world';

In CloudWatch:

Capture d’écran 2020-03-13 à 22 45 48

In Insights:

Capture d’écran 2020-03-13 à 22 47 35

Analysis: the @requestId field (or the request ID value itself) does not appear in that record.

Experiment 1

echo json_encode([
        'message' => 'Hello world!',
        'requestId' => $context->getAwsRequestId(),
        '@requestId' => $context->getAwsRequestId(),
]) . PHP_EOL;

In CloudWatch:

Capture d’écran 2020-03-13 à 22 40 59

In Insights:

Capture d’écran 2020-03-13 à 22 42 14

Analysis: it looked promising at first, but it turns out the @requestId field is escaped to @@requestId by CloudWatch (or Lambda?).

That means that we still have no @requestId field. For example, this query does not return the lines we logged in PHP:

fields @message
| filter @requestId = '6dc9b447-3898-4793-83cf-60730058c833'

Experiment 2

Based on the tabulated format from the JS runtime, could it be that CloudWatch interprets data that way?

echo "2020-03-13T21:21:56.341Z\t{$context->getAwsRequestId()}\tINFO\tHello world" . PHP_EOL;

CloudWatch:

Capture d’écran 2020-03-13 à 22 50 17

Insights:

Capture d’écran 2020-03-13 à 22 52 37

Analysis: OMG it works!!!

The field is correctly recognized now:

Capture d’écran 2020-03-13 à 22 54 54

Even serverless logs parsed the fields:

Capture d’écran 2020-03-13 à 22 56 35

mnapoli avatar Mar 13 '20 21:03 mnapoli

Experiment 3

echo $context->getAwsRequestId() . "\tHello world\n";

CloudWatch:

Capture d’écran 2020-03-13 à 22 58 19

Insights:

Capture d’écran 2020-03-13 à 22 57 57

Analysis: worked too! The log message is simpler, easier to read as well. However it's not colored in the terminal by serverless logs:

Capture d’écran 2020-03-13 à 22 58 52

The serverless logs color come from here: https://github.com/serverless/serverless/blob/f822ae7e4c4062178b9f6843104fb33f0a586ef5/lib/plugins/aws/utils/formatLambdaLogEvent.js#L21-L43

mnapoli avatar Mar 13 '20 22:03 mnapoli

Experiment 4

Same as experiment 3, but in the case where we want to log structured data with JSON:

echo "{$context->getAwsRequestId()}\t" . json_encode([
    'message' => 'This is a message',
]) . PHP_EOL;

(yes we mix string and JSON content, CloudWatch supports it on purpose)

Capture d’écran 2020-03-13 à 23 09 45 Capture d’écran 2020-03-13 à 23 08 51

Seems to work perfectly as well!

Just tested on NodeJS as well to confirm, they do the same thing:

2020-03-13T22:12:01.928Z	206e2f13-4cc3-415b-8ed2-a5b108111680	INFO	{ message: 'Hello from Lambda!' }

mnapoli avatar Mar 13 '20 22:03 mnapoli

OK now the difficulty is: the request ID is scoped at the invocation level.

The logger is scope at the application level.

How do we combine this properly 🤔

mnapoli avatar Mar 14 '20 08:03 mnapoli

For reference, here is JS code that is responsible for formatting console.log() in the NodeJS runtime:

function patchLogging(invokeId) {
    console.log = console.error = console.warn = console.info = function prettyConsoleLog() {
        var dateString = new Date().toISOString();
        var message = `${dateString}\t${invokeId}\t${util.format.apply(this, arguments)}\n`;
        awslambda.sendConsoleLogs(message, Buffer.byteLength(message));
    };
};

Digging further:

sendConsoleLogs: consoleLog,
function formatConsole(str) {
  return str.replace(/^[0-9TZ:.-]+\t[0-9a-f-]+\t/, '\u001b[34m$&\u001b[0m')
}

function consoleLog(str) {
  process.stderr.write(formatConsole(str))
}

function systemLog(str) {
  process.stderr.write(formatSystem(str) + '\n')
}

function systemErr(str) {
  process.stderr.write(formatErr(str) + '\n')
}

function handleResult(resultStr, cb) {
  if (!process.stdout.write('\n' + resultStr + '\n')) {
    process.stdout.once('drain', cb)
  } else {
    process.nextTick(cb)
  }
}

So it is reassuring: the NodeJS runtime indeed logs via stderr, just like we do in PHP.

For a long time I thought they used a secret technique for logging with the @requestId variable, but it seems to be AWS CloudWatch that has a secret behavior of parsing the \t separated values. Hooray 🎉

mnapoli avatar Mar 14 '20 11:03 mnapoli

Before dealing with all the log lines, we can start applying all that to exceptions/errors: see https://github.com/brefphp/bref/pull/590

mnapoli avatar Mar 14 '20 11:03 mnapoli

@mnapoli I love how thorough you are in your notes! And that you make them public.

I just started using Github as a place to document my personal journey through issues in my private repos (as you know). At first I thought, "Oh man Christian, this is too much. You're wasting time."

But I know the the future me will really love this when I inevitably forget how to configure a serverless setting 2+ years from now.

I guess what I'm saying is thank you for:

  1. Affirming, indirectly, that I'm not crazy for doing the same and
  2. For writing this up.

Watching someone else solve problems in "real time" helps me learn to be a better problem solver.

Excellent work as always man! 💯

ctessmer avatar May 10 '23 13:05 ctessmer