Featured image of post Using AWS CloudFront Functions for Request and Response Logging

Using AWS CloudFront Functions for Request and Response Logging

Introduction

We have multiple CloudFront distributions deployed in our AWS account, and we want to monitor the requests and responses. Most of the distributions are running in the free tier except the critical production ones. CloudFront distributions using a flat-rate free plan offer no request logs to understand security and delivery traffic patterns.

This brings us to our solution: using native CloudFront Functions to log the requests and responses to CloudWatch Logs.

CloudFront Settings

In order to make sure that the requests contain the correct headers that can be logged, we need to set Origin request policy to AllViewerExceptHostHeader. This policy forwards all viewer request headers, cookies, and query strings to your origin server, except for the Host header.

The AllViewerExceptHostHeader policy is intended for use with Amazon API Gateway and AWS Lambda function URLs. It usually breaks Application Load Balancers (ALBs) and is unnecessary for Amazon S3. Since we are using S3 as our origin, we can safely set this policy to capture all viewer request headers.

CloudFront Function for Request and Response Logging

We will now create the request logging function. We are using CloudFront Functions JavaScript 2.0 runtime ( cloudfront-js-2.0):

1
2
3
4
5
6
7
8
9
function handler(event) {
    var request = event.request;

    // Log the entire request object as a JSON string to CloudWatch
    console.log(JSON.stringify(request));

    // Always return the request object
    return request;
}

The above function will log the entire request object to CloudWatch Logs and has to be attached to the viewer request event type. Here is the code for the viewer response event:

1
2
3
4
5
6
7
8
9
function handler(event) {
    var response = event.response;

    // Log the entire response object (Status code, headers, etc.) as a JSON string to CloudWatch
    console.log(JSON.stringify(response));

    // Always return the response object to pass it back to the client browser
    return response;
}

By attaching these functions to the viewer request and viewer response events, we can log the entire request and response objects to CloudWatch Logs. Here is a sample request logged object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
  "request": {
    "method": "GET",
    "uri": "/tags/container/index.html",
    "querystring": {},
    "headers": {
      "user-agent": {
        "value": "Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (HTML, like Gecko) Mobile Safari/537.36"
      },
      "cloudfront-viewer-http-version": {
        "value": "1.1"
      },
      "cloudfront-is-tablet-viewer": {
        "value": "false"
      },
      "cloudfront-is-smarttv-viewer": {
        "value": "false"
      },
      "cloudfront-viewer-asn": {
        "value": "136907"
      },
      "cloudfront-is-android-viewer": {
        "value": "true"
      },
      "referer": {
        "value": "https://habibiops.com/p/terraform-container-image-for-cicd-pipelines/"
      },
      "cloudfront-viewer-tls": {
        "value": "TLSv1.3:TLS_AES_128_GCM_SHA256:fullHandshake"
      },
      "accept": {
        "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
      },
      "host": {
        "value": "habibiops.com"
      },
      "cloudfront-viewer-country": {
        "value": "SG"
      },
      "cloudfront-viewer-city": {
        "value": "Singapore"
      },
      "cloudfront-is-mobile-viewer": {
        "value": "true"
      },
      "cloudfront-viewer-time-zone": {
        "value": "Asia/Singapore"
      },
      "cloudfront-forwarded-proto": {
        "value": "https"
      },
      "cloudfront-viewer-address": {
        "value": "114.119.<redacted>.<redacted>:29579"
      },
      "accept-language": {
        "value": "en"
      },
      "cloudfront-is-desktop-viewer": {
        "value": "false"
      },
      "cloudfront-viewer-postal-code": {
        "value": "17"
      },
      "accept-encoding": {
        "value": "gzip, deflate, br, zstd"
      },
      "cloudfront-viewer-longitude": {
        "value": "103.85070"
      },
      "cloudfront-is-ios-viewer": {
        "value": "false"
      },
      "cloudfront-viewer-country-name": {
        "value": "Singapore"
      },
      "cloudfront-viewer-latitude": {
        "value": "1.28720"
      }
    },
    "cookies": {}
  }
}

Here is a sample response logged object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
  "response": {
    "statusDescription": "OK",
    "statusCode": 200,
    "headers": {
      "server": {
        "value": "AmazonS3"
      },
      "transfer-encoding": {
        "value": "chunked"
      },
      "x-frame-options": {
        "value": "SAMEORIGIN"
      },
      "x-content-type-options": {
        "value": "nosniff"
      },
      "date": {
        "value": "Sun, 11 Oct 2026 14:30:56 GMT"
      },
      "content-encoding": {
        "value": "br"
      },
      "etag": {
        "value": "W/\"<redacted>\""
      },
      "last-modified": {
        "value": "Wed, 29 Jul 2026 12:22:56 GMT"
      },
      "x-xss-protection": {
        "value": "1; mode=block"
      },
      "vary": {
        "value": "Accept-Encoding"
      },
      "content-type": {
        "value": "text/html"
      },
      "x-amz-server-side-encryption": {
        "value": "AES256"
      },
      "via": {
        "value": "1.1 <redacted>.cloudfront.net (CloudFront)"
      },
      "referrer-policy": {
        "value": "strict-origin-when-cross-origin"
      },
      "strict-transport-security": {
        "value": "max-age=31536000"
      }
    },
    "cookies": {}
  }
}

To correlate the requests and responses to each other, we can also log the request ID in both functions: event.context.requestId.

The CloudFront Function CloudWatch log group will be created automatically inside the N. Virginia (us-east-1) region. They will be named as follows: /aws/cloudfront/function/<FUNCTION_NAME>. The log group will have a “never expire” retention policy. Be sure to either export the logs somewhere else or delete them after processing.

Request Headers and Query Parameters

The CloudFront event structure contains a lot of information about the request, including the request headers, query parameters, and cookies. We can get some insights from the following fields:

  • uri: determine the URI.
  • user-agent inside the headers object: determine the type of device and browser or if it is a bot.
  • cloudfront-is-tablet-viewer, cloudfront-is-android-viewer, cloudfront-is-mobile-viewer, or cloudfront-is-desktop-viewer: determine the type of device.
  • referer: determine the referrer of the request.
  • cloudfront-viewer-country: determine the country of the viewer.
  • cloudfront-viewer-city: determine the city of the viewer.
  • cloudfront-viewer-address: determine the ip address of the viewer.
  • cloudfront-viewer-country-name: determine the country name of the viewer.

Conclusion

We saw how to use CloudFront Functions to log the requests and responses to CloudWatch Logs. This can be useful for monitoring and debugging when using a free tier CloudFront distribution that does not support enabling access logs. We will see in the coming posts how to use Grafana to visualize these logs with a nice dashboard.

References: