Skip to content

Feature request: Event Handler Request Logging #4602

@perpil

Description

@perpil

Use case

I'm attempting to emit a request log (also known as canonical log) per request. Using global middleware I've got something working but I have a few bits of feedback.

Per request, I'd like to be able to emit key metadata like source ip, user-agent, requestId, http method, route, path, status code, error message, error type, error stack and useful information I pick out of the request.

I've hacked something together that kind of works, but it's not quite 100%. The code below has 3 routes, /ping returns a 200. /error throws a 400 BadRequestError and /fault throws an uncaught Error resulting in a 500 Internal Server Error.

import { BadRequestError, Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
import { Metrics, MetricUnit } from '@aws-lambda-powertools/metrics';
import { Logger } from '@aws-lambda-powertools/logger';
import {
  correlationPaths,
  search,
} from '@aws-lambda-powertools/logger/correlationId';
import type { Context } from 'aws-lambda';

let startTime: number;

const logger = new Logger({
  correlationIdSearchFn: search,
});
const metrics = new Metrics({
  namespace: 'testing',
  serviceName: 'powertools-event-handler',
});

const app = new Router({ logger });
app.use(async ({ reqCtx, next }) => {
  try {
    metrics.addMetadata('route', reqCtx.route); //note route doesn't exist on context
    metrics.addMetadata('path', reqCtx.event.path);
    metrics.addMetadata('method', reqCtx.req.method);
    metrics.addMetadata('ip', reqCtx.event.headers['X-Forwarded-For'].split(',')[0]);
    metrics.addMetadata('userAgent', reqCtx.event.headers['User-Agent']);
    metrics.addMetadata('apiGRequestId', reqCtx.event.requestId);
    metrics.addMetadata('apiGExtendedRequestId', reqCtx.event.extendedRequestId);
    metrics.addMetadata('cfRequestId', reqCtx.event.headers['X-Amz-Cf-Id']);
    metrics.addMetadata('requestId', reqCtx.context.awsRequestId);
    metrics.addMetadata('functionVersion', reqCtx.context.functionVersion);
    metrics.addMetadata('traceId', reqCtx.event.headers['X-Amzn-Trace-Id']?.replace('Root=', ''));
  }catch(error){
    logger.error('Error adding metadata', { error });
  }
  try {
    await next();
  } catch(error) {
    if(error){
      metrics.addMetadata('errorDetails', error.message);
      metrics.addMetadata('errorType', error.name);
      metrics.addMetadata('errorStack', error.cause ? error.cause.stack : error.stack);
      if (reqCtx.res.status >= 500) {
        throw error;
      }
    }
  }
  finally {
    metrics.addMetadata('responseCode', reqCtx.res.status + "");
    metrics.addMetric('fault', MetricUnit.Count, reqCtx.res.status >= 500 ? 1 : 0);
    metrics.addMetric('error', MetricUnit.Count, (reqCtx.res.status >= 400 && reqCtx.res.status < 500) ? 1 : 0);
    metrics.addMetric('latency', MetricUnit.Milliseconds, Date.now() - startTime);
    metrics.publishStoredMetrics();
  }
});

app.get('/ping', async () => {
  return { message: 'pong' };
});

app.get('/error', async () => {
  throw new BadRequestError("This is an error");
});

app.get('/fault', async () => {
  throw new Error('This is a fault');
});

export const handler = async (event: unknown, context: Context) => {
  startTime = Date.now();
  logger.addContext(context);
  logger.setCorrelationId(event, correlationPaths.API_GATEWAY_REST);
  return app.resolve(event, context);
};

Below are some sample log outputs for the exposed routes:

/ping

{
    "_aws": {
        "Timestamp": 1759372476245,
        "CloudWatchMetrics": [
            {
                "Namespace": "testing",
                "Dimensions": [
                    [
                        "service"
                    ]
                ],
                "Metrics": [
                    {
                        "Name": "fault",
                        "Unit": "Count"
                    },
                    {
                        "Name": "error",
                        "Unit": "Count"
                    },
                    {
                        "Name": "latency",
                        "Unit": "Milliseconds"
                    }
                ]
            }
        ]
    },
    "service": "powertools-event-handler",
    "fault": 0,
    "error": 0,
    "latency": 241,
    "path": "/ping",
    "method": "GET",
    "ip": "71.212.29.6",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
    "apiGRequestId": "8300e8f6-45ca-467c-92ee-fac812d54f1d",
    "apiGExtendedRequestId": "RzCtYHR8iYcEu2A=",
    "cfRequestId": "viQrbfwMYaFhUEEkozh1ykmtBbixdN0JzRBMx-oNMpAH5sLzcszLaA==",
    "requestId": "cf60e110-fe37-4a6b-8ab4-ac251c3f7271",
    "functionVersion": "39",
    "traceId": "1-68dde4bb-44b8df72029d7fff1208aba3",
    "responseCode": "200"
}

/error

{
    "_aws": {
        "Timestamp": 1759372479479,
        "CloudWatchMetrics": [
            {
                "Namespace": "testing",
                "Dimensions": [
                    [
                        "service"
                    ]
                ],
                "Metrics": [
                    {
                        "Name": "fault",
                        "Unit": "Count"
                    },
                    {
                        "Name": "error",
                        "Unit": "Count"
                    },
                    {
                        "Name": "latency",
                        "Unit": "Milliseconds"
                    }
                ]
            }
        ]
    },
    "service": "powertools-event-handler",
    "fault": 1,
    "error": 0,
    "latency": 2,
    "path": "/error",
    "method": "GET",
    "ip": "71.212.29.6",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
    "apiGRequestId": "5385f574-dc12-476a-a3b2-616f39e83b41",
    "apiGExtendedRequestId": "RzCt-HVCCYcEv5Q=",
    "cfRequestId": "XLdWKQSJAsw1-6E4_qKfeghpxlixMVL6MNcUz_h2TnNnBlTE8w2dYg==",
    "requestId": "894ee350-9444-4b49-bf2c-9f095ee8b875",
    "functionVersion": "39",
    "traceId": "1-68dde4bf-4bc7c50f7cd809de016d0508",
    "errorDetails": "This is an error",
    "errorType": "BadRequestError",
    "errorStack": "BadRequestError: This is an error\n    at /var/task/index.js:6903:9\n    at handlerMiddleware (/var/task/index.js:1683:39)\n    at dispatch (/var/task/index.js:1019:38)\n    at nextFn (/var/task/index.js:1014:23)\n    at /var/task/index.js:6881:11\n    at dispatch (/var/task/index.js:1019:38)\n    at /var/task/index.js:1030:11\n    at Router.resolve (/var/task/index.js:1694:38)\n    at Runtime.handler (/var/task/index.js:6912:14)\n    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1306:29)",
    "responseCode": "500"
}

Note this has the responseCode set to 500 even though the request returns a 400 so the fault and error counters are wrong.

/fault

{
    "_aws": {
        "Timestamp": 1759372482624,
        "CloudWatchMetrics": [
            {
                "Namespace": "testing",
                "Dimensions": [
                    [
                        "service"
                    ]
                ],
                "Metrics": [
                    {
                        "Name": "fault",
                        "Unit": "Count"
                    },
                    {
                        "Name": "error",
                        "Unit": "Count"
                    },
                    {
                        "Name": "latency",
                        "Unit": "Milliseconds"
                    }
                ]
            }
        ]
    },
    "service": "powertools-event-handler",
    "fault": 1,
    "error": 0,
    "latency": 5,
    "path": "/fault",
    "method": "GET",
    "ip": "71.212.29.6",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
    "apiGRequestId": "6b762937-0ca9-4406-b2fe-2f93c856cd2d",
    "apiGExtendedRequestId": "RzCueEv6iYcEFlg=",
    "cfRequestId": "p-ODGZvBOTfvfYIfjXcOvwbu6X7FWgpG0viM8rjjEo4wcyoCs9kvZA==",
    "requestId": "1af4b006-09b7-4770-823a-15ddab8af6e3",
    "functionVersion": "39",
    "traceId": "1-68dde4c2-7a71c6d017b69c45550b64b7",
    "errorDetails": "This is a fault",
    "errorType": "Error",
    "errorStack": "Error: This is a fault\n    at /var/task/index.js:6906:9\n    at handlerMiddleware (/var/task/index.js:1683:39)\n    at dispatch (/var/task/index.js:1019:38)\n    at nextFn (/var/task/index.js:1014:23)\n    at /var/task/index.js:6881:11\n    at dispatch (/var/task/index.js:1019:38)\n    at /var/task/index.js:1030:11\n    at Router.resolve (/var/task/index.js:1694:38)\n    at Runtime.handler (/var/task/index.js:6912:14)\n    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1306:29)",
    "responseCode": "500"
}

/notfound

{
    "_aws": {
        "Timestamp": 1759372493850,
        "CloudWatchMetrics": [
            {
                "Namespace": "testing",
                "Dimensions": [
                    [
                        "service"
                    ]
                ],
                "Metrics": [
                    {
                        "Name": "fault",
                        "Unit": "Count"
                    },
                    {
                        "Name": "error",
                        "Unit": "Count"
                    },
                    {
                        "Name": "latency",
                        "Unit": "Milliseconds"
                    }
                ]
            }
        ]
    },
    "service": "powertools-event-handler",
    "fault": 0,
    "error": 1,
    "latency": 9,
    "path": "/notfound",
    "method": "GET",
    "ip": "71.212.29.6",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0",
    "apiGRequestId": "a3ccaaef-1673-453e-9886-f414ead740e7",
    "apiGExtendedRequestId": "RzCwOFQ-CYcEpvQ=",
    "cfRequestId": "6Cc8LZyzOOhGVAbndbYXYGAFN8d6LawxHNSw-9IIQaWRlOrhUtDgRQ==",
    "requestId": "287cbfd3-a533-4c5f-b78d-eec34b26d56a",
    "functionVersion": "39",
    "traceId": "1-68dde4cd-656de8ab1e1bdfcf550e9f06",
    "responseCode": "404"
}

Solution/User Experience

After doing this exercise I have 4 requests.

  1. Normalize common metadata and expose it in a consistent way regardless of source, i.e. regardless of whether it's APIG or a Function URL, expose things like ip, path and user-agent with context.ip or some other mechanism so the user doesn't need to extract them from headers. lambda-api has a good example of this
  2. Expose the matched route to middleware on the context, I didn't see it surfaced.
  3. In global middleware, when throwing pre-defined errors, the status on the response should not be 500, it should be the status code for the actual error. When the response is returned to the browser it is the correct value, just in the global middleware it is 500 (see /error log above). Note, this is not the case on /notfound so I'm not sure whether this is user error. I did read the errorHandling docs but I don't want a separate handler per error type. I just want to throw the predefined error from the route and have the default propogate to the end user.
  4. Expose a way to set values on the context from middleware. For example if I had a JWT middleware, I might want to add username to the context. Both hono and lambda-api allow you to do something like context.set('username', username) so you can access it later in the request.

Alternative solutions

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestThis item refers to a feature request for an existing or new utility

    Type

    No type

    Projects

    Status

    Ideas

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions