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.

1 min read

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.

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:

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.