diff --git a/Gemfile b/Gemfile index d41b122..74f8f43 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,10 @@ gem "bootsnap", require: false # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible # gem "rack-cors" +gem "opentelemetry-sdk", "~> 1.4" +gem "opentelemetry-exporter-otlp" +gem "opentelemetry-instrumentation-all", "~> 0.60.0" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ] diff --git a/Gemfile.lock b/Gemfile.lock index adc7e8a..2b23152 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,6 +75,8 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) base64 (0.2.0) bigdecimal (3.1.7) bootsnap (1.18.3) @@ -82,15 +84,28 @@ GEM builder (3.2.4) concurrent-ruby (1.2.3) connection_pool (2.4.1) + crack (1.0.0) + bigdecimal + rexml crass (1.0.6) date (3.3.4) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.5.1) drb (2.2.1) erubi (1.12.0) globalid (1.2.1) activesupport (>= 6.1) + google-protobuf (3.25.3) + google-protobuf (3.25.3-aarch64-linux) + google-protobuf (3.25.3-arm64-darwin) + google-protobuf (3.25.3-x86-linux) + google-protobuf (3.25.3-x86_64-darwin) + google-protobuf (3.25.3-x86_64-linux) + googleapis-common-protos-types (1.14.0) + google-protobuf (~> 3.18) + hashdiff (1.1.0) i18n (1.14.4) concurrent-ruby (~> 1.0) io-console (0.7.2) @@ -132,8 +147,212 @@ GEM racc (~> 1.4) nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) + opentelemetry-api (1.2.5) + opentelemetry-common (0.20.1) + opentelemetry-api (~> 1.0) + opentelemetry-exporter-otlp (0.26.3) + google-protobuf (~> 3.14) + googleapis-common-protos-types (~> 1.3) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-sdk (~> 1.2) + opentelemetry-semantic_conventions + opentelemetry-helpers-mysql (0.1.0) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20) + opentelemetry-helpers-sql-obfuscation (0.1.0) + opentelemetry-common (~> 0.20) + opentelemetry-instrumentation-action_pack (0.9.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rack (~> 0.21) + opentelemetry-instrumentation-action_view (0.7.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-active_support (~> 0.1) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_job (0.7.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_model_serializers (0.20.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_record (0.7.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_support (0.5.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-all (0.60.0) + opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) + opentelemetry-instrumentation-aws_sdk (~> 0.5.0) + opentelemetry-instrumentation-bunny (~> 0.21.0) + opentelemetry-instrumentation-concurrent_ruby (~> 0.21.1) + opentelemetry-instrumentation-dalli (~> 0.25.0) + opentelemetry-instrumentation-delayed_job (~> 0.22.0) + opentelemetry-instrumentation-ethon (~> 0.21.1) + opentelemetry-instrumentation-excon (~> 0.22.0) + opentelemetry-instrumentation-faraday (~> 0.24.0) + opentelemetry-instrumentation-grape (~> 0.1.3) + opentelemetry-instrumentation-graphql (~> 0.28.0) + opentelemetry-instrumentation-gruf (~> 0.2.0) + opentelemetry-instrumentation-http (~> 0.23.1) + opentelemetry-instrumentation-http_client (~> 0.22.1) + opentelemetry-instrumentation-koala (~> 0.20.1) + opentelemetry-instrumentation-lmdb (~> 0.22.1) + opentelemetry-instrumentation-mongo (~> 0.22.1) + opentelemetry-instrumentation-mysql2 (~> 0.27.0) + opentelemetry-instrumentation-net_http (~> 0.22.1) + opentelemetry-instrumentation-pg (~> 0.27.0) + opentelemetry-instrumentation-que (~> 0.8.0) + opentelemetry-instrumentation-racecar (~> 0.3.0) + opentelemetry-instrumentation-rack (~> 0.24.0) + opentelemetry-instrumentation-rails (~> 0.30.0) + opentelemetry-instrumentation-rake (~> 0.2.1) + opentelemetry-instrumentation-rdkafka (~> 0.4.0) + opentelemetry-instrumentation-redis (~> 0.25.1) + opentelemetry-instrumentation-resque (~> 0.5.0) + opentelemetry-instrumentation-restclient (~> 0.22.1) + opentelemetry-instrumentation-ruby_kafka (~> 0.21.0) + opentelemetry-instrumentation-sidekiq (~> 0.25.0) + opentelemetry-instrumentation-sinatra (~> 0.23.1) + opentelemetry-instrumentation-trilogy (~> 0.59.0) + opentelemetry-instrumentation-aws_sdk (0.5.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-base (0.22.3) + opentelemetry-api (~> 1.0) + opentelemetry-registry (~> 0.1) + opentelemetry-instrumentation-bunny (0.21.3) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-concurrent_ruby (0.21.3) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-dalli (0.25.1) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-delayed_job (0.22.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-ethon (0.21.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-excon (0.22.1) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-faraday (0.24.2) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-grape (0.1.7) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rack (~> 0.21) + opentelemetry-instrumentation-graphql (0.28.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-gruf (0.2.1) + opentelemetry-api (>= 1.0.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-http (0.23.3) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-http_client (0.22.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-koala (0.20.3) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-lmdb (0.22.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-mongo (0.22.3) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-mysql2 (0.27.1) + opentelemetry-api (~> 1.0) + opentelemetry-helpers-mysql + opentelemetry-helpers-sql-obfuscation + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-net_http (0.22.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-pg (0.27.2) + opentelemetry-api (~> 1.0) + opentelemetry-helpers-sql-obfuscation + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-que (0.8.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-racecar (0.3.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rack (0.24.2) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rails (0.30.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-action_pack (~> 0.9.0) + opentelemetry-instrumentation-action_view (~> 0.7.0) + opentelemetry-instrumentation-active_job (~> 0.7.0) + opentelemetry-instrumentation-active_record (~> 0.7.0) + opentelemetry-instrumentation-active_support (~> 0.5.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rake (0.2.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rdkafka (0.4.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-redis (0.25.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-resque (0.5.2) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-restclient (0.22.4) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-ruby_kafka (0.21.1) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-sidekiq (0.25.3) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-sinatra (0.23.3) + opentelemetry-api (~> 1.0) + opentelemetry-common (~> 0.20.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rack (~> 0.21) + opentelemetry-instrumentation-trilogy (0.59.3) + opentelemetry-api (~> 1.0) + opentelemetry-helpers-mysql + opentelemetry-helpers-sql-obfuscation + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-semantic_conventions (>= 1.8.0) + opentelemetry-registry (0.3.1) + opentelemetry-api (~> 1.1) + opentelemetry-sdk (1.4.1) + opentelemetry-api (~> 1.1) + opentelemetry-common (~> 0.20) + opentelemetry-registry (~> 0.2) + opentelemetry-semantic_conventions + opentelemetry-semantic_conventions (1.10.0) + opentelemetry-api (~> 1.0) psych (5.1.2) stringio + public_suffix (5.0.5) puma (6.4.2) nio4r (~> 2.0) racc (1.7.3) @@ -179,6 +398,24 @@ GEM psych (>= 4.0.0) reline (0.5.5) io-console (~> 0.5) + rexml (3.2.6) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) sqlite3 (1.7.3-aarch64-linux) sqlite3 (1.7.3-arm-linux) sqlite3 (1.7.3-arm64-darwin) @@ -190,6 +427,11 @@ GEM timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + vcr (6.2.0) + webmock (3.23.0) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) @@ -207,10 +449,16 @@ PLATFORMS DEPENDENCIES bootsnap debug + opentelemetry-exporter-otlp + opentelemetry-instrumentation-all (~> 0.60.0) + opentelemetry-sdk (~> 1.4) puma (>= 5.0) rails (~> 7.1.3, >= 7.1.3.2) + rspec-rails (~> 6.1.0) sqlite3 (~> 1.4) tzinfo-data + vcr + webmock RUBY VERSION ruby 3.3.1p55 diff --git a/config/application.rb b/config/application.rb index cf96179..f006171 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,5 @@ require_relative "boot" +require_relative "../lib/middleware/performance_monitor" require "rails" # Pick the frameworks you want: @@ -33,12 +34,14 @@ class Application < Rails::Application # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # - # config.time_zone = "Central Time (US & Canada)" + config.time_zone = "Japan" # config.eager_load_paths << Rails.root.join("extras") # Only loads a smaller set of middleware suitable for API only apps. # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true + + config.middleware.use PerformanceMonitor end end diff --git a/config/initializers/opentelemetry.rb b/config/initializers/opentelemetry.rb new file mode 100644 index 0000000..4007baf --- /dev/null +++ b/config/initializers/opentelemetry.rb @@ -0,0 +1,9 @@ +# REF: https://opentelemetry.io/docs/languages/ruby/getting-started/ + +require 'opentelemetry/sdk' +require 'opentelemetry/instrumentation/all' + +OpenTelemetry::SDK.configure do |c| + c.service_name = 'tablecheck-customer-reliability-take-home' + c.use_all() # enables all instrumentation! +end \ No newline at end of file diff --git a/lib/middleware/performance_monitor.rb b/lib/middleware/performance_monitor.rb new file mode 100644 index 0000000..5490b56 --- /dev/null +++ b/lib/middleware/performance_monitor.rb @@ -0,0 +1,56 @@ +class PerformanceMonitor + def initialize(app) + @app = app + @response_times = [] + @request_timestamps = [] + end + + def call(env) + request = Rack::Request.new(env) + + pattern = %r{/api/v1/weather/\S+} + if request.path.match?(pattern) + status = headers = response = nil + + trace_average_response_time { status, headers, response = @app.call(env) } + trace_requests_per_minute + + [status, headers, response] + else + @app.call(env) + end + end + + private + + # Note: The following does not actually trace the average server response time. It traces: + # - the time spent in ruby to send the request + # - the network time of the request + # - the server's response time + # - the network time of the response + # - the time spent in ruby to process the response + # If the actual average server response time is required, another method of doing so will + # need to be investigated (response headers?). + def trace_average_response_time(&block) + tracer = OpenTelemetry.tracer_provider.tracer("tablecheck-customer-reliability-take-home") + tracer.in_span("api_requests") do |span| + start_time = Time.current + yield + end_time = Time.current + duration = end_time - start_time + @response_times << duration + average_response_time = @response_times.sum / @response_times.length + span.set_attribute('api_requests.average_response_time', average_response_time) + end + end + + def trace_requests_per_minute + tracer = OpenTelemetry.tracer_provider.tracer("tablecheck-customer-reliability-take-home") + tracer.in_span("api_requests") do |span| + @request_timestamps << Time.current + @request_timestamps.reject! { |timestamp| timestamp < (Time.current - 60) } + requests_per_minute = @request_timestamps.length + span.set_attribute('api_requests.requests_per_minute', requests_per_minute) + end + end +end \ No newline at end of file