rack-ai 0.1.0 → 0.3.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.
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module AI
5
+ module Features
6
+ class AnomalyDetection
7
+ attr_reader :name, :provider, :config
8
+
9
+ def initialize(provider, config)
10
+ @name = :anomaly_detection
11
+ @provider = provider
12
+ @config = config
13
+ @baseline_metrics = {}
14
+ @request_patterns = {}
15
+ end
16
+
17
+ def enabled?
18
+ @config.feature_enabled?(:anomaly_detection)
19
+ end
20
+
21
+ def process_response?
22
+ false
23
+ end
24
+
25
+ def process_request(env)
26
+ current_time = Time.now
27
+ client_ip = get_client_ip(env)
28
+
29
+ # Extract request metrics
30
+ metrics = extract_request_metrics(env)
31
+
32
+ # Update baseline and detect anomalies
33
+ anomalies = detect_anomalies(client_ip, metrics, current_time)
34
+
35
+ # Calculate risk score
36
+ risk_score = calculate_risk_score(anomalies, metrics)
37
+
38
+ # Determine action based on risk score
39
+ action = determine_action(risk_score)
40
+
41
+ {
42
+ action: action,
43
+ feature: :anomaly_detection,
44
+ timestamp: current_time.iso8601,
45
+ risk_score: risk_score,
46
+ anomalies: anomalies,
47
+ metrics: metrics,
48
+ baseline_deviation: calculate_baseline_deviation(metrics)
49
+ }
50
+ end
51
+
52
+ private
53
+
54
+ def get_client_ip(env)
55
+ env['HTTP_X_FORWARDED_FOR']&.split(',')&.first&.strip ||
56
+ env['HTTP_X_REAL_IP'] ||
57
+ env['REMOTE_ADDR'] ||
58
+ 'unknown'
59
+ end
60
+
61
+ def extract_request_metrics(env)
62
+ {
63
+ request_size: calculate_request_size(env),
64
+ header_count: env.select { |k, _| k.start_with?('HTTP_') }.size,
65
+ path_length: env['PATH_INFO']&.length || 0,
66
+ query_complexity: calculate_query_complexity(env['QUERY_STRING']),
67
+ user_agent_entropy: calculate_entropy(env['HTTP_USER_AGENT']),
68
+ request_frequency: calculate_request_frequency(get_client_ip(env)),
69
+ time_of_day: Time.now.hour,
70
+ day_of_week: Time.now.wday
71
+ }
72
+ end
73
+
74
+ def calculate_request_size(env)
75
+ size = 0
76
+ size += env['CONTENT_LENGTH'].to_i if env['CONTENT_LENGTH']
77
+ size += env['QUERY_STRING']&.length || 0
78
+ env.each { |k, v| size += k.length + v.to_s.length if k.start_with?('HTTP_') }
79
+ size
80
+ end
81
+
82
+ def calculate_query_complexity(query_string)
83
+ return 0 unless query_string
84
+
85
+ params = query_string.split('&')
86
+ complexity = params.size
87
+
88
+ # Add complexity for nested parameters, arrays, etc.
89
+ params.each do |param|
90
+ complexity += 1 if param.include?('[')
91
+ complexity += 1 if param.include?('%')
92
+ complexity += param.scan(/[=&]/).size
93
+ end
94
+
95
+ complexity
96
+ end
97
+
98
+ def calculate_entropy(string)
99
+ return 0 unless string
100
+
101
+ frequencies = Hash.new(0)
102
+ string.each_char { |char| frequencies[char] += 1 }
103
+
104
+ entropy = 0
105
+ string.length.times do
106
+ freq = frequencies.values.shift
107
+ next if freq == 0
108
+
109
+ probability = freq.to_f / string.length
110
+ entropy -= probability * Math.log2(probability)
111
+ end
112
+
113
+ entropy
114
+ end
115
+
116
+ def calculate_request_frequency(client_ip)
117
+ current_time = Time.now
118
+ @request_patterns[client_ip] ||= []
119
+
120
+ # Clean old entries (older than 1 hour)
121
+ @request_patterns[client_ip].reject! { |time| current_time - time > 3600 }
122
+
123
+ # Add current request
124
+ @request_patterns[client_ip] << current_time
125
+
126
+ # Return requests per minute
127
+ recent_requests = @request_patterns[client_ip].count { |time| current_time - time <= 60 }
128
+ recent_requests
129
+ end
130
+
131
+ def detect_anomalies(client_ip, metrics, current_time)
132
+ anomalies = []
133
+
134
+ # Update baseline for this client
135
+ @baseline_metrics[client_ip] ||= initialize_baseline
136
+ baseline = @baseline_metrics[client_ip]
137
+
138
+ metrics.each do |metric, value|
139
+ next unless value.is_a?(Numeric)
140
+
141
+ # Calculate z-score
142
+ mean = baseline[metric][:mean]
143
+ std_dev = baseline[metric][:std_dev]
144
+
145
+ if std_dev > 0
146
+ z_score = (value - mean).abs / std_dev
147
+
148
+ if z_score > 3 # 3 standard deviations
149
+ anomalies << {
150
+ metric: metric,
151
+ value: value,
152
+ expected_range: [mean - 2 * std_dev, mean + 2 * std_dev],
153
+ z_score: z_score,
154
+ severity: z_score > 5 ? :high : :medium
155
+ }
156
+ end
157
+ end
158
+
159
+ # Update baseline with exponential moving average
160
+ alpha = 0.1
161
+ baseline[metric][:mean] = alpha * value + (1 - alpha) * mean
162
+
163
+ # Update standard deviation
164
+ variance = baseline[metric][:variance] || 0
165
+ new_variance = alpha * (value - mean) ** 2 + (1 - alpha) * variance
166
+ baseline[metric][:variance] = new_variance
167
+ baseline[metric][:std_dev] = Math.sqrt(new_variance)
168
+ end
169
+
170
+ anomalies
171
+ end
172
+
173
+ def initialize_baseline
174
+ Hash.new do |hash, key|
175
+ hash[key] = { mean: 0, variance: 0, std_dev: 1 }
176
+ end
177
+ end
178
+
179
+ def calculate_risk_score(anomalies, metrics)
180
+ base_score = 0
181
+
182
+ # Score based on anomalies
183
+ anomalies.each do |anomaly|
184
+ case anomaly[:severity]
185
+ when :high
186
+ base_score += 30
187
+ when :medium
188
+ base_score += 15
189
+ else
190
+ base_score += 5
191
+ end
192
+ end
193
+
194
+ # Additional risk factors
195
+ base_score += 10 if metrics[:request_frequency] > 60 # More than 1 req/sec
196
+ base_score += 15 if metrics[:request_size] > 1_000_000 # Large requests
197
+ base_score += 5 if metrics[:query_complexity] > 50 # Complex queries
198
+ base_score += 10 if metrics[:user_agent_entropy] < 2 # Suspicious UA
199
+
200
+ # Time-based risk (higher risk during off-hours)
201
+ if metrics[:time_of_day] < 6 || metrics[:time_of_day] > 22
202
+ base_score += 5
203
+ end
204
+
205
+ [base_score, 100].min # Cap at 100
206
+ end
207
+
208
+ def calculate_baseline_deviation(metrics)
209
+ # Simple calculation of how much current metrics deviate from expected
210
+ deviation_score = 0
211
+
212
+ metrics.each do |_, value|
213
+ next unless value.is_a?(Numeric)
214
+ # Simplified deviation calculation
215
+ deviation_score += value > 1000 ? 1 : 0
216
+ end
217
+
218
+ deviation_score
219
+ end
220
+
221
+ def determine_action(risk_score)
222
+ case risk_score
223
+ when 0..20
224
+ :allow
225
+ when 21..50
226
+ :monitor
227
+ when 51..80
228
+ :challenge
229
+ else
230
+ :block
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module AI
5
+ module Features
6
+ class RateLimiting
7
+ attr_reader :name, :provider, :config
8
+
9
+ def initialize(provider, config)
10
+ @name = :rate_limiting
11
+ @provider = provider
12
+ @config = config
13
+ @rate_store = {}
14
+ @cleanup_timer = Time.now
15
+ end
16
+
17
+ def enabled?
18
+ @config.feature_enabled?(:rate_limiting)
19
+ end
20
+
21
+ def process_response?
22
+ false
23
+ end
24
+
25
+ def process_request(env)
26
+ client_ip = get_client_ip(env)
27
+ current_time = Time.now
28
+
29
+ # Cleanup old entries every 5 minutes
30
+ cleanup_old_entries if current_time - @cleanup_timer > 300
31
+
32
+ # Get rate limit settings
33
+ window_size = @config.rate_limiting.window_size || 3600 # 1 hour default
34
+ max_requests = @config.rate_limiting.max_requests || 1000 # 1000 requests default
35
+
36
+ # Initialize client data if not exists
37
+ @rate_store[client_ip] ||= { requests: [], blocked_until: nil }
38
+ client_data = @rate_store[client_ip]
39
+
40
+ # Check if client is currently blocked
41
+ if client_data[:blocked_until] && current_time < client_data[:blocked_until]
42
+ return {
43
+ action: :block,
44
+ reason: "Rate limit exceeded - blocked until #{client_data[:blocked_until]}",
45
+ status: 429,
46
+ feature: :rate_limiting,
47
+ timestamp: current_time.iso8601,
48
+ retry_after: (client_data[:blocked_until] - current_time).to_i
49
+ }
50
+ end
51
+
52
+ # Remove old requests outside the window
53
+ window_start = current_time - window_size
54
+ client_data[:requests].reject! { |timestamp| timestamp < window_start }
55
+
56
+ # Check if rate limit is exceeded
57
+ if client_data[:requests].length >= max_requests
58
+ block_duration = @config.rate_limiting.block_duration || 3600 # 1 hour default
59
+ client_data[:blocked_until] = current_time + block_duration
60
+
61
+ return {
62
+ action: :block,
63
+ reason: "Rate limit exceeded: #{client_data[:requests].length}/#{max_requests} requests in #{window_size}s",
64
+ status: 429,
65
+ feature: :rate_limiting,
66
+ timestamp: current_time.iso8601,
67
+ retry_after: block_duration,
68
+ requests_count: client_data[:requests].length,
69
+ window_size: window_size
70
+ }
71
+ end
72
+
73
+ # Add current request
74
+ client_data[:requests] << current_time
75
+
76
+ {
77
+ action: :allow,
78
+ feature: :rate_limiting,
79
+ timestamp: current_time.iso8601,
80
+ requests_count: client_data[:requests].length,
81
+ requests_remaining: max_requests - client_data[:requests].length,
82
+ window_size: window_size,
83
+ reset_time: (window_start + window_size).iso8601
84
+ }
85
+ end
86
+
87
+ private
88
+
89
+ def get_client_ip(env)
90
+ # Try various headers for real IP
91
+ forwarded_for = env['HTTP_X_FORWARDED_FOR']
92
+ return forwarded_for.split(',').first.strip if forwarded_for
93
+
94
+ real_ip = env['HTTP_X_REAL_IP']
95
+ return real_ip if real_ip
96
+
97
+ env['REMOTE_ADDR'] || 'unknown'
98
+ end
99
+
100
+ def cleanup_old_entries
101
+ current_time = Time.now
102
+ @rate_store.each do |ip, data|
103
+ # Remove clients with no recent activity and no active blocks
104
+ if data[:requests].empty? &&
105
+ (data[:blocked_until].nil? || current_time > data[:blocked_until])
106
+ @rate_store.delete(ip)
107
+ end
108
+ end
109
+ @cleanup_timer = current_time
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -83,7 +83,9 @@ module Rack
83
83
  routing: Features::Routing,
84
84
  logging: Features::Logging,
85
85
  enhancement: Features::Enhancement,
86
- security: Features::Security
86
+ security: Features::Security,
87
+ rate_limiting: Features::RateLimiting,
88
+ anomaly_detection: Features::AnomalyDetection
87
89
  }
88
90
 
89
91
  features = @config.features.map do |feature_name|
@@ -93,7 +95,7 @@ module Rack
93
95
  feature_class.new(@provider, @config)
94
96
  end
95
97
 
96
- Utils::Logger.debug("Built features: #{features.map(&:name)}")
98
+ Utils::Logger.debug("Built features: #{features.map { |f| f.class.name.split('::').last }}")
97
99
  features
98
100
  end
99
101
 
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'json'
5
+
6
+ module Rack
7
+ module AI
8
+ module Utils
9
+ class AdvancedLogger
10
+ LOG_LEVELS = {
11
+ debug: ::Logger::DEBUG,
12
+ info: ::Logger::INFO,
13
+ warn: ::Logger::WARN,
14
+ error: ::Logger::ERROR,
15
+ fatal: ::Logger::FATAL
16
+ }.freeze
17
+
18
+ def self.instance
19
+ @instance ||= new
20
+ end
21
+
22
+ def initialize(output = $stdout, level: :info, format: :json)
23
+ @logger = ::Logger.new(output)
24
+ @logger.level = LOG_LEVELS[level] || ::Logger::INFO
25
+ @format = format
26
+ @logger.formatter = method(:format_message)
27
+ end
28
+
29
+ def debug(message, context = {})
30
+ log(:debug, message, context)
31
+ end
32
+
33
+ def info(message, context = {})
34
+ log(:info, message, context)
35
+ end
36
+
37
+ def warn(message, context = {})
38
+ log(:warn, message, context)
39
+ end
40
+
41
+ def error(message, context = {})
42
+ log(:error, message, context)
43
+ end
44
+
45
+ def fatal(message, context = {})
46
+ log(:fatal, message, context)
47
+ end
48
+
49
+ def log_request(env, duration: nil, status: nil)
50
+ context = {
51
+ method: env['REQUEST_METHOD'],
52
+ path: env['PATH_INFO'],
53
+ query: env['QUERY_STRING'],
54
+ user_agent: env['HTTP_USER_AGENT'],
55
+ remote_ip: env['REMOTE_ADDR'],
56
+ duration_ms: duration ? (duration * 1000).round(2) : nil,
57
+ status: status
58
+ }.compact
59
+
60
+ info("HTTP Request", context)
61
+ end
62
+
63
+ def log_ai_processing(feature, result, duration: nil)
64
+ context = {
65
+ feature: feature,
66
+ action: result[:action],
67
+ confidence: result[:confidence],
68
+ duration_ms: duration ? (duration * 1000).round(2) : nil,
69
+ provider: result[:provider]
70
+ }.compact
71
+
72
+ info("AI Processing", context)
73
+ end
74
+
75
+ def log_error(error, context = {})
76
+ error_context = {
77
+ error_class: error.class.name,
78
+ error_message: error.message,
79
+ backtrace: error.backtrace&.first(5)
80
+ }.merge(context)
81
+
82
+ error("Application Error", error_context)
83
+ end
84
+
85
+ private
86
+
87
+ def log(level, message, context)
88
+ @logger.public_send(level) do
89
+ case @format
90
+ when :json
91
+ format_json(level, message, context)
92
+ when :structured
93
+ format_structured(level, message, context)
94
+ else
95
+ format_simple(level, message, context)
96
+ end
97
+ end
98
+ end
99
+
100
+ def format_message(severity, datetime, progname, msg)
101
+ msg
102
+ end
103
+
104
+ def format_json(level, message, context)
105
+ {
106
+ timestamp: Time.now.iso8601,
107
+ level: level.to_s.upcase,
108
+ message: message,
109
+ component: 'rack-ai',
110
+ **context
111
+ }.to_json + "\n"
112
+ end
113
+
114
+ def format_structured(level, message, context)
115
+ timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
116
+ context_str = context.empty? ? '' : " #{context.inspect}"
117
+ "[#{timestamp}] #{level.to_s.upcase} [rack-ai] #{message}#{context_str}\n"
118
+ end
119
+
120
+ def format_simple(level, message, context)
121
+ context_str = context.empty? ? '' : " (#{context.map { |k, v| "#{k}=#{v}" }.join(', ')})"
122
+ "#{message}#{context_str}\n"
123
+ end
124
+ end
125
+
126
+ # Alias for backward compatibility
127
+ EnhancedLogger = AdvancedLogger
128
+ end
129
+ end
130
+ end
@@ -13,23 +13,23 @@ module Rack
13
13
  end
14
14
 
15
15
  def debug(message, metadata = {})
16
- log(:debug, message, metadata)
16
+ puts "[DEBUG] #{message} #{metadata}" if ENV['RACK_AI_DEBUG']
17
17
  end
18
18
 
19
19
  def info(message, metadata = {})
20
- log(:info, message, metadata)
20
+ puts "[INFO] #{message} #{metadata}"
21
21
  end
22
22
 
23
23
  def warn(message, metadata = {})
24
- log(:warn, message, metadata)
24
+ puts "[WARN] #{message} #{metadata}"
25
25
  end
26
26
 
27
27
  def error(message, metadata = {})
28
- log(:error, message, metadata)
28
+ puts "[ERROR] #{message} #{metadata}"
29
29
  end
30
30
 
31
31
  def fatal(message, metadata = {})
32
- log(:fatal, message, metadata)
32
+ puts "[FATAL] #{message} #{metadata}"
33
33
  end
34
34
 
35
35
  private
@@ -106,6 +106,7 @@ module Rack
106
106
  end
107
107
  end
108
108
  end
109
+
109
110
  end
110
111
  end
111
112
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  module AI
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/rack/ai.rb CHANGED
@@ -8,15 +8,18 @@ require_relative "ai/providers/openai"
8
8
  require_relative "ai/providers/huggingface"
9
9
  require_relative "ai/providers/local"
10
10
  require_relative "ai/features/classification"
11
+ require_relative "ai/features/security"
11
12
  require_relative "ai/features/moderation"
12
13
  require_relative "ai/features/caching"
13
14
  require_relative "ai/features/routing"
14
15
  require_relative "ai/features/logging"
15
16
  require_relative "ai/features/enhancement"
16
- require_relative "ai/features/security"
17
+ require_relative "ai/features/rate_limiting"
18
+ require_relative "ai/features/anomaly_detection"
17
19
  require_relative "ai/utils/logger"
18
20
  require_relative "ai/utils/metrics"
19
21
  require_relative "ai/utils/sanitizer"
22
+ require_relative "ai/utils/enhanced_logger"
20
23
 
21
24
  module Rack
22
25
  module AI
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmet KAHRAMAN
@@ -241,15 +241,20 @@ files:
241
241
  - ROADMAP.md
242
242
  - Rakefile
243
243
  - benchmarks/performance_benchmark.rb
244
+ - examples/comprehensive_example.rb
244
245
  - examples/rails_integration.rb
246
+ - examples/rails_integration_advanced.rb
245
247
  - examples/sinatra_integration.rb
248
+ - examples/sinatra_microservice.rb
246
249
  - lib/rack/ai.rb
247
250
  - lib/rack/ai/configuration.rb
251
+ - lib/rack/ai/features/anomaly_detection.rb
248
252
  - lib/rack/ai/features/caching.rb
249
253
  - lib/rack/ai/features/classification.rb
250
254
  - lib/rack/ai/features/enhancement.rb
251
255
  - lib/rack/ai/features/logging.rb
252
256
  - lib/rack/ai/features/moderation.rb
257
+ - lib/rack/ai/features/rate_limiting.rb
253
258
  - lib/rack/ai/features/routing.rb
254
259
  - lib/rack/ai/features/security.rb
255
260
  - lib/rack/ai/middleware.rb
@@ -257,6 +262,7 @@ files:
257
262
  - lib/rack/ai/providers/huggingface.rb
258
263
  - lib/rack/ai/providers/local.rb
259
264
  - lib/rack/ai/providers/openai.rb
265
+ - lib/rack/ai/utils/enhanced_logger.rb
260
266
  - lib/rack/ai/utils/logger.rb
261
267
  - lib/rack/ai/utils/metrics.rb
262
268
  - lib/rack/ai/utils/sanitizer.rb