diff --git a/Gemfile b/Gemfile index a29fffd5c..ec902ddfa 100644 --- a/Gemfile +++ b/Gemfile @@ -60,6 +60,12 @@ gem "uk_postcode" # For structured logging gem "lograge" +# For distributed tracing and telemetry +gem "opentelemetry-exporter-otlp", "~> 0.31.1" +gem "opentelemetry-instrumentation-all", "~> 0.89.1" +gem "opentelemetry-propagator-xray", "~> 0.26.0" +gem "opentelemetry-sdk", "~> 1.10" + # For AWS interactions gem "aws-sdk-cloudwatch" gem "aws-sdk-codepipeline", "~> 1.110" diff --git a/Gemfile.lock b/Gemfile.lock index 3d14afc39..a58546a6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -211,6 +211,29 @@ GEM raabro (~> 1.4) globalid (1.3.0) activesupport (>= 6.1) + google-protobuf (4.33.2) + bigdecimal + rake (>= 13) + google-protobuf (4.33.2-aarch64-linux-gnu) + bigdecimal + rake (>= 13) + google-protobuf (4.33.2-aarch64-linux-musl) + bigdecimal + rake (>= 13) + google-protobuf (4.33.2-arm64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.33.2-x86_64-darwin) + bigdecimal + rake (>= 13) + google-protobuf (4.33.2-x86_64-linux-gnu) + bigdecimal + rake (>= 13) + google-protobuf (4.33.2-x86_64-linux-musl) + bigdecimal + rake (>= 13) + googleapis-common-protos-types (1.22.0) + google-protobuf (~> 4.26) govuk-components (5.13.1) html-attributes-utils (~> 1.0.0, >= 1.0.0) pagy (>= 6, < 10) @@ -309,6 +332,180 @@ GEM racc (~> 1.4) notifications-ruby-client (6.2.0) jwt (>= 1.5, < 3) + opentelemetry-api (1.7.0) + opentelemetry-common (0.23.0) + opentelemetry-api (~> 1.0) + opentelemetry-exporter-otlp (0.31.1) + google-protobuf (>= 3.18) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-sdk (~> 1.10) + opentelemetry-semantic_conventions + opentelemetry-helpers-mysql (0.4.0) + opentelemetry-api (~> 1.7) + opentelemetry-common (~> 0.21) + opentelemetry-helpers-sql (0.3.0) + opentelemetry-api (~> 1.7) + opentelemetry-helpers-sql-processor (0.3.1) + opentelemetry-common (~> 0.21) + opentelemetry-instrumentation-action_mailer (0.6.1) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-action_pack (0.15.1) + opentelemetry-instrumentation-rack (~> 0.29) + opentelemetry-instrumentation-action_view (0.11.1) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-active_job (0.10.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-active_model_serializers (0.24.0) + opentelemetry-instrumentation-active_support (>= 0.7.0) + opentelemetry-instrumentation-active_record (0.11.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-active_storage (0.3.1) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-active_support (0.10.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-all (0.89.1) + opentelemetry-instrumentation-active_model_serializers (~> 0.24.0) + opentelemetry-instrumentation-anthropic (~> 0.3.0) + opentelemetry-instrumentation-aws_lambda (~> 0.6.0) + opentelemetry-instrumentation-aws_sdk (~> 0.11.0) + opentelemetry-instrumentation-bunny (~> 0.24.0) + opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0) + opentelemetry-instrumentation-dalli (~> 0.29.0) + opentelemetry-instrumentation-delayed_job (~> 0.25.1) + opentelemetry-instrumentation-ethon (~> 0.26.0) + opentelemetry-instrumentation-excon (~> 0.26.0) + opentelemetry-instrumentation-faraday (~> 0.30.0) + opentelemetry-instrumentation-grape (~> 0.5.0) + opentelemetry-instrumentation-graphql (~> 0.31.1) + opentelemetry-instrumentation-grpc (~> 0.4.1) + opentelemetry-instrumentation-gruf (~> 0.5.0) + opentelemetry-instrumentation-http (~> 0.27.0) + opentelemetry-instrumentation-http_client (~> 0.26.0) + opentelemetry-instrumentation-httpx (~> 0.5.0) + opentelemetry-instrumentation-koala (~> 0.23.0) + opentelemetry-instrumentation-lmdb (~> 0.25.0) + opentelemetry-instrumentation-mongo (~> 0.25.0) + opentelemetry-instrumentation-mysql2 (~> 0.32.1) + opentelemetry-instrumentation-net_http (~> 0.26.0) + opentelemetry-instrumentation-pg (~> 0.34.1) + opentelemetry-instrumentation-que (~> 0.12.0) + opentelemetry-instrumentation-racecar (~> 0.6.0) + opentelemetry-instrumentation-rack (~> 0.29.0) + opentelemetry-instrumentation-rails (~> 0.39.1) + opentelemetry-instrumentation-rake (~> 0.5.0) + opentelemetry-instrumentation-rdkafka (~> 0.9.0) + opentelemetry-instrumentation-redis (~> 0.28.0) + opentelemetry-instrumentation-resque (~> 0.8.0) + opentelemetry-instrumentation-restclient (~> 0.26.0) + opentelemetry-instrumentation-ruby_kafka (~> 0.24.0) + opentelemetry-instrumentation-sidekiq (~> 0.28.1) + opentelemetry-instrumentation-sinatra (~> 0.28.0) + opentelemetry-instrumentation-trilogy (~> 0.65.1) + opentelemetry-instrumentation-anthropic (0.3.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-aws_lambda (0.6.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-aws_sdk (0.11.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-base (0.25.0) + opentelemetry-api (~> 1.7) + opentelemetry-common (~> 0.21) + opentelemetry-registry (~> 0.1) + opentelemetry-instrumentation-bunny (0.24.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-concurrent_ruby (0.24.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-dalli (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-delayed_job (0.25.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-ethon (0.26.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-excon (0.26.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-faraday (0.30.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-grape (0.5.0) + opentelemetry-instrumentation-rack (~> 0.29) + opentelemetry-instrumentation-graphql (0.31.2) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-grpc (0.4.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-gruf (0.5.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-http (0.27.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-http_client (0.26.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-httpx (0.5.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-koala (0.23.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-lmdb (0.25.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-mongo (0.25.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-mysql2 (0.32.1) + opentelemetry-helpers-mysql + opentelemetry-helpers-sql + opentelemetry-helpers-sql-processor + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-net_http (0.26.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-pg (0.34.1) + opentelemetry-helpers-sql + opentelemetry-helpers-sql-processor + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-que (0.12.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-racecar (0.6.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-rack (0.29.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-rails (0.39.1) + opentelemetry-instrumentation-action_mailer (~> 0.6) + opentelemetry-instrumentation-action_pack (~> 0.15) + opentelemetry-instrumentation-action_view (~> 0.11) + opentelemetry-instrumentation-active_job (~> 0.10) + opentelemetry-instrumentation-active_record (~> 0.11) + opentelemetry-instrumentation-active_storage (~> 0.3) + opentelemetry-instrumentation-active_support (~> 0.10) + opentelemetry-instrumentation-concurrent_ruby (~> 0.23) + opentelemetry-instrumentation-rake (0.5.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-rdkafka (0.9.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-redis (0.28.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-resque (0.8.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-restclient (0.26.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-ruby_kafka (0.24.0) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-sidekiq (0.28.1) + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-instrumentation-sinatra (0.28.0) + opentelemetry-instrumentation-rack (~> 0.29) + opentelemetry-instrumentation-trilogy (0.65.1) + opentelemetry-helpers-mysql + opentelemetry-helpers-sql + opentelemetry-helpers-sql-processor + opentelemetry-instrumentation-base (~> 0.25) + opentelemetry-semantic_conventions (>= 1.8.0) + opentelemetry-propagator-xray (0.26.0) + opentelemetry-api (~> 1.7) + opentelemetry-registry (0.4.0) + opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.10.0) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.36.0) + opentelemetry-api (~> 1.0) ostruct (0.6.3) pagy (9.4.0) parallel (1.27.0) @@ -562,6 +759,10 @@ DEPENDENCIES i18n-tasks (~> 1.1.2) json_schemer lograge + opentelemetry-exporter-otlp (~> 0.31.1) + opentelemetry-instrumentation-all (~> 0.89.1) + opentelemetry-propagator-xray (~> 0.26.0) + opentelemetry-sdk (~> 1.10) pg (~> 1.6) puma (~> 7.1.0) rails (= 8.1.1) @@ -645,6 +846,14 @@ CHECKSUMS faker (3.5.3) sha256=b961482dc0bb15ccb9a98ea7878b925669e9ae8f5e59b607da540b768137d765 fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 + google-protobuf (4.33.2) sha256=748150d6c642fd655ef39efa23ecf2abe6d616020039a6d1c1764be1da530315 + google-protobuf (4.33.2-aarch64-linux-gnu) sha256=822b2dcb707e94e652cd994642c31035935fca021adfac6164772c511eb7acd4 + google-protobuf (4.33.2-aarch64-linux-musl) sha256=c4b64428183cfd1953ec8c37beec1036668c8ec0865cfb0b18df21181ca397ee + google-protobuf (4.33.2-arm64-darwin) sha256=6d0ac185fed18768e5f16338455b1e4b7c38a97fc46f352e709f7a3007b64e1d + google-protobuf (4.33.2-x86_64-darwin) sha256=87cde586234674562cf099e2b708a65e376e2d39b0f0f48281f4b4ea182b47f8 + google-protobuf (4.33.2-x86_64-linux-gnu) sha256=73cba041477afcac92ff383fcbdec195ea28d96b994876d1deaa944d18f91786 + google-protobuf (4.33.2-x86_64-linux-musl) sha256=97cdf4f772c5540f9274603b00f1474ed5e6e2238b1d8b1585e77f941a36bd2c + googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7 govuk-components (5.13.1) sha256=74808d8188a7de2c5e83e5a829c5f8ac3851df27f82410a4a2444cf53c24cfe5 govuk-forms-markdown (0.6.0) govuk_design_system_formbuilder (5.13.0) sha256=126ff4af70b36f06395e8f4f09d399944bcc10e9a41c2ff7d7773e81e92ca885 @@ -687,6 +896,62 @@ CHECKSUMS nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c nokogiri (1.19.0-x86_64-linux-musl) sha256=1c4ca6b381622420073ce6043443af1d321e8ed93cc18b08e2666e5bd02ffae4 notifications-ruby-client (6.2.0) sha256=da4ba515cef3105d7ed46d17c644a3ea437f07cbc02c6cf095cf1984a656cf72 + opentelemetry-api (1.7.0) sha256=ccfd264ea6f2db5bf4185e3c07a1297977b44a944e2ce65457c4fe63a697214f + opentelemetry-common (0.23.0) sha256=da721190479d57bae0ad2207468f47f3e2c3b9a91024b5bc32c9d280183eb32c + opentelemetry-exporter-otlp (0.31.1) sha256=5358be17d7849cbcc4f49e1fc24105edc780a6f96c8e57b64192ab9a8e47474a + opentelemetry-helpers-mysql (0.4.0) sha256=d309c0b20825bdd14d4dbc75e0d3b381ffdad37d16424ceca3cb8453d9cb5a4f + opentelemetry-helpers-sql (0.3.0) sha256=4bb08017d6a16dd41c4d1c53c7fd30f9c5bb691195d8b458933724627b3f37f9 + opentelemetry-helpers-sql-processor (0.3.1) sha256=6041e934bee76c593e971a8bca0979a6ee411723affdd3c11ca1312903bcd9bc + opentelemetry-instrumentation-action_mailer (0.6.1) sha256=8384866bdb066ae14b9a1fe686ffaf1f23468326a35af64390c0395fcd471057 + opentelemetry-instrumentation-action_pack (0.15.1) sha256=84fade740783caeebf260aaefcbf8f1a7a4c49f946944ff520a2fb1d6b07f273 + opentelemetry-instrumentation-action_view (0.11.1) sha256=426134dd7604b77032abe26a49d75d16de32d89af962704c0fdf4ab203e5f599 + opentelemetry-instrumentation-active_job (0.10.1) sha256=aea1311224c20d064a8f218a44299171152dc36eeb531b9eba84bed8b3942a89 + opentelemetry-instrumentation-active_model_serializers (0.24.0) sha256=8fe81e44167d17e45d9acfa588d20140c7640c323e58aca99e266de1bb3fce15 + opentelemetry-instrumentation-active_record (0.11.1) sha256=1b083f34eea0449f8d6f4370b3fb4b935757fac6e4e538e67bb98211809e7c92 + opentelemetry-instrumentation-active_storage (0.3.1) sha256=f89b0fef54921f17c0c4c38a6e0926d29afabd0ac98436fcdbb8bde85dfde89e + opentelemetry-instrumentation-active_support (0.10.1) sha256=82ea98367158797e33c6de96581f10aa4fe8adf0ebec832dcff5fd04c59bc57d + opentelemetry-instrumentation-all (0.89.1) sha256=6a7de5fd7498024a34eecb63f3d69e8d3e1a3c7933bfef444e1d64e8c2b69f04 + opentelemetry-instrumentation-anthropic (0.3.0) sha256=09bd9b4ba6189389a6c0f7ba49f1d11f387d93b411ab585137a48b59925a48de + opentelemetry-instrumentation-aws_lambda (0.6.0) sha256=1a3161393cfe9bc9eddd81a0668d076c38a0a2c3d5df40e95d02f5a8fcd3334c + opentelemetry-instrumentation-aws_sdk (0.11.0) sha256=67a21e754ddf51e2bb8c3e46e116aa9158d8db800f34c2a9b1e0da5a6ca911e3 + opentelemetry-instrumentation-base (0.25.0) sha256=642a3a7f08354e6e969423327a4fa67ed2cca7ac6fe5ee09e55b17d1c576da27 + opentelemetry-instrumentation-bunny (0.24.0) sha256=1ec484e48a5f42a1d0c33e8e6bc7e9e78dd80f3ed9d63520b8a22ba564aa2585 + opentelemetry-instrumentation-concurrent_ruby (0.24.0) sha256=229bd8b72000c59de693609bb637b8a9114992f5e0ab03730d7fd7ef91f7d1d2 + opentelemetry-instrumentation-dalli (0.29.0) sha256=a2686650545609e8d7e281c9fd1aef529ab578ef2dcf9a6258737e4ba214bc2f + opentelemetry-instrumentation-delayed_job (0.25.1) sha256=47f35b10d2bfd9ac7c2bbbe10dea095a2e25db2a84f5351860ead969d180c3ec + opentelemetry-instrumentation-ethon (0.26.0) sha256=d4461082c84e8912ab1204340f31cae4aba58b4ae2a854d517b27116750e3752 + opentelemetry-instrumentation-excon (0.26.1) sha256=a856816c98d45ff4cd3ec3b0d7fc1e5e340390f47478f882eb4a688cfc678fef + opentelemetry-instrumentation-faraday (0.30.1) sha256=526822c0575aba333e53bfdb26a4a7b6a30c9cb1b1d400d514d891f4507732f9 + opentelemetry-instrumentation-grape (0.5.0) sha256=b9fcbe13b015b663577b8bde5b419c297da2588d0a022f4ce40f9ffc49df7624 + opentelemetry-instrumentation-graphql (0.31.2) sha256=a4455f225427f8f9058247c8c0b351b8932567913c35ef049f7958801d401b1f + opentelemetry-instrumentation-grpc (0.4.1) sha256=5ffa2bb1d5ec69bcd1fe23e1d8c1a563a00351ce052fe9d76885cc43f21ebc87 + opentelemetry-instrumentation-gruf (0.5.0) sha256=ee21be36e312e71b847c9a87168225625890121140a364b68d3668e0df58dacd + opentelemetry-instrumentation-http (0.27.1) sha256=ba70029da6fe9bdfadd31d539823fae3d7fefff11f3487aeeb4cc47b48cf7303 + opentelemetry-instrumentation-http_client (0.26.1) sha256=f6af45487998db43d4b8772b4929d692189b66c5591420220e06c3848012e6ff + opentelemetry-instrumentation-httpx (0.5.1) sha256=3ef926ec56e208290052c8d278e3f82890a7f6dadeb01a7c9a706a3fb4da52dd + opentelemetry-instrumentation-koala (0.23.0) sha256=8f324b50a2a64fd4994bb2b105a4cb0c80b64ec05cf5487d2daa906c650bc6f9 + opentelemetry-instrumentation-lmdb (0.25.0) sha256=1e4d66d583ea242d4f72051062971f5af1ea353484d224abbd0aabdd1ce5f5cb + opentelemetry-instrumentation-mongo (0.25.0) sha256=d04585669f928ea82e7c469f996061d39d8ff184278d57cf4fc77a6d607f9c7a + opentelemetry-instrumentation-mysql2 (0.32.1) sha256=9f5c705b374f7804d374e4fdb5b793c0321dea25644a006bbb76b2150452277a + opentelemetry-instrumentation-net_http (0.26.1) sha256=354ebf161f2aca0eaafdc9decc014f98b246994308a5de3ccf303f1d4abdfa2c + opentelemetry-instrumentation-pg (0.34.1) sha256=8d6d75f8d895eea040f72ac7765257e82f9aaa81eed44bd1afbcaa7f49ebb5c2 + opentelemetry-instrumentation-que (0.12.0) sha256=3b7a84341f6af5a04f8c57860aeba4033f87c855d40c611a2fc40dde849944fb + opentelemetry-instrumentation-racecar (0.6.1) sha256=833f6611906fb661f577e841d4ec52549474d32b4e8edea8048162348d35b845 + opentelemetry-instrumentation-rack (0.29.0) sha256=9e2cbb8336087064cbe33b502d917d85b174162bc717efda1cfdbd182342f377 + opentelemetry-instrumentation-rails (0.39.1) sha256=7959df7895543040fbb5cd3877c37bc9f95d79ff9d7749334314c50b871ac96f + opentelemetry-instrumentation-rake (0.5.0) sha256=fa6bd019078975ac8a67eaea06294e4fe6707e6770d8ced88d74dc573b0a01ef + opentelemetry-instrumentation-rdkafka (0.9.0) sha256=f3beb56828c584d7d91a2c46f6e5a2ef82289b1d4445b1eb5bc13b80ab6aca89 + opentelemetry-instrumentation-redis (0.28.0) sha256=8721957d1c527dd22bd564d17f3a8db252081abb302be189511282d023693900 + opentelemetry-instrumentation-resque (0.8.0) sha256=559edde9d6273dd757ae5149ed36e26d147b63028d084121203f51c8cff805e5 + opentelemetry-instrumentation-restclient (0.26.0) sha256=5d4e9d93ef51564a1023c076e17b4ac3b42fe81003321d9fb66e44886538bdce + opentelemetry-instrumentation-ruby_kafka (0.24.0) sha256=257e891f4ce630ba3e0669408d497b44afcc493cd49aed09343d5a51fa8952c2 + opentelemetry-instrumentation-sidekiq (0.28.1) sha256=abc85d62996a5362e7a9fd7af9f6c709d01ce04795514d12fee5126335ae97ae + opentelemetry-instrumentation-sinatra (0.28.0) sha256=9f11d68c580a421cadd633aca1f8f92707d6b6995d48fffa045a48c187347f26 + opentelemetry-instrumentation-trilogy (0.65.1) sha256=4e20ef2aee15613ea9e5468c1561184fc94cde615f3174c6f5d80c76fea8ccbd + opentelemetry-propagator-xray (0.26.0) sha256=c8f4328599e5749d3896aec118088de4a58914c4801802fe49cdf4f4fba6374f + opentelemetry-registry (0.4.0) sha256=903fa6bfaa29eac1c1d73a4fdd29b850977b5353b84b8cdff11222c00ad2968f + opentelemetry-sdk (1.10.0) sha256=43719949be8df24dcaeb86ebbf75636cda87d51a01af2729499b92a48b80521a + opentelemetry-semantic_conventions (1.36.0) sha256=c1b1607dbc7853aac7f9e23f6e8b76969c45b07f2b812a4aa4383c19a3b0f617 ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 pagy (9.4.0) sha256=db3f2e043f684155f18f78be62a81e8d033e39b9f97b1e1a8d12ad38d7bce738 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d339a9643..7bd6ba0e9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -35,6 +35,16 @@ def set_request_logging_attributes CurrentRequestLoggingAttributes.page_slug = params[:page_slug] if params[:page_slug].present? CurrentRequestLoggingAttributes.session_id_hash = session_id_hash CurrentRequestLoggingAttributes.trace_id = request.env["HTTP_X_AMZN_TRACE_ID"] if request.env["HTTP_X_AMZN_TRACE_ID"].present? + + # Add same attributes to OpenTelemetry span for journey tracking + TelemetryService.set_request_attributes({ + "session.id_hash" => session_id_hash, + "request.host" => request.host, + "request.id" => request.request_id, + "form.id" => params[:form_id], + "page.id" => params[:page_slug]&.match(Page::PAGE_ID_REGEX) ? params[:page_slug] : nil, + "page.slug" => params[:page_slug], + }) end def log_rescued_exception(exception) diff --git a/app/controllers/forms/base_controller.rb b/app/controllers/forms/base_controller.rb index a4b94046f..ff6a30621 100644 --- a/app/controllers/forms/base_controller.rb +++ b/app/controllers/forms/base_controller.rb @@ -23,6 +23,14 @@ def set_request_logging_attributes super CurrentRequestLoggingAttributes.form_name = @form.name CurrentRequestLoggingAttributes.preview = mode.preview? + + # Add form-level attributes to OpenTelemetry span + TelemetryService.set_request_attributes({ + "form.name" => @form.name, + "form.slug" => @form.form_slug, + "mode.type" => mode.to_s, + "mode.preview" => mode.preview?, + }) end private diff --git a/app/controllers/forms/page_controller.rb b/app/controllers/forms/page_controller.rb index 406256e3f..bff046854 100644 --- a/app/controllers/forms/page_controller.rb +++ b/app/controllers/forms/page_controller.rb @@ -6,6 +6,9 @@ def set_request_logging_attributes super CurrentRequestLoggingAttributes.question_number = @step.page_number if @step&.page_number CurrentRequestLoggingAttributes.answer_type = @step&.page&.answer_type if @step&.page&.answer_type + + # Add question-level attributes to OpenTelemetry span + TelemetryService.set_question_attributes(@step, @form) if @step && @form end def show diff --git a/app/lib/flow/context.rb b/app/lib/flow/context.rb index d447bf088..8e1196ab7 100644 --- a/app/lib/flow/context.rb +++ b/app/lib/flow/context.rb @@ -15,7 +15,15 @@ def initialize(form:, store:) delegate :save_submission_details, :get_submission_reference, :requested_email_confirmation?, :clear_submission_details, to: :confirmation_details_store def save_step(step, context: nil) - return false unless step.valid?(context) + is_valid = step.valid?(context) + + if is_valid + TelemetryService.record_validation_success + else + TelemetryService.record_validation_failure(step) + end + + return false unless is_valid step.save_to_store(@answer_store) end diff --git a/app/services/api/v2/form_document_repository.rb b/app/services/api/v2/form_document_repository.rb index 613c48384..71e9e159e 100644 --- a/app/services/api/v2/form_document_repository.rb +++ b/app/services/api/v2/form_document_repository.rb @@ -3,11 +3,26 @@ class << self def find(form_id:, tag:, language: :en) raise ActiveResource::ResourceNotFound.new(404, "Not Found") unless form_id.to_s =~ /^[[:alnum:]]+$/ - form_document = Api::V2::FormDocumentResource.get(form_id, tag, **options_for_language(language)) - form = Form.new(form_document, true) - form.document_json = form_document - form.prefix_options = { form_id:, tag: } - form + TelemetryService.trace("api.forms_admin.fetch_form", attributes: { + "api.endpoint" => "#{Settings.forms_api.base_url}/api/v2/form/#{form_id}/#{tag}", + "api.method" => "GET", + "form.id" => form_id.to_s, + "form.tag" => tag.to_s, + "form.language" => language.to_s, + }) do |span| + form_document = Api::V2::FormDocumentResource.get(form_id, tag, **options_for_language(language)) + + span.set_attribute("api.response.status", 200) + span.set_attribute("form.name", form_document.name) if form_document.respond_to?(:name) + + form = Form.new(form_document, true) + form.document_json = form_document + form.prefix_options = { form_id:, tag: } + form + end + rescue ActiveResource::ResourceNotFound => e + # Re-raise but let the span record the error + raise end def find_with_mode(form_id:, mode:, language: :en) diff --git a/app/services/form_submission_service.rb b/app/services/form_submission_service.rb index 446e0f1dd..e09679959 100644 --- a/app/services/form_submission_service.rb +++ b/app/services/form_submission_service.rb @@ -23,14 +23,22 @@ def initialize(current_context:, email_confirmation_input:, mode:) end def submit - ensure_form_english - validate_submission - - confirmation_mail = setup_confirmation_email if requested_confirmation? - deliver_submission - send_confirmation_email(confirmation_mail) if confirmation_mail.present? - - submission_reference + TelemetryService.trace("form.submission.process", attributes: { + "submission.type" => form.submission_type, + "submission.format" => form.submission_format, + "submission.reference" => submission_reference, + "form.id" => form.id.to_s, + "confirmation.requested" => requested_confirmation?, + }) do + ensure_form_english + validate_submission + + confirmation_mail = setup_confirmation_email if requested_confirmation? + deliver_submission + send_confirmation_email(confirmation_mail) if confirmation_mail.present? + + submission_reference + end end private @@ -71,31 +79,42 @@ def deliver_submission end def deliver_submission_via_s3 - s3_submission_service = S3SubmissionService.new( - journey: current_context.journey, - form: form, - timestamp: timestamp, - submission_reference: submission_reference, - is_preview: mode.preview?, - ) - - s3_submission_service.submit + TelemetryService.trace("form.submission.deliver_s3", attributes: { + "submission.reference" => submission_reference, + "submission.format" => form.submission_format, + "form.id" => form.id.to_s, + }) do + s3_submission_service = S3SubmissionService.new( + journey: current_context.journey, + form: form, + timestamp: timestamp, + submission_reference: submission_reference, + is_preview: mode.preview?, + ) + + s3_submission_service.submit + end end def deliver_submission_via_email - submission = Submission.create!( - reference: submission_reference, - form_id: form.id, - answers: current_context.answers, - mode: mode, - form_document: form.document_json, - ) - - SendSubmissionJob.perform_later(submission) do |job| - unless job.successfully_enqueued? - submission.destroy! - message_suffix = ": #{job.enqueue_error&.message}" if job.enqueue_error - raise StandardError, "Failed to enqueue submission for reference #{submission_reference}#{message_suffix}" + TelemetryService.trace("form.submission.deliver_email", attributes: { + "submission.reference" => submission_reference, + "form.id" => form.id.to_s, + }) do + submission = Submission.create!( + reference: submission_reference, + form_id: form.id, + answers: current_context.answers, + mode: mode, + form_document: form.document_json, + ) + + SendSubmissionJob.perform_later(submission) do |job| + unless job.successfully_enqueued? + submission.destroy! + message_suffix = ": #{job.enqueue_error&.message}" if job.enqueue_error + raise StandardError, "Failed to enqueue submission for reference #{submission_reference}#{message_suffix}" + end end end end diff --git a/app/services/s3_submission_service.rb b/app/services/s3_submission_service.rb index 42ea914ee..a23aafeb0 100644 --- a/app/services/s3_submission_service.rb +++ b/app/services/s3_submission_service.rb @@ -17,23 +17,36 @@ def submit raise StandardError, "S3 bucket account ID is not set on the form" if @form.s3_bucket_aws_account_id.nil? raise StandardError, "S3 bucket region is not set on the form" if @form.s3_bucket_region.nil? - # We send the uploaded files before the submissions CSV so that processors can have automations run when the CSV - # file arrives and the referenced files will already be present - copy_uploaded_files_to_bucket - - submission_content, key = - case @form.submission_format - when %w[csv] - [generate_csv_submission, generate_key("form_submission.csv")] - when %w[json] - [generate_json_submission, generate_key("form_submission.json")] - else - raise StandardError, "Unsupported submission format: #{@form.submission_format.inspect}" - end - - upload_submission_to_s3(submission_content, key) - - delete_uploaded_files_from_our_bucket + file_count = @journey.completed_file_upload_questions.count + + TelemetryService.trace("submission.s3.upload", attributes: { + "s3.bucket" => @form.s3_bucket_name, + "s3.region" => @form.s3_bucket_region, + "submission.format" => @form.submission_format.join(","), + "submission.reference" => @submission_reference, + "submission.file_count" => file_count, + }) do |span| + # We send the uploaded files before the submissions CSV so that processors can have automations run when the CSV + # file arrives and the referenced files will already be present + copy_uploaded_files_to_bucket + + submission_content, key = + case @form.submission_format + when %w[csv] + [generate_csv_submission, generate_key("form_submission.csv")] + when %w[json] + [generate_json_submission, generate_key("form_submission.json")] + else + raise StandardError, "Unsupported submission format: #{@form.submission_format.inspect}" + end + + span.set_attribute("submission.size_bytes", submission_content.bytesize) + span.set_attribute("s3.key", key) + + upload_submission_to_s3(submission_content, key) + + delete_uploaded_files_from_our_bucket + end end private diff --git a/app/services/telemetry_service.rb b/app/services/telemetry_service.rb new file mode 100644 index 000000000..e36cb85af --- /dev/null +++ b/app/services/telemetry_service.rb @@ -0,0 +1,117 @@ +class TelemetryService + # OpenTelemetry tracing service for adding span attributes + # Follows the same pattern as CloudWatchService and LogEventService + # + # We are heavily using attributes, not events because X-Ray does not support events + # see: https://github.com/aws-observability/aws-otel-collector/issues/821 + + # Set request-level attributes for journey tracking + # Call from ApplicationController to add form/session context to all spans + def self.set_request_attributes(attrs) + return unless defined?(OpenTelemetry) + + # Ensure all values are primitives (string, number, boolean, nil) + sanitized = attrs.compact.transform_values { |v| sanitize_attribute_value(v) } + current_span.add_attributes(sanitized.transform_keys(&:to_s)) + rescue StandardError => e + Sentry.capture_exception(e) if defined?(Sentry) + end + + # Set question-level attributes on page requests + # Call from PageController to add question context to all page spans + def self.set_question_attributes(step, form) + return unless defined?(OpenTelemetry) + + attrs = { + "question.type" => step.question.class.name, + "question.id" => step.page_id, + "question.text" => step.question_text, + "question.answer_type" => step.page&.answer_type, + "question.number" => step.page_number, + "question.is_optional" => step.question.is_optional?, + "question.is_repeatable" => step.repeatable?, + "form.submission_type" => form.submission_type, + }.compact + + sanitized = attrs.transform_values { |v| sanitize_attribute_value(v) } + current_span.add_attributes(sanitized) + rescue StandardError => e + Sentry.capture_exception(e) if defined?(Sentry) + end + + def self.record_validation_failure(step) + return unless defined?(OpenTelemetry) + + attrs = { + "validation.failed" => true, + "validation.error_count" => step.question.errors.count, + "validation.errors" => step.question.errors.full_messages.join(", "), + "validation.error_attributes" => step.question.errors.attribute_names.map(&:to_s).join(", "), + } + + sanitized = attrs.transform_values { |v| sanitize_attribute_value(v) } + current_span.add_attributes(sanitized) + rescue StandardError => e + # Silently fail - don't break the app if telemetry has issues + Sentry.capture_exception(e) if defined?(Sentry) + end + + def self.record_validation_success + return unless defined?(OpenTelemetry) + + current_span.set_attribute("validation.passed", true) + rescue StandardError => e + Sentry.capture_exception(e) if defined?(Sentry) + end + + # Create a custom span for wrapping important operations + # Usage: TelemetryService.trace('operation.name', attributes: {...}) { ... } + def self.trace(span_name, attributes: {}, &block) + return yield(NoOpSpan.new) unless defined?(OpenTelemetry) + + # Get tracer + tracer = OpenTelemetry.tracer_provider.tracer("forms-runner") + + # Sanitize attributes to ensure they're primitives + sanitized = attributes.compact.transform_values { |v| sanitize_attribute_value(v) } + + tracer.in_span(span_name, attributes: sanitized, &block) + rescue StandardError => e + Sentry.capture_exception(e) if defined?(Sentry) + # If tracing fails, still execute the block with a no-op span + # This ensures business logic runs even if telemetry breaks + yield(NoOpSpan.new) + end + + def self.current_span + OpenTelemetry::Trace.current_span + end + private_class_method :current_span + + # Sanitize attribute values to ensure they're primitives (String, Integer, Float, Boolean) + # OpenTelemetry requires attribute values to be primitives, not complex objects + def self.sanitize_attribute_value(value) + case value + when String, Integer, Float, TrueClass, FalseClass, NilClass + value + when Array + value.join(", ") + else + value.to_s + end + end + private_class_method :sanitize_attribute_value + + # No-op span that safely ignores all method calls + # Used as a fallback when tracing is disabled or fails + class NoOpSpan + def method_missing(_method_name, *_args, **_kwargs, &_block) + # Silently ignore all method calls (set_attribute, add_event, etc.) + nil + end + + def respond_to_missing?(_method_name, _include_private = false) + true + end + end +end diff --git a/config/initializers/opentelemetry.rb b/config/initializers/opentelemetry.rb new file mode 100644 index 000000000..68e611742 --- /dev/null +++ b/config/initializers/opentelemetry.rb @@ -0,0 +1,12 @@ +require "opentelemetry/sdk" +require "opentelemetry/instrumentation/all" + +return unless ENV["ENABLE_OTEL"] == "true" + +OpenTelemetry::SDK.configure do |c| + instrumentation_config = { "OpenTelemetry::Instrumentation::Rack" => { untraced_endpoints: ["/up"] } } + c.use_all(instrumentation_config) + + # 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