# Privacy-friendly site analytics with CloudFront logs and Athena

> No tracking script, no cookie banner: query your CloudFront access logs with Athena to see what people read, for a few cents a month.

- Author: Jimmy Morin (Cloud Infrastructure Engineer)
- Published: 2026-10-08
- Updated: 2026-10-08
- Tags: aws, cloudfront, athena, finops
- Canonical: https://jimmymorin.cloud/posts/cloudfront-logs-athena-analytics

For a personal site, a JavaScript analytics tag is often overkill: it slows pages down, needs a consent banner in the EU, and gets blocked by a large share of visitors anyway. CloudFront already records every request. You just need a way to ask questions of those logs.

## The setup

1. Enable **standard logging** on the distribution, writing to a dedicated S3 bucket.
2. Create an **Athena** table over that bucket.
3. Query with SQL when you are curious.

There is nothing to run between queries, so the cost is S3 storage plus Athena's per-query data scanned — negligible at personal-site traffic. Add an S3 lifecycle rule so old logs expire.

## The table

CloudFront standard logs are gzipped, tab-separated files with two header lines. Columns must be declared in order; trailing ones you don't need can be left off.

```sql
CREATE EXTERNAL TABLE IF NOT EXISTS cloudfront_logs (
  `date`        DATE,
  `time`        STRING,
  location      STRING,
  bytes         BIGINT,
  request_ip    STRING,
  method        STRING,
  host          STRING,
  uri           STRING,
  status        INT,
  referrer      STRING,
  user_agent    STRING,
  query_string  STRING,
  cookie        STRING,
  result_type   STRING,
  request_id    STRING,
  host_header   STRING,
  request_protocol STRING,
  request_bytes BIGINT,
  time_taken    FLOAT
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 's3://my-cloudfront-logs/site/'
TBLPROPERTIES ('skip.header.line.count' = '2');
```

## Questions worth asking

Most-read articles over the last 30 days, ignoring assets and bots:

```sql
SELECT uri, count(*) AS views
FROM cloudfront_logs
WHERE "date" >= current_date - INTERVAL '30' DAY
  AND status = 200
  AND uri LIKE '/blog/%'
  AND uri NOT LIKE '%.%'
  AND lower(user_agent) NOT LIKE '%bot%'
GROUP BY uri
ORDER BY views DESC
LIMIT 20;
```

Swap the `GROUP BY` for `referrer` to see where readers come from, or for `result_type` to check your cache hit ratio.

## Trade-offs

- It counts **requests**, not people. That's fine for "what do readers care about?".
- Logs land with a delay, so this is not real-time.
- IP addresses are personal data under GDPR: keep retention short and restrict who can query the bucket.

For a blog, that's a fair deal: no script, no cookies, full ownership of the data.
