-
Notifications
You must be signed in to change notification settings - Fork 14
Add ADR052 on storing analytics in Aurora #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
theseanything
wants to merge
1
commit into
main
Choose a base branch
from
ADR-business-analytics-storage-options
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+71
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # ADR052: Store analytics in a separate Aurora PostgreSQL database | ||
|
|
||
| Date: 2026-09-07 | ||
|
|
||
| ## Status | ||
|
|
||
| Accepted | ||
|
|
||
| ## Context | ||
|
|
||
| We want to measure key performance indicators for GOV.UK Forms, starting with: | ||
|
|
||
| - the number of submissions over time | ||
| - the number of live and archived forms over time | ||
|
|
||
| We need at least 2 years of data for year-on-year comparisons. Longer retention is preferred provided it is cheap and efficient for us. | ||
|
|
||
| These are business analytics rather than operational monitoring. Accuracy matters. The data does not need to be real-time, but having up-to-date figures is useful. | ||
|
|
||
| forms-runner already logs a structured `form_submission` event for every submission. | ||
|
|
||
| Recording the number of submissions per form and the number of forms per organisation is not a necessity, but the granularity would be useful if we later wanted to reuse the metrics for form-level analytics, for example showing per-form statistics in forms-admin. | ||
|
|
||
| ### Options considered and rejected | ||
|
|
||
| **CloudWatch OpenTelemetry metrics (tried).** This is AWS's current, recommended metrics model, billed per GB ingested with 15 months of retention. Metrics are statistical aggregates, not a record of events, so should not be used for anything that needs exact figures. For example, counters are aggregated in-process and exported on an interval, so counts are lost if a task exits before export, and PromQL `rate()` and `increase()` extrapolate across the query window and can return non-integer results. The OpenTelemetry SDK also caps each metric at 2,000 attribute combinations by default, after which per-form measurements collapse into a single overflow bucket. | ||
|
|
||
| **CloudWatch Classic metrics.** Every unique dimension combination is a separately billed metric at $0.30 per metric per month, so a per-form dimension costs roughly $0.30 per form per environment per month and grows with every form we host. Metrics expire after 15 months. | ||
|
|
||
| **Google Analytics 4.** Client-side events only fire after a user accepts usage cookies, so they undercount. The server-side Measurement Protocol requires a user id on every event, so we would have to create pseudo ids. Sending submissions with fabricated ids either inflates user counts or lands them under "(not set)", polluting our real user metrics. A per-form dimension exceeds GA4's 500-value high-cardinality threshold, so less common forms are condensed into an "(other)" row. Event-level data is retained for at most 14 months on a standard property. | ||
|
|
||
| **Splunk.** We keep logs for 12 months, it is not a tool we own, and we intend to move away from it. | ||
|
|
||
| **CloudWatch Logs Insights over existing logs.** This needs no new infrastructure, but every query scans the whole runner log group rather than just submission events, so we pay for and wait on data we do not need. Keeping the whole log group for years to preserve a handful of events would be wasteful. | ||
|
|
||
| **The forms-runner database.** This would grant observability tooling access to a database with potentially sensitive information, and analytics queries could affect production performance. | ||
|
|
||
| **Amazon Redshift Serverless or OpenSearch.** Both are capable, but their cost far exceeds what a few small tables need. | ||
|
|
||
| **Amazon S3 Tables, queried with Amazon Athena (prototyped).** S3 Tables is storage optimised for analytics workloads, storing tables in Apache Iceberg format, and Athena is a serverless query engine that can read them. A working prototype fed the table from the structured log lines the applications already emit, using a CloudWatch Logs subscription filter, a Lambda transform and Kinesis Data Firehose, so it needed no application changes. It is very cheap, less than $10 per year per environment, and fully managed. | ||
|
|
||
| However: | ||
|
|
||
| - S3 Tables, Firehose and Athena are all unfamiliar services to the team, and the pipeline has several moving parts to understand and maintain. | ||
| - Firehose delivery is at-least-once, so queries would need de-duplication or the transform would need an idempotency key. | ||
| - Table schemas are defined in Terraform, but the provider does not support partitioning yet. | ||
| - Athena queries take seconds, too slow for the request path. If we wanted to extend this to showing per-form statistics in forms-admin would need a separate, faster way to query the same data. | ||
|
|
||
| ## Decision | ||
|
|
||
| We will store analytics in a new Aurora PostgreSQL cluster, separate from the forms-admin and forms-runner databases, holding only analytics tables. Each environment gets its own cluster. | ||
|
|
||
| The applications write events to it directly through a second Rails database connection, rather than deriving events from logs. Only non-sensitive fields are stored: form identifiers and metadata, never answers or personal data. | ||
|
|
||
| We chose Aurora because: | ||
|
|
||
| - It is familiar. We already run Aurora PostgreSQL, so there are no new services to learn and we can reuse existing Terraform, backup and access patterns. | ||
| - Writes are transactional and exactly-once, with no log parsing or de-duplication, which suits data where accuracy matters. | ||
| - Indexed rows give low-latency queries, so the same store can later serve per-form analytics inside forms-admin. | ||
| - It is isolated from production databases, so analytics queries cannot affect production performance. | ||
| - We can set our own retention limits, and the data is straightforward to export if we later need to move it elsewhere. | ||
| - It can be queried by data visualisation tools, including Grafana's PostgreSQL datasource. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Always-on cost, about $500 per year per environment, as the cluster must run whenever we accept submissions. | ||
| - More to manage: engine upgrades, backups and credentials for another cluster in each environment. | ||
| - Durability depends on our own backup and restore, so the cluster must be included in our existing backup arrangements. | ||
| - A row store is not designed for analytical queries. This is not a problem at our current scale, and the data can be moved to a columnar store such as S3 Tables if it becomes one. | ||
| - The applications need a second database connection and a small amount of code to record events, so this is not a zero-change option. | ||
| - We also need to implement a way to visualise this data. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we know how big of a problem this actually is?
I think we have relatively low traffic. I'm wondering whether we could make it work with the existing clusters. Is it worth considering adding read replicas to deal with any performance concerns? I believe this is cheaper than adding a new cluster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think - you still pay the same ACU costs, but uses the existing storage (tho that is negligible anyways) - however, we might be able to allow the read replica scale down to zero (but the long start up times, wouldn't be suitable prod)?
I guess the other thing is how we limit data access, because it'd be the forms-runner db with the submission data - which would require creating another (Postgres) Role in the DB. You could do it as a Rails migration? but kinda messy, you need to also get the password into Secrets Manager - and co-ordinate when the sequence of events.
Not convinced the complexity of keeping the data secure is worth the cost savings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had assumed we'd create a separate database within the same cluster. So that the isolation is provide at that level. I think the access is managed per database, rather than per cluster
But I might be getting confused about how that works 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you can create another logical "database" in postgres cluster - but you still need to manage permissions to that inside postgres (not something you can do with IAM). Within the physical cluster - you can create a "read replica", but that accesses the same underlying data (with all the logical databases).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we definitely not go with the option of storing the metrics in the admin db, using that data to drive metrics we provide to form creators (instead of cloudwatch), and offer a simple dashboard and download of these stats for internal purposes?