QueryWise 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +45 -0
  3. data/CLOUD_RUN_README.md +263 -0
  4. data/DOCKER_README.md +327 -0
  5. data/Dockerfile +69 -0
  6. data/Dockerfile.cloudrun +76 -0
  7. data/Dockerfile.dev +36 -0
  8. data/GEM_Gemfile +16 -0
  9. data/GEM_README.md +421 -0
  10. data/GEM_Rakefile +10 -0
  11. data/GEM_gitignore +137 -0
  12. data/LICENSE.txt +21 -0
  13. data/PUBLISHING_GUIDE.md +269 -0
  14. data/README.md +392 -0
  15. data/app/controllers/api/v1/analysis_controller.rb +340 -0
  16. data/app/controllers/api/v1/api_keys_controller.rb +83 -0
  17. data/app/controllers/api/v1/base_controller.rb +93 -0
  18. data/app/controllers/api/v1/health_controller.rb +86 -0
  19. data/app/controllers/application_controller.rb +2 -0
  20. data/app/controllers/concerns/.keep +0 -0
  21. data/app/jobs/application_job.rb +7 -0
  22. data/app/mailers/application_mailer.rb +4 -0
  23. data/app/models/app_profile.rb +18 -0
  24. data/app/models/application_record.rb +3 -0
  25. data/app/models/concerns/.keep +0 -0
  26. data/app/models/optimization_suggestion.rb +44 -0
  27. data/app/models/query_analysis.rb +47 -0
  28. data/app/models/query_pattern.rb +55 -0
  29. data/app/services/missing_index_detector_service.rb +244 -0
  30. data/app/services/n_plus_one_detector_service.rb +177 -0
  31. data/app/services/slow_query_analyzer_service.rb +225 -0
  32. data/app/services/sql_parser_service.rb +352 -0
  33. data/app/validators/query_data_validator.rb +96 -0
  34. data/app/views/layouts/mailer.html.erb +13 -0
  35. data/app/views/layouts/mailer.text.erb +1 -0
  36. data/app.yaml +109 -0
  37. data/cloudbuild.yaml +47 -0
  38. data/config/application.rb +32 -0
  39. data/config/boot.rb +4 -0
  40. data/config/cable.yml +17 -0
  41. data/config/cache.yml +16 -0
  42. data/config/credentials.yml.enc +1 -0
  43. data/config/database.yml +69 -0
  44. data/config/deploy.yml +116 -0
  45. data/config/environment.rb +5 -0
  46. data/config/environments/development.rb +70 -0
  47. data/config/environments/production.rb +87 -0
  48. data/config/environments/test.rb +53 -0
  49. data/config/initializers/cors.rb +16 -0
  50. data/config/initializers/filter_parameter_logging.rb +8 -0
  51. data/config/initializers/inflections.rb +16 -0
  52. data/config/locales/en.yml +31 -0
  53. data/config/master.key +1 -0
  54. data/config/puma.rb +41 -0
  55. data/config/puma_cloudrun.rb +48 -0
  56. data/config/queue.yml +18 -0
  57. data/config/recurring.yml +15 -0
  58. data/config/routes.rb +28 -0
  59. data/config/storage.yml +34 -0
  60. data/config.ru +6 -0
  61. data/db/cable_schema.rb +11 -0
  62. data/db/cache_schema.rb +14 -0
  63. data/db/migrate/20250818214709_create_app_profiles.rb +13 -0
  64. data/db/migrate/20250818214731_create_query_analyses.rb +22 -0
  65. data/db/migrate/20250818214740_create_query_patterns.rb +22 -0
  66. data/db/migrate/20250818214805_create_optimization_suggestions.rb +20 -0
  67. data/db/queue_schema.rb +129 -0
  68. data/db/schema.rb +79 -0
  69. data/db/seeds.rb +9 -0
  70. data/init.sql +9 -0
  71. data/lib/query_optimizer_client/client.rb +176 -0
  72. data/lib/query_optimizer_client/configuration.rb +43 -0
  73. data/lib/query_optimizer_client/generators/install_generator.rb +43 -0
  74. data/lib/query_optimizer_client/generators/templates/README +46 -0
  75. data/lib/query_optimizer_client/generators/templates/analysis_job.rb +84 -0
  76. data/lib/query_optimizer_client/generators/templates/initializer.rb +30 -0
  77. data/lib/query_optimizer_client/middleware.rb +126 -0
  78. data/lib/query_optimizer_client/railtie.rb +37 -0
  79. data/lib/query_optimizer_client/tasks.rake +228 -0
  80. data/lib/query_optimizer_client/version.rb +5 -0
  81. data/lib/query_optimizer_client.rb +48 -0
  82. data/lib/tasks/.keep +0 -0
  83. data/public/robots.txt +1 -0
  84. data/query_optimizer_client.gemspec +60 -0
  85. data/script/.keep +0 -0
  86. data/storage/.keep +0 -0
  87. data/storage/development.sqlite3 +0 -0
  88. data/storage/test.sqlite3 +0 -0
  89. data/vendor/.keep +0 -0
  90. metadata +265 -0
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'json'
5
+
6
+ module QueryOptimizerClient
7
+ class Client
8
+ include HTTParty
9
+
10
+ def initialize(config)
11
+ @config = config
12
+ @config.validate!
13
+
14
+ self.class.base_uri(@config.api_url)
15
+ self.class.default_timeout(@config.timeout)
16
+ end
17
+
18
+ def analyze_queries(queries)
19
+ return nil unless @config.enabled?
20
+
21
+ validate_queries!(queries)
22
+
23
+ with_retry do
24
+ response = self.class.post('/analyze',
25
+ headers: headers,
26
+ body: { queries: format_queries(queries) }.to_json
27
+ )
28
+
29
+ handle_response(response)
30
+ end
31
+ end
32
+
33
+ def analyze_for_ci(queries, threshold: nil)
34
+ return { 'data' => { 'passed' => true, 'score' => 100 } } unless @config.enabled?
35
+
36
+ threshold ||= @config.default_threshold
37
+ validate_queries!(queries)
38
+ validate_threshold!(threshold)
39
+
40
+ with_retry do
41
+ response = self.class.post('/analyze_ci',
42
+ headers: headers,
43
+ body: {
44
+ queries: format_queries(queries),
45
+ threshold_score: threshold
46
+ }.to_json
47
+ )
48
+
49
+ handle_response(response)
50
+ end
51
+ end
52
+
53
+ def health_check
54
+ response = self.class.get('/health', headers: basic_headers)
55
+ handle_response(response)
56
+ end
57
+
58
+ def create_api_key(app_name)
59
+ response = self.class.post('/api_keys',
60
+ headers: basic_headers,
61
+ body: { app_name: app_name }.to_json
62
+ )
63
+
64
+ handle_response(response)
65
+ end
66
+
67
+ private
68
+
69
+ def headers
70
+ basic_headers.merge('X-API-Key' => @config.api_key)
71
+ end
72
+
73
+ def basic_headers
74
+ { 'Content-Type' => 'application/json' }
75
+ end
76
+
77
+ def format_queries(queries)
78
+ queries.map do |query|
79
+ {
80
+ sql: extract_sql(query),
81
+ duration_ms: extract_duration(query)
82
+ }.compact
83
+ end
84
+ end
85
+
86
+ def extract_sql(query)
87
+ case query
88
+ when Hash
89
+ query[:sql] || query['sql']
90
+ when String
91
+ query
92
+ else
93
+ query.to_s
94
+ end
95
+ end
96
+
97
+ def extract_duration(query)
98
+ return nil unless query.is_a?(Hash)
99
+
100
+ duration = query[:duration_ms] || query['duration_ms']
101
+ duration&.to_f&.round(2)
102
+ end
103
+
104
+ def validate_queries!(queries)
105
+ raise ValidationError, "Queries must be an array" unless queries.is_a?(Array)
106
+ raise ValidationError, "At least one query is required" if queries.empty?
107
+ raise ValidationError, "Maximum #{@config.batch_size} queries allowed" if queries.length > @config.batch_size
108
+
109
+ queries.each_with_index do |query, index|
110
+ sql = extract_sql(query)
111
+ raise ValidationError, "Query #{index + 1}: SQL is required" if sql.blank?
112
+ raise ValidationError, "Query #{index + 1}: SQL too long (max 10000 chars)" if sql.length > 10000
113
+ end
114
+ end
115
+
116
+ def validate_threshold!(threshold)
117
+ unless threshold.is_a?(Numeric) && threshold >= 0 && threshold <= 100
118
+ raise ValidationError, "Threshold must be a number between 0 and 100"
119
+ end
120
+ end
121
+
122
+ def handle_response(response)
123
+ case response.code
124
+ when 200
125
+ JSON.parse(response.body)
126
+ when 400
127
+ raise ValidationError, parse_error_message(response)
128
+ when 401
129
+ raise AuthenticationError, "Invalid API key"
130
+ when 429
131
+ raise RateLimitError, "Rate limit exceeded"
132
+ when 500..599
133
+ raise APIError, "Server error: #{response.code}"
134
+ else
135
+ raise APIError, "Unexpected response: #{response.code}"
136
+ end
137
+ rescue JSON::ParserError => e
138
+ raise APIError, "Invalid JSON response: #{e.message}"
139
+ end
140
+
141
+ def parse_error_message(response)
142
+ body = JSON.parse(response.body)
143
+ body['error'] || body['errors']&.join(', ') || 'Unknown error'
144
+ rescue JSON::ParserError
145
+ "HTTP #{response.code}: #{response.body}"
146
+ end
147
+
148
+ def with_retry(&block)
149
+ retries = 0
150
+
151
+ begin
152
+ yield
153
+ rescue RateLimitError => e
154
+ if @config.rate_limit_retry && retries < @config.retries
155
+ wait_time = 2 ** retries
156
+ @config.logger&.warn("Rate limited, waiting #{wait_time} seconds...")
157
+ sleep(wait_time)
158
+ retries += 1
159
+ retry
160
+ else
161
+ raise e
162
+ end
163
+ rescue Net::TimeoutError, Errno::ECONNREFUSED => e
164
+ if retries < @config.retries
165
+ wait_time = 2 ** retries
166
+ @config.logger&.warn("Connection error, retrying in #{wait_time} seconds...")
167
+ sleep(wait_time)
168
+ retries += 1
169
+ retry
170
+ else
171
+ raise APIError, "Connection failed after #{@config.retries} retries: #{e.message}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryOptimizerClient
4
+ class Configuration
5
+ attr_accessor :api_url, :api_key, :enabled, :timeout, :retries, :logger,
6
+ :rate_limit_retry, :default_threshold, :batch_size
7
+
8
+ def initialize
9
+ @api_url = ENV.fetch('QUERY_OPTIMIZER_API_URL', 'http://localhost:3000/api/v1')
10
+ @api_key = ENV['QUERY_OPTIMIZER_API_KEY']
11
+ @enabled = ENV.fetch('QUERY_OPTIMIZER_ENABLED', 'false') == 'true'
12
+ @timeout = ENV.fetch('QUERY_OPTIMIZER_TIMEOUT', '30').to_i
13
+ @retries = ENV.fetch('QUERY_OPTIMIZER_RETRIES', '3').to_i
14
+ @rate_limit_retry = ENV.fetch('QUERY_OPTIMIZER_RATE_LIMIT_RETRY', 'true') == 'true'
15
+ @default_threshold = ENV.fetch('QUERY_OPTIMIZER_THRESHOLD', '80').to_i
16
+ @batch_size = ENV.fetch('QUERY_OPTIMIZER_BATCH_SIZE', '50').to_i
17
+ @logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
18
+ end
19
+
20
+ def enabled?
21
+ @enabled && @api_key.present?
22
+ end
23
+
24
+ def valid?
25
+ api_key.present? && api_url.present?
26
+ end
27
+
28
+ def validate!
29
+ raise ValidationError, "API key is required" unless api_key.present?
30
+ raise ValidationError, "API URL is required" unless api_url.present?
31
+ raise ValidationError, "Invalid API URL format" unless valid_url?(api_url)
32
+ end
33
+
34
+ private
35
+
36
+ def valid_url?(url)
37
+ uri = URI.parse(url)
38
+ uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
39
+ rescue URI::InvalidURIError
40
+ false
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module QueryOptimizerClient
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ desc "Install QueryWise"
9
+
10
+ source_root File.expand_path('templates', __dir__)
11
+
12
+ def create_initializer
13
+ template 'initializer.rb', 'config/initializers/query_optimizer_client.rb'
14
+ end
15
+
16
+ def create_job
17
+ template 'analysis_job.rb', 'app/jobs/query_optimizer_client/analysis_job.rb'
18
+ end
19
+
20
+ def add_environment_variables
21
+ environment_template = <<~ENV
22
+ # Query Optimizer Client Configuration
23
+ # QUERY_OPTIMIZER_API_URL=http://localhost:3000/api/v1
24
+ # QUERY_OPTIMIZER_API_KEY=your_api_key_here
25
+ # QUERY_OPTIMIZER_ENABLED=true
26
+ # QUERY_OPTIMIZER_THRESHOLD=80
27
+ ENV
28
+
29
+ append_to_file '.env.example', environment_template
30
+
31
+ if File.exist?('.env')
32
+ append_to_file '.env', environment_template
33
+ else
34
+ create_file '.env', environment_template
35
+ end
36
+ end
37
+
38
+ def show_readme
39
+ readme 'README'
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ ===============================================================================
2
+
3
+ QueryWise has been installed!
4
+
5
+ ===============================================================================
6
+
7
+ Next steps:
8
+
9
+ 1. Set up your environment variables:
10
+
11
+ Add to your .env file:
12
+ QUERY_OPTIMIZER_API_URL=http://localhost:3000/api/v1
13
+ QUERY_OPTIMIZER_API_KEY=your_api_key_here
14
+ QUERY_OPTIMIZER_ENABLED=true
15
+
16
+ 2. Generate an API key (if you don't have one):
17
+
18
+ rails query_optimizer:generate_key["My App Name"]
19
+
20
+ 3. Test your configuration:
21
+
22
+ rails query_optimizer:check
23
+
24
+ 4. Run a sample analysis:
25
+
26
+ rails query_optimizer:analyze
27
+
28
+ 5. Set up CI/CD integration:
29
+
30
+ Add to your CI pipeline:
31
+ rails query_optimizer:ci[85]
32
+
33
+ 6. Customize the configuration in:
34
+
35
+ config/initializers/query_optimizer_client.rb
36
+
37
+ 7. Optional: Customize the analysis job in:
38
+
39
+ app/jobs/query_optimizer_client/analysis_job.rb
40
+
41
+ ===============================================================================
42
+
43
+ For more information, visit:
44
+ https://github.com/yourusername/query_optimizer_client
45
+
46
+ ===============================================================================
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryOptimizerClient
4
+ class AnalysisJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(queries, endpoint = nil)
8
+ return unless QueryOptimizerClient.enabled?
9
+
10
+ result = QueryOptimizerClient.analyze_queries(queries)
11
+
12
+ return unless result&.dig('success')
13
+
14
+ data = result['data']
15
+ score = data['summary']['optimization_score']
16
+
17
+ # Log analysis results
18
+ log_analysis_results(data, endpoint)
19
+
20
+ # Store results (optional - implement based on your needs)
21
+ store_analysis_results(data, endpoint) if respond_to?(:store_analysis_results, true)
22
+
23
+ # Send alerts if performance is poor
24
+ send_alerts(data, endpoint, score) if score < 70
25
+
26
+ rescue => e
27
+ Rails.logger.error "Query analysis failed: #{e.message}"
28
+ Rails.logger.error e.backtrace.join("\n") if Rails.env.development?
29
+ end
30
+
31
+ private
32
+
33
+ def log_analysis_results(data, endpoint)
34
+ endpoint_info = endpoint ? " on #{endpoint}" : ""
35
+
36
+ Rails.logger.info "Query analysis completed#{endpoint_info}: Score #{data['summary']['optimization_score']}%, #{data['summary']['issues_found']} issues found"
37
+
38
+ # Log N+1 queries
39
+ if data['n_plus_one']['detected']
40
+ Rails.logger.warn "N+1 queries detected#{endpoint_info}:"
41
+ data['n_plus_one']['patterns'].each do |pattern|
42
+ Rails.logger.warn " - #{pattern['table']}.#{pattern['column']}: #{pattern['suggestion']}"
43
+ end
44
+ end
45
+
46
+ # Log slow queries
47
+ data['slow_queries'].each do |query|
48
+ Rails.logger.warn "Slow query#{endpoint_info} (#{query['duration_ms']}ms): #{query['sql'][0..100]}..."
49
+ query['suggestions'].each do |suggestion|
50
+ Rails.logger.warn " 💡 #{suggestion}"
51
+ end
52
+ end
53
+
54
+ # Log missing indexes
55
+ data['missing_indexes'].each do |index|
56
+ Rails.logger.info "Missing index suggestion#{endpoint_info}: #{index['sql']}"
57
+ end
58
+ end
59
+
60
+ def send_alerts(data, endpoint, score)
61
+ endpoint_info = endpoint ? " on #{endpoint}" : ""
62
+
63
+ if score < 50
64
+ Rails.logger.error "🚨 CRITICAL: Very low performance score (#{score}%)#{endpoint_info}"
65
+ # Add your critical alerting logic here (Slack, email, etc.)
66
+ elsif score < 70
67
+ Rails.logger.warn "⚠️ WARNING: Low performance score (#{score}%)#{endpoint_info}"
68
+ # Add your warning alerting logic here
69
+ end
70
+ end
71
+
72
+ # Optional: Implement this method to store analysis results in your database
73
+ # def store_analysis_results(data, endpoint)
74
+ # PerformanceAnalysis.create!(
75
+ # endpoint: endpoint,
76
+ # optimization_score: data['summary']['optimization_score'],
77
+ # issues_found: data['summary']['issues_found'],
78
+ # total_queries: data['summary']['total_queries'],
79
+ # analysis_data: data,
80
+ # analyzed_at: Time.current
81
+ # )
82
+ # end
83
+ end
84
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ QueryOptimizerClient.configure do |config|
4
+ # API Configuration
5
+ config.api_url = ENV.fetch('QUERY_OPTIMIZER_API_URL', 'http://localhost:3000/api/v1')
6
+ config.api_key = ENV['QUERY_OPTIMIZER_API_KEY']
7
+ config.enabled = ENV.fetch('QUERY_OPTIMIZER_ENABLED', 'false') == 'true'
8
+
9
+ # Request Configuration
10
+ config.timeout = ENV.fetch('QUERY_OPTIMIZER_TIMEOUT', '30').to_i
11
+ config.retries = ENV.fetch('QUERY_OPTIMIZER_RETRIES', '3').to_i
12
+ config.batch_size = ENV.fetch('QUERY_OPTIMIZER_BATCH_SIZE', '50').to_i
13
+
14
+ # Analysis Configuration
15
+ config.default_threshold = ENV.fetch('QUERY_OPTIMIZER_THRESHOLD', '80').to_i
16
+ config.rate_limit_retry = ENV.fetch('QUERY_OPTIMIZER_RATE_LIMIT_RETRY', 'true') == 'true'
17
+
18
+ # Logging
19
+ config.logger = Rails.logger
20
+ end
21
+
22
+ # Optional: Configure Rails-specific settings
23
+ Rails.application.configure do
24
+ # Middleware configuration
25
+ config.query_optimizer_client.min_queries = 3
26
+ config.query_optimizer_client.min_duration = 10
27
+ config.query_optimizer_client.skip_paths = ['/assets', '/health', '/api']
28
+ config.query_optimizer_client.async = true
29
+ config.query_optimizer_client.job_class = 'QueryOptimizerClient::AnalysisJob'
30
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QueryOptimizerClient
4
+ class Middleware
5
+ def initialize(app, options = {})
6
+ @app = app
7
+ @options = default_options.merge(options)
8
+ @client = QueryOptimizerClient.client
9
+ end
10
+
11
+ def call(env)
12
+ return @app.call(env) unless should_monitor?(env)
13
+
14
+ queries = []
15
+
16
+ subscription = ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
17
+ duration = (finish - start) * 1000
18
+
19
+ next if skip_query?(payload, duration)
20
+
21
+ queries << {
22
+ sql: payload[:sql],
23
+ duration_ms: duration.round(2)
24
+ }
25
+ end
26
+
27
+ status, headers, response = @app.call(env)
28
+
29
+ # Analyze queries asynchronously if we have enough
30
+ if queries.length >= @options[:min_queries]
31
+ analyze_queries_async(queries, extract_endpoint(env))
32
+ end
33
+
34
+ [status, headers, response]
35
+ ensure
36
+ ActiveSupport::Notifications.unsubscribe(subscription) if subscription
37
+ end
38
+
39
+ private
40
+
41
+ def default_options
42
+ {
43
+ min_queries: 3,
44
+ min_duration: 10,
45
+ skip_paths: ['/assets', '/health', '/api'],
46
+ skip_methods: ['OPTIONS', 'HEAD'],
47
+ async: true,
48
+ job_class: 'QueryOptimizerClient::AnalysisJob'
49
+ }
50
+ end
51
+
52
+ def should_monitor?(env)
53
+ return false unless QueryOptimizerClient.enabled?
54
+
55
+ request = Rack::Request.new(env)
56
+
57
+ # Skip certain paths
58
+ return false if @options[:skip_paths].any? { |path| request.path.start_with?(path) }
59
+
60
+ # Skip certain methods
61
+ return false if @options[:skip_methods].include?(request.request_method)
62
+
63
+ true
64
+ end
65
+
66
+ def skip_query?(payload, duration)
67
+ # Skip schema queries, cache queries, and very fast queries
68
+ payload[:name] =~ /SCHEMA|CACHE/ || duration < @options[:min_duration]
69
+ end
70
+
71
+ def extract_endpoint(env)
72
+ request = Rack::Request.new(env)
73
+ "#{request.request_method} #{request.path}"
74
+ end
75
+
76
+ def analyze_queries_async(queries, endpoint)
77
+ if @options[:async] && defined?(ActiveJob)
78
+ job_class = @options[:job_class].constantize
79
+ job_class.perform_later(queries, endpoint)
80
+ else
81
+ analyze_queries_sync(queries, endpoint)
82
+ end
83
+ rescue NameError
84
+ # Fallback to sync if job class doesn't exist
85
+ analyze_queries_sync(queries, endpoint)
86
+ end
87
+
88
+ def analyze_queries_sync(queries, endpoint)
89
+ result = @client.analyze_queries(queries)
90
+
91
+ if result&.dig('success')
92
+ handle_analysis_result(result['data'], endpoint)
93
+ end
94
+ rescue => e
95
+ QueryOptimizerClient.configuration.logger&.error("Query analysis failed: #{e.message}")
96
+ end
97
+
98
+ def handle_analysis_result(data, endpoint)
99
+ score = data['summary']['optimization_score']
100
+
101
+ # Log issues
102
+ if data['n_plus_one']['detected']
103
+ QueryOptimizerClient.configuration.logger&.warn(
104
+ "N+1 queries detected on #{endpoint}: #{data['n_plus_one']['patterns'].length} patterns"
105
+ )
106
+ end
107
+
108
+ data['slow_queries'].each do |query|
109
+ QueryOptimizerClient.configuration.logger&.warn(
110
+ "Slow query on #{endpoint} (#{query['duration_ms']}ms): #{query['sql'][0..100]}..."
111
+ )
112
+ end
113
+
114
+ # Alert if score is low
115
+ if score < 60
116
+ QueryOptimizerClient.configuration.logger&.error(
117
+ "CRITICAL: Low performance score (#{score}%) on #{endpoint}"
118
+ )
119
+ elsif score < 75
120
+ QueryOptimizerClient.configuration.logger&.warn(
121
+ "WARNING: Performance score (#{score}%) on #{endpoint}"
122
+ )
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+
5
+ module QueryOptimizerClient
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :query_optimizer_client
8
+
9
+ config.query_optimizer_client = ActiveSupport::OrderedOptions.new
10
+
11
+ initializer "query_optimizer_client.configure" do |app|
12
+ QueryOptimizerClient.configure do |config|
13
+ # Set Rails logger
14
+ config.logger = Rails.logger
15
+
16
+ # Apply any configuration from Rails config
17
+ app.config.query_optimizer_client.each do |key, value|
18
+ config.public_send("#{key}=", value) if config.respond_to?("#{key}=")
19
+ end
20
+ end
21
+ end
22
+
23
+ initializer "query_optimizer_client.middleware" do |app|
24
+ if QueryOptimizerClient.configuration.enabled?
25
+ app.config.middleware.use QueryOptimizerClient::Middleware
26
+ end
27
+ end
28
+
29
+ rake_tasks do
30
+ load "query_optimizer_client/tasks.rake"
31
+ end
32
+
33
+ generators do
34
+ require "query_optimizer_client/generators/install_generator"
35
+ end
36
+ end
37
+ end