Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ gem "lograge"

# For distributed tracing and telemetry
gem "opentelemetry-exporter-otlp", "~> 0.34.1"
gem "opentelemetry-exporter-otlp-metrics", "~> 0.10.0"
gem "opentelemetry-instrumentation-all", "~> 0.94.0"
gem "opentelemetry-metrics-sdk", "~> 0.15.0"
gem "opentelemetry-propagator-xray", "~> 0.27.0"
gem "opentelemetry-sdk", "~> 1.12"

Expand Down
20 changes: 20 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-sdk (~> 1.10)
opentelemetry-semantic_conventions
opentelemetry-exporter-otlp-metrics (0.10.0)
google-protobuf (>= 3.18, < 5.0)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-metrics-api (~> 0.2)
opentelemetry-metrics-sdk (~> 0.5)
opentelemetry-sdk (~> 1.2)
opentelemetry-semantic_conventions
opentelemetry-helpers-mysql (0.6.0)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
Expand Down Expand Up @@ -512,6 +521,12 @@ GEM
opentelemetry-helpers-sql-processor
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-semantic_conventions (>= 1.8.0)
opentelemetry-metrics-api (0.6.1)
opentelemetry-api (~> 1.0)
opentelemetry-metrics-sdk (0.15.0)
opentelemetry-api (~> 1.1)
opentelemetry-metrics-api (~> 0.2)
opentelemetry-sdk (~> 1.2)
opentelemetry-propagator-xray (0.27.0)
opentelemetry-api (~> 1.7)
opentelemetry-registry (0.6.3)
Expand Down Expand Up @@ -776,7 +791,9 @@ DEPENDENCIES
omniauth-rails_csrf_protection
omniauth_govuk_one_login!
opentelemetry-exporter-otlp (~> 0.34.1)
opentelemetry-exporter-otlp-metrics (~> 0.10.0)
opentelemetry-instrumentation-all (~> 0.94.0)
opentelemetry-metrics-sdk (~> 0.15.0)
opentelemetry-propagator-xray (~> 0.27.0)
opentelemetry-sdk (~> 1.12)
pg (~> 1.6)
Expand Down Expand Up @@ -918,6 +935,7 @@ CHECKSUMS
opentelemetry-api (1.10.1) sha256=5db9535a8aa9e569808518aa103b2cfac44728672a1799e1dd217fe6d9c5642e
opentelemetry-common (0.25.1) sha256=38fe87c751e4e13fcd47a8290b4ad67bbedf76c4ed89aeced5ea5c620e66bfb1
opentelemetry-exporter-otlp (0.34.1) sha256=4697d81e03aff80ea6fbcc741b1c3c700d3624eddea14b453602ee23275450ca
opentelemetry-exporter-otlp-metrics (0.10.0) sha256=d8cbff9b8a3391eb61486b8be9b6ad74e3b9306a3c60fb4c906b28bc857167c8
opentelemetry-helpers-mysql (0.6.0) sha256=7eeb5e6950c434775a8cf28b5fde4defc12e8b865c86479ce3119fcf593d9337
opentelemetry-helpers-sql (0.4.0) sha256=b10e8c3a2cca28a98af951bbb3e4efdc59e68b25ba0825e055574af543420afb
opentelemetry-helpers-sql-processor (0.5.0) sha256=b199241bc9451fcbd9f00b2f454830af19d4ca27c2219ea379c9b0d53cd0e0f1
Expand Down Expand Up @@ -967,6 +985,8 @@ CHECKSUMS
opentelemetry-instrumentation-sidekiq (0.29.0) sha256=b1d2a0cb9041a5e14239fe7c94d99e3dd07f870e2759460ab63592d7cdd8aadc
opentelemetry-instrumentation-sinatra (0.30.0) sha256=b67301153420f43264a0c68cdb3ca5bd77467cf5054e57b83a2bf891aaaa0361
opentelemetry-instrumentation-trilogy (0.69.0) sha256=0676dd720eeab284abfa52f273967442156fcac7084a1e1411373cf14ec026ad
opentelemetry-metrics-api (0.6.1) sha256=e1785d317c0b4fe5bb128ab9dd4644486107fe28fba844f5329bf3db90aaa5ec
opentelemetry-metrics-sdk (0.15.0) sha256=611a9cd9f473c461095c7401b8c25f9774160d286a1acbfcbf044da2972aeada
opentelemetry-propagator-xray (0.27.0) sha256=753f756c7ad3146f182d428b06041084eecc77769edfd280f365e0bc09b9c4d1
opentelemetry-registry (0.6.3) sha256=568fa8eaea94394f29f4fce3f0339ab10c3bf6d6c8beed6b3cfee6fd37fcc608
opentelemetry-sdk (1.12.0) sha256=a224abe0c59023d41cb7ac1c634d9d28843907efcd045ed1ae320796c48b864b
Expand Down
2 changes: 2 additions & 0 deletions app/services/form_submission_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def submit
submission = create_submission_record
enqueue_deliveries(submission)

Metrics.record_submission(form_id: form.id, form_name: form.name, mode:)

LogEventService.log_submit(
current_context,
requested_email_confirmation: requested_confirmation?,
Expand Down
45 changes: 45 additions & 0 deletions app/services/metrics.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module Metrics
METER_NAME = "forms-runner".freeze
METER_VERSION = "1.0".freeze

# Forms with these names are used for automated testing of the platform
TEST_FORM_NAME_PATTERNS = [/\Acapybara/, /smoke/, /\As3/].freeze

class << self
def record_submission(form_id:, form_name:, mode:)
submission_counter.add(
1,
attributes: {
"form.id" => form_id.to_s,
"form.submission.mode" => mode_label(form_name:, mode:),
},
)
rescue StandardError => e
Sentry.capture_exception(e)
end

private

def mode_label(form_name:, mode:)
return "test" if test_form?(form_name)

mode.to_s
end

def test_form?(form_name)
TEST_FORM_NAME_PATTERNS.any? { |pattern| pattern.match?(form_name.to_s) }
end

def submission_counter
@submission_counter ||= meter.create_counter(
"form.submission.created",
unit: "{submission}",
description: "The number of form submissions queued for delivery",
)
end

def meter
@meter ||= OpenTelemetry.meter_provider.meter(METER_NAME, version: METER_VERSION)
end
end
end
10 changes: 10 additions & 0 deletions config/initializers/opentelemetry.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require "opentelemetry/sdk"
require "opentelemetry/instrumentation/all"
require "opentelemetry-metrics-sdk"
require "opentelemetry-exporter-otlp-metrics"

return unless ENV["ENABLE_OTEL"] == "true"

Expand All @@ -12,6 +14,14 @@
c.id_generator = OpenTelemetry::Propagator::XRay::IDGenerator
end

unless ENV.fetch("OTEL_METRICS_EXPORTER", "otlp") == "none"
c.add_metric_reader(
OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
exporter: OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new,
),
)
end

# Disable logging for Rake tasks to avoid cluttering output
c.logger = Logger.new(File::NULL) if Rails.const_defined?(:Rake) && Rake.application.top_level_tasks.any?
end
22 changes: 22 additions & 0 deletions spec/services/form_submission_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@
expect(log_line["submission_reference"]).to eq(reference)
end

it "records a submission count metric" do
expect(Metrics).to receive(:record_submission).with(
form_id: form.id,
form_name: form.name,
mode:,
)

service.submit
end

shared_examples "logging" do
it "logs submission" do
allow(LogEventService).to receive(:log_submit).once
Expand Down Expand Up @@ -152,6 +162,12 @@
allow(SendS3SubmissionJob).to receive(:perform_later).and_yield(instance_double(SendS3SubmissionJob, successfully_enqueued?: false, enqueue_error:))
end

it "does not record a submission count metric" do
expect(Metrics).not_to receive(:record_submission)

expect { service.submit }.to raise_error(StandardError)
end

context "and there is no enqueue error" do
it "raises an error" do
expect { service.submit }
Expand Down Expand Up @@ -238,6 +254,12 @@
allow(SendSubmissionJob).to receive(:perform_later).and_yield(instance_double(SendSubmissionJob, successfully_enqueued?: false, enqueue_error:))
end

it "does not record a submission count metric" do
expect(Metrics).not_to receive(:record_submission)

expect { service.submit }.to raise_error(StandardError)
end

context "and there is no enqueue error" do
it "raises an error" do
expect { service.submit }
Expand Down
141 changes: 141 additions & 0 deletions spec/services/metrics_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
require "rails_helper"
require "opentelemetry-metrics-sdk"

describe Metrics do
let(:meter_provider) { OpenTelemetry::SDK::Metrics::MeterProvider.new }
let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new }
let(:form_id) { 42 }
let(:form_name) { "Apply for a juggling licence" }
let(:mode) { Mode.new("form") }

before do
allow(OpenTelemetry).to receive(:meter_provider).and_return(meter_provider)
meter_provider.add_metric_reader(metric_exporter)
reset_memoized_instruments
end

after do
reset_memoized_instruments
end

describe ".record_submission" do
it "records a submission count metric" do
described_class.record_submission(form_id:, form_name:, mode:)

expect(exported_data_points).to contain_exactly(
have_attributes(
value: 1,
attributes: {
"form.id" => form_id.to_s,
"form.submission.mode" => "form",
},
),
)
end

context "when mode is preview" do
let(:mode) { Mode.new("preview-live") }

it "records a metric with the preview mode label" do
described_class.record_submission(form_id:, form_name:, mode:)

expect(exported_data_points).to contain_exactly(
have_attributes(
value: 1,
attributes: include("form.submission.mode" => "preview-live"),
),
)
end
end

it "accumulates counts for the same form" do
2.times { described_class.record_submission(form_id:, form_name:, mode:) }

expect(exported_data_points).to contain_exactly(
have_attributes(
value: 2,
attributes: include("form.id" => form_id.to_s),
),
)
end

it "records separate counts per form" do
described_class.record_submission(form_id:, form_name:, mode:)
described_class.record_submission(form_id: 99, form_name:, mode:)

expect(exported_data_points).to contain_exactly(
have_attributes(
value: 1,
attributes: include("form.id" => form_id.to_s),
),
have_attributes(
value: 1,
attributes: include("form.id" => "99"),
),
)
end

it "records separate counts per mode" do
described_class.record_submission(form_id:, form_name:, mode:)
described_class.record_submission(form_id:, form_name:, mode: Mode.new("preview-draft"))

expect(exported_data_points).to contain_exactly(
have_attributes(
value: 1,
attributes: include("form.submission.mode" => "form"),
),
have_attributes(
value: 1,
attributes: include("form.submission.mode" => "preview-draft"),
),
)
end

context "when the form is a test form" do
[
"capybara test form",
"Automated smoke test form",
"s3 submission test form",
].each do |test_form_name|
context "with the name #{test_form_name.inspect}" do
let(:form_name) { test_form_name }

it "records a metric with the test mode label" do
described_class.record_submission(form_id:, form_name:, mode:)

expect(exported_data_points).to contain_exactly(
have_attributes(
value: 1,
attributes: include("form.submission.mode" => "test"),
),
)
end
end
end
end

context "when recording the metric raises an error" do
let(:error) { StandardError.new("metrics unavailable") }

before do
allow(described_class).to receive(:submission_counter).and_raise(error)
end

it "captures the exception in Sentry and does not raise" do
expect(Sentry).to receive(:capture_exception).with(error)

expect { described_class.record_submission(form_id:, form_name:, mode:) }.not_to raise_error
end
end
end

def exported_data_points
metric_exporter.pull
metric_exporter.metric_snapshots.flat_map(&:data_points)
end

def reset_memoized_instruments
described_class.instance_variable_set(:@meter, nil)
described_class.instance_variable_set(:@submission_counter, nil)
end
end