Class: RailsErrorDashboard::Services::PatternDetector
- Inherits:
-
Object
- Object
- RailsErrorDashboard::Services::PatternDetector
- Defined in:
- lib/rails_error_dashboard/services/pattern_detector.rb
Overview
Service object for detecting occurrence patterns in errors
Provides two main pattern detection capabilities:
-
Cyclical patterns - Daily/weekly rhythms (e.g., business hours pattern)
-
Burst detection - Many errors in short time period
Class Method Summary collapse
-
.analyze_cyclical_pattern(error_type:, platform:, days: 30) ⇒ Hash
Analyze cyclical patterns in error occurrences.
-
.calculate_pattern_strength(hourly_dist) ⇒ Object
Calculate pattern strength (0.0-1.0) Measures how concentrated the errors are in peak hours.
-
.classify_burst_intensity(count) ⇒ Object
Classify burst intensity based on error count.
-
.detect_bursts(error_type:, platform:, days: 7) ⇒ Array<Hash>
Detect error bursts (sequences where errors occur rapidly).
-
.determine_pattern_type(hourly_dist, weekday_dist) ⇒ Object
Determine the pattern type based on hour and weekday distributions.
-
.empty_pattern ⇒ Object
Empty pattern result.
-
.finalize_burst(burst_data) ⇒ Object
Finalize burst metadata.
-
.find_peak_hours(hourly_dist) ⇒ Object
Find peak hours (hours with >2x average).
Class Method Details
.analyze_cyclical_pattern(error_type:, platform:, days: 30) ⇒ Hash
Analyze cyclical patterns in error occurrences
Detects:
-
Business hours pattern (9am-5pm peak)
-
Night pattern (midnight-6am peak)
-
Weekend pattern (Sat-Sun peak)
-
Uniform pattern (no clear pattern)
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 50 def self.analyze_cyclical_pattern(error_type:, platform:, days: 30) start_date = days.days.ago # Get all error occurrences for this error type/platform errors = ErrorLog .where(error_type: error_type, platform: platform) .where("occurred_at >= ?", start_date) return empty_pattern if errors.empty? # Group by hour of day (0-23) hourly_distribution = Hash.new(0) weekday_distribution = Hash.new(0) errors.each do |error| hour = error.occurred_at.hour wday = error.occurred_at.wday # 0 = Sunday, 6 = Saturday hourly_distribution[hour] += 1 weekday_distribution[wday] += 1 end # Calculate pattern type and peaks pattern_type = determine_pattern_type(hourly_distribution, weekday_distribution) peak_hours = find_peak_hours(hourly_distribution) pattern_strength = calculate_pattern_strength(hourly_distribution) { pattern_type: pattern_type, peak_hours: peak_hours, hourly_distribution: hourly_distribution, weekday_distribution: weekday_distribution, pattern_strength: pattern_strength, total_errors: errors.count, analysis_days: days } end |
.calculate_pattern_strength(hourly_dist) ⇒ Object
Calculate pattern strength (0.0-1.0) Measures how concentrated the errors are in peak hours
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 221 def self.calculate_pattern_strength(hourly_dist) return 0.0 if hourly_dist.empty? total = hourly_dist.values.sum return 0.0 if total.zero? # Calculate coefficient of variation (std dev / mean) # Higher variation = stronger pattern values = (0..23).map { |h| hourly_dist[h] || 0 } mean = total.to_f / 24 variance = values.sum { |v| (v - mean)**2 } / 24 std_dev = Math.sqrt(variance) # Normalize to 0-1 scale (coefficient of variation) # Divide by sqrt(mean) to get a rough 0-1 scale cv = mean > 0 ? std_dev / mean : 0 [ cv.round(2), 1.0 ].min end |
.classify_burst_intensity(count) ⇒ Object
Classify burst intensity based on error count
257 258 259 260 261 262 263 264 265 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 257 def self.classify_burst_intensity(count) if count >= 20 :high elsif count >= 10 :medium else :low end end |
.detect_bursts(error_type:, platform:, days: 7) ⇒ Array<Hash>
Detect error bursts (sequences where errors occur rapidly)
A burst is defined as a sequence where inter-arrival time < 1 minute Burst intensity:
-
:high - 20+ errors in burst
-
:medium - 10-19 errors
-
:low - 5-9 errors
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 99 def self.detect_bursts(error_type:, platform:, days: 7) start_date = days.days.ago # Get all error occurrences sorted by time errors = ErrorLog .where(error_type: error_type, platform: platform) .where("occurred_at >= ?", start_date) .order(:occurred_at) return [] if errors.count < 5 # Need at least 5 errors to detect a burst # Get all occurrence timestamps = errors.flat_map do |error| # If error has error_occurrences, use those timestamps if error.respond_to?(:error_occurrences) && error.error_occurrences.any? error.error_occurrences.pluck(:occurred_at) else # Otherwise use the error's occurred_at repeated by occurrence_count Array.new(error.occurrence_count || 1, error.occurred_at) end end.sort return [] if .size < 5 # Detect bursts: sequences where inter-arrival < 60 seconds bursts = [] current_burst = nil .each_with_index do |, i| next if i.zero? inter_arrival = - [i - 1] if inter_arrival <= 60 # 60 seconds threshold # Start new burst or continue existing if current_burst.nil? current_burst = { start_time: [i - 1], timestamps: [ [i - 1], ] } else current_burst[:timestamps] << end else # End current burst if it exists and has enough errors if current_burst && current_burst[:timestamps].size >= 5 bursts << finalize_burst(current_burst) end current_burst = nil end end # Don't forget the last burst if current_burst && current_burst[:timestamps].size >= 5 bursts << finalize_burst(current_burst) end bursts end |
.determine_pattern_type(hourly_dist, weekday_dist) ⇒ Object
Determine the pattern type based on hour and weekday distributions
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 175 def self.determine_pattern_type(hourly_dist, weekday_dist) return :none if hourly_dist.empty? # Calculate average errors per hour avg_per_hour = hourly_dist.values.sum.to_f / 24 # Find peak hours (>2x average) peak_hours = hourly_dist.select { |_, count| count > avg_per_hour * 2 }.keys.sort # Business hours pattern: peaks between 9am-5pm business_hours = (9..17).to_a business_peaks = peak_hours & business_hours if business_peaks.size >= 3 return :business_hours end # Night pattern: peaks between midnight-6am night_hours = (0..6).to_a night_peaks = peak_hours & night_hours if night_peaks.size >= 2 return :night end # Weekend pattern: most errors on Sat/Sun if weekday_dist.any? weekend_count = (weekday_dist[0] || 0) + (weekday_dist[6] || 0) # Sun + Sat total_count = weekday_dist.values.sum if weekend_count > total_count * 0.5 return :weekend end end # No clear pattern :uniform end |
.empty_pattern ⇒ Object
Empty pattern result
162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 162 def self.empty_pattern { pattern_type: :none, peak_hours: [], hourly_distribution: {}, weekday_distribution: {}, pattern_strength: 0.0, total_errors: 0, analysis_days: 0 } end |
.finalize_burst(burst_data) ⇒ Object
Finalize burst metadata
241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 241 def self.finalize_burst(burst_data) start_time = burst_data[:start_time] end_time = burst_data[:timestamps].last duration = end_time - start_time count = burst_data[:timestamps].size { start_time: start_time, end_time: end_time, duration_seconds: duration.round(1), error_count: count, burst_intensity: classify_burst_intensity(count) } end |
.find_peak_hours(hourly_dist) ⇒ Object
Find peak hours (hours with >2x average)
212 213 214 215 216 217 |
# File 'lib/rails_error_dashboard/services/pattern_detector.rb', line 212 def self.find_peak_hours(hourly_dist) return [] if hourly_dist.empty? avg = hourly_dist.values.sum.to_f / 24 hourly_dist.select { |_, count| count > avg * 2 }.keys.sort end |