Skip to content

Commit 32db903

Browse files
committed
Add service to publish form counts to CloudWatch
Publishes form counts grouped by organisation and state so they can be monitored in CloudWatch.
1 parent 9327778 commit 32db903

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
module Metrics
2+
class FormCountService
3+
METRIC_NAME = "FormCount".freeze
4+
METRICS_NAMESPACE = CloudWatchService::METRICS_NAMESPACE
5+
REGION = CloudWatchService::REGION
6+
BATCH_SIZE = 500
7+
UNKNOWN_ORG = "Unknown".freeze
8+
9+
def publish_form_counts
10+
metric_count = 0
11+
12+
form_counts_by_org_and_state.each_slice(BATCH_SIZE) do |batch|
13+
cloudwatch_client.put_metric_data(
14+
namespace: METRICS_NAMESPACE,
15+
metric_data: batch.map { |((org, state), count)| metric_datum(org:, state:, count:) },
16+
)
17+
metric_count += batch.size
18+
end
19+
20+
Rails.logger.info "Published #{metric_count} form count metrics to CloudWatch"
21+
rescue Aws::CloudWatch::Errors::ServiceError,
22+
Aws::Errors::MissingCredentialsError => e
23+
Sentry.capture_exception(e)
24+
raise
25+
end
26+
27+
private
28+
29+
def form_counts_by_org_and_state
30+
counts_by_org_and_state = Form
31+
.where.not(state: :deleted)
32+
.left_joins(group_form: { group: :organisation })
33+
.group(Organisation.arel_table[:name], Form.arel_table[:state])
34+
.count
35+
36+
counts_by_org_and_state.each_with_object(Hash.new(0)) do |((org_name, state), count), totals|
37+
totals[[org_name || UNKNOWN_ORG, metric_state(state)]] += count
38+
end
39+
end
40+
41+
def metric_state(state)
42+
case state
43+
when "live", "live_with_draft" then "live"
44+
when "archived", "archived_with_draft" then "archived"
45+
when "draft" then "draft"
46+
end
47+
end
48+
49+
def metric_datum(org:, state:, count:)
50+
{
51+
metric_name: METRIC_NAME,
52+
dimensions: [
53+
environment_dimension,
54+
{ name: "Org", value: org },
55+
{ name: "State", value: state },
56+
],
57+
value: count,
58+
unit: "Count",
59+
timestamp: Time.zone.now,
60+
}
61+
end
62+
63+
def environment_dimension
64+
{
65+
name: "Environment",
66+
value: Settings.forms_env.downcase,
67+
}
68+
end
69+
70+
def cloudwatch_client
71+
@cloudwatch_client ||= Aws::CloudWatch::Client.new(region: REGION)
72+
end
73+
end
74+
end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require "rails_helper"
2+
3+
describe Metrics::FormCountService do
4+
subject(:service) { described_class.new }
5+
6+
let(:forms_env) { "test" }
7+
let(:cloud_watch_client) { Aws::CloudWatch::Client.new(stub_responses: true) }
8+
let(:organisation) { create(:organisation, name: "Department for Testing") }
9+
let(:group) { create(:group, organisation:) }
10+
11+
before do
12+
allow(Settings).to receive(:forms_env).and_return(forms_env)
13+
allow(Aws::CloudWatch::Client).to receive(:new).and_return(cloud_watch_client)
14+
end
15+
16+
around do |example|
17+
travel_to(Time.zone.local(2026, 6, 3, 12, 0, 0)) do
18+
example.run
19+
end
20+
end
21+
22+
describe "#publish_form_counts" do
23+
before do
24+
Form.destroy_all
25+
26+
create(:form, :with_group, group:, state: :draft)
27+
create(:form, :live, :with_group, group:)
28+
create(:form, :live_with_draft, :with_group, group:)
29+
create(:form, :archived, :with_group, group:)
30+
create(:form, :archived_with_draft, :with_group, group:)
31+
create(:form, state: :draft)
32+
end
33+
34+
it "publishes grouped form counts to CloudWatch" do
35+
expect(cloud_watch_client).to receive(:put_metric_data).with(
36+
namespace: "Forms",
37+
metric_data: contain_exactly(
38+
metric_datum(org: organisation.name, state: "draft", count: 1),
39+
metric_datum(org: organisation.name, state: "live", count: 2),
40+
metric_datum(org: organisation.name, state: "archived", count: 2),
41+
metric_datum(org: "Unknown", state: "draft", count: 1),
42+
),
43+
)
44+
45+
service.publish_form_counts
46+
end
47+
48+
context "when CloudWatch returns an error" do
49+
before do
50+
allow(cloud_watch_client).to receive(:put_metric_data)
51+
.and_raise(Aws::CloudWatch::Errors::ServiceError.new(nil, "CloudWatch error", nil))
52+
end
53+
54+
it "captures the exception and re-raises" do
55+
expect(Sentry).to receive(:capture_exception).with(instance_of(Aws::CloudWatch::Errors::ServiceError))
56+
57+
expect { service.publish_form_counts }.to raise_error(Aws::CloudWatch::Errors::ServiceError)
58+
end
59+
end
60+
end
61+
62+
def metric_datum(org:, state:, count:)
63+
{
64+
metric_name: "FormCount",
65+
dimensions: [
66+
{
67+
name: "Environment",
68+
value: forms_env,
69+
},
70+
{
71+
name: "Org",
72+
value: org,
73+
},
74+
{
75+
name: "State",
76+
value: state,
77+
},
78+
],
79+
value: count,
80+
unit: "Count",
81+
timestamp: Time.zone.local(2026, 6, 3, 12, 0, 0),
82+
}
83+
end
84+
end

0 commit comments

Comments
 (0)