recheck 0.4.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d3abb8f0a8b65c31746964627a98cd5e713f77370797153268465e16a515646
4
- data.tar.gz: 767cc703b8a396e486f2350d9f67e39c7d6b572b749dba5e1a9ef48e6394da7c
3
+ metadata.gz: 229b7b48547c7da457bff99e2d10cef1d46a915ad4f3699dd0ae3ad29b7075af
4
+ data.tar.gz: 55d665725da5fe601a0c86ba9c4f62943702b56c7ae239aa3c8c1fe31e01b94c
5
5
  SHA512:
6
- metadata.gz: 7509ef3cc281c76f022fd1c2599242a4cc5b758f8feb2975de6d70b8fa9756419d45d23740e16487b44b8088506ae8f5a11fc62bf25d77ec38255553d8d044f3
7
- data.tar.gz: ec42bd74543f4ea2a8d884528005d3da2a2f302e1a987f16e15631f324d901f5e817da143151116a836a59b0e3c98aa81f438ccf0c67f29038411559a01737cf
6
+ metadata.gz: b2b403ea3658c20f66b825f1198c3c6ea341161b84face61a83529c284135f230b069bd4db13f2550dde0e7965944ac36d3de56b3ec0cb4b579a99796e16dc5a
7
+ data.tar.gz: 1dbfd3a4dd3252b9e8603a9b9bbb28fb35255acdb4d068dfe74d55fde7e0683b53ff66b59c89eae29848070193a0ea48c8da51a78195e88ede9e3f8c35b6039a
@@ -136,7 +136,7 @@ module Recheck
136
136
 
137
137
  def run
138
138
  create_helper
139
- create_reporter_dir
139
+ create_samples
140
140
  create_site_checks
141
141
  setup_model_checks
142
142
  run_linter
@@ -168,8 +168,9 @@ module Recheck
168
168
  copy_template("#{template_dir}/recheck_helper.rb", "recheck/recheck_helper.rb")
169
169
  end
170
170
 
171
- def create_reporter_dir
172
- FileUtils.mkdir_p("recheck/reporter")
171
+ def create_samples
172
+ copy_template("#{template_dir}/reporter_sample.rb", "recheck/reporter/reporter.rb.sample")
173
+ copy_template("#{template_dir}/regression_checker_sample.rb", "recheck/regression/regression_checker.rb.sample")
173
174
  end
174
175
 
175
176
  def create_site_checks
@@ -35,10 +35,12 @@ module Recheck
35
35
  #
36
36
  # around_run -> for each Checker class:
37
37
  # around_checker ->
38
- # run each query() method
39
- # for each 'check_' method on the checker:
40
- # for each record queried:
41
- # around_check -> check(record)
38
+ # around_query ->
39
+ # run each query() method
40
+ # for each 'check_' method on the checker:
41
+ # for each record queried:
42
+ # around_check ->
43
+ # check(record)
42
44
 
43
45
  def around_run(checkers: [])
44
46
  total_count = yield
@@ -48,6 +50,10 @@ module Recheck
48
50
  counts = yield
49
51
  end
50
52
 
53
+ def around_query(checker:, query:, checks: [])
54
+ yield
55
+ end
56
+
51
57
  def around_check(checker:, query:, check:, record:)
52
58
  result = yield
53
59
  end
@@ -97,20 +103,20 @@ module Recheck
97
103
 
98
104
  def print_errors
99
105
  failure_details = []
100
- grouped_errors = @errors.group_by { |e| [e.checker_class, e.query, e.check, e.type] }
106
+ grouped_errors = @errors.group_by { |e| [e.checker, e.query, e.check, e.type] }
101
107
 
102
- grouped_errors.each do |(checker_class, query, check), group_errors|
108
+ grouped_errors.each do |(checker, query, check), group_errors|
103
109
  case group_errors.first.type
104
110
  when :fail
105
111
  ids = group_errors.map { |e| fetch_record_id(e.record) }.join(", ")
106
- failure_details << " #{checker_class}##{query} -> #{check} failed for records: #{ids}"
112
+ failure_details << " #{checker}##{query} -> #{check} failed for records: #{ids}"
107
113
  when :exception
108
114
  error = group_errors.first
109
- error_message = " #{checker_class}##{query} -> #{check} exception #{error.exception.message} for #{group_errors.size} records"
115
+ error_message = " #{checker}##{query} -> #{check} exception #{error.exception.message} for #{group_errors.size} records"
110
116
  failure_details << error_message
111
117
  failure_details << error.record.full_message(highlight: false, order: :top) if error.record.respond_to?(:full_message)
112
118
  when :blanket
113
- failure_details << " #{checker_class}: Skipping because the first 20 checks all failed. Either there's a lot of bad data or there's something wrong with the checks."
119
+ failure_details << " #{checker}: Skipping because the first 20 checks all failed. Either there's a lot of bad data or there's something wrong with the checks."
114
120
  end
115
121
  end
116
122
  puts failure_details
@@ -12,7 +12,7 @@ module Recheck
12
12
  end
13
13
  end
14
14
 
15
- Error = Data.define(:checker, :query, :check, :record, :type, :exception) do
15
+ Error = Data.define(:type, :checker, :query, :check, :record, :exception) do
16
16
  def initialize(*args)
17
17
  super
18
18
  raise ArgumentError unless ERROR_TYPES.include? type
@@ -110,39 +110,43 @@ module Recheck
110
110
  reduce(reporters: @reporters, hook: :around_checker, kwargs: {checker:, queries:, checks:}) do
111
111
  # for each query_...
112
112
  queries.each do |query|
113
- checker_counts.increment :queries
114
- # for each record...
115
- # TODO: must handle if the query method yields (find_each) OR returns (current)
116
- (checker.public_send(query) || []).each do |record|
117
- # for each check_method...
118
- checks.each do |check|
119
- raw_result = nil
120
- reduce(reporters: @reporters, hook: :around_check, kwargs: {checker:, query:, check:, record:}) do
121
- raw_result = checker.public_send(check, record)
122
- result = raw_result ? pass : Error.new(checker:, query:, check:, record:, type: :fail, exception: nil)
123
-
124
- checker_counts.increment(result.type)
125
- break if checker_counts.reached_blanket_failure?
126
-
127
- result
128
- rescue *PASSTHROUGH_EXCEPTIONS
129
- raise
130
- rescue => e
131
- Error.new(checker:, query:, check:, record:, type: :exception, exception: e)
113
+ reduce(reporters: @reporters, hook: :around_query, kwargs: {checker:, query:, checks:}) do
114
+ checker_counts.increment :queries
115
+ # for each record...
116
+ # TODO: must handle if the query method yields (find_each) OR returns (current)
117
+ (checker.public_send(query) || []).each do |record|
118
+ # for each check_method...
119
+ checks.each do |check|
120
+ raw_result = nil
121
+ reduce(reporters: @reporters, hook: :around_check, kwargs: {checker:, query:, check:, record:}) do
122
+ raw_result = checker.public_send(check, record)
123
+ result = raw_result ? pass : Error.new(checker:, query:, check:, record:, type: :fail, exception: nil)
124
+
125
+ checker_counts.increment(result.type)
126
+ break if checker_counts.reached_blanket_failure?
127
+
128
+ result
129
+ rescue *PASSTHROUGH_EXCEPTIONS
130
+ raise
131
+ rescue => e
132
+ Error.new(checker:, query:, check:, record:, type: :exception, exception: e)
133
+ end
132
134
  end
133
- end
134
- @yields.raise_unless_all_reporters_yielded(hook: :around_check)
135
+ @yields.raise_unless_all_reporters_yielded(hook: :around_check)
135
136
 
136
- # if the first 20 error out, halt the check method, it's probably buggy
137
- if checker_counts.reached_blanket_failure?
138
- checker_counts.increment :blanket
137
+ # if the first 20 error out, halt the check method, it's probably buggy
138
+ if checker_counts.reached_blanket_failure?
139
+ checker_counts.increment :blanket
139
140
 
140
- error = Error.new(checker:, query:, check: nil, record: nil, type: :blanket, exception: nil)
141
- @reporters.each { it.halt(checker:, query:, check: nil, error:) }
141
+ error = Error.new(checker:, query:, check: nil, record: nil, type: :blanket, exception: nil)
142
+ @reporters.each { it.halt(checker:, query:, check: nil, error:) }
142
143
 
143
- break
144
+ break
145
+ end
144
146
  end
147
+ nil # yield nothing around_query
145
148
  end
149
+ @yields.raise_unless_all_reporters_yielded(hook: :around_query)
146
150
  rescue *PASSTHROUGH_EXCEPTIONS
147
151
  raise
148
152
  rescue => e
@@ -160,6 +164,7 @@ module Recheck
160
164
  @total_counts
161
165
  end
162
166
  @yields.raise_unless_all_reporters_yielded(hook: :around_run)
167
+ @total_counts
163
168
  end
164
169
  end
165
170
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Recheck
4
- VERSION = '0.4.0'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -0,0 +1,206 @@
1
+ # Recheck Checker API Documentation
2
+ #
3
+ # This file documents the complete Checker API for Recheck.
4
+ # Use this as a reference when creating your own checkers.
5
+
6
+ # All checkers must inherit from Recheck::Checker::Base to be registered
7
+ class SampleChecker < Recheck::Checker::Base
8
+ #
9
+ # CHECKER LIFECYCLE AND HOOKS
10
+ # ==========================
11
+ #
12
+ # Checkers have a simple lifecycle with four hooks:
13
+ #
14
+ # 1. initialize - Optional setup
15
+ # 2. query_* methods - Required to fetch records to check
16
+ # 3. check_* methods - Required to validate each record
17
+ # 4. Metadata methods - Optional for routing and prioritization
18
+ #
19
+
20
+ # Hook 1: initialize (optional)
21
+ # ----------------------------
22
+ # Use initialize to set up any resources needed by your checks.
23
+ # This runs once when the checker is instantiated.
24
+ def initialize
25
+ # You can:
26
+ # - Connect to external services
27
+ # - Load reference data
28
+ # - Set up caches or other shared resources
29
+ # - Configure the checker based on environment
30
+
31
+ @external_service = ExternalService.new(api_key: ENV["API_KEY"])
32
+ @reference_data = load_reference_data
33
+ @cache = {}
34
+ end
35
+
36
+ # You can use helper methods to organize your code
37
+ def load_reference_data
38
+ # Load data from a file, database, or API
39
+ # This is just a helper method, not part of the Checker API
40
+ {}
41
+ end
42
+
43
+ # Hook 2: query_* methods (one required)
44
+ # ---------------------------------
45
+ # At least one query method is required. Query methods must:
46
+ # - Start with "query" (or they won't be detected)
47
+ # - Return an Enumerable of records to check
48
+ # - Be efficient (they often run against production databases)
49
+
50
+ # Basic query method - returns all records of a type
51
+ def query
52
+ # The simplest query just returns all records
53
+ # Use find_each with ActiveRecord for batching
54
+ Model.find_each
55
+ end
56
+
57
+ # You can have multiple query methods to:
58
+ # - Check different record types
59
+ # - Optimize performance with targeted queries
60
+ # - Focus on specific subsets of data
61
+ def query_recent
62
+ # Focus on recently created/updated records
63
+ Model.where("updated_at > ?", 1.day.ago).find_each
64
+ end
65
+
66
+ def query_problematic
67
+ # Target records that might have issues
68
+ Model.where(status: "error")
69
+ .or(Model.where(processed_at: nil))
70
+ .includes(:related_records) # Eager load associations
71
+ .find_each
72
+ end
73
+
74
+ # Query methods can return any Enumerable, not just ActiveRecord
75
+ def query_external_data
76
+ # You can check data from external sources
77
+ @external_service.fetch_records.map do |record|
78
+ # Transform external data if needed
79
+ {id: record["id"], data: record["payload"]}
80
+ end
81
+ end
82
+
83
+ # Query methods can return arrays, hashes, or custom objects
84
+ def query_composite
85
+ # You can join data from multiple sources
86
+ [
87
+ {type: "config", value: AppConfig.settings},
88
+ {type: "status", value: SystemStatus.current}
89
+ ]
90
+ end
91
+
92
+ # Hook 3: check_* methods (one required)
93
+ # ---------------------------------
94
+ # Check methods must:
95
+ # - Start with "check_" (or they won't be detected)
96
+ # - Take a single record parameter
97
+ # - Return false/nil for failing records, anything else for passing
98
+
99
+ # Basic check - validates a single aspect of a record
100
+ def check_record_is_valid(record)
101
+ # The simplest check just calls ActiveRecord validations
102
+ # Returns false if invalid, true if valid
103
+ record.valid?
104
+ end
105
+
106
+ # Checks can implement complex business rules
107
+ def check_business_rule(record)
108
+ # Implement any business logic
109
+ # Return false/nil for failing records
110
+ if record.status == "completed" && record.completed_at.nil?
111
+ # This is a failing condition
112
+ return false
113
+ end
114
+
115
+ # Any non-false/nil return is considered passing
116
+ true
117
+ end
118
+
119
+ # Checks can integrate with external systems
120
+ def check_external_consistency(record)
121
+ # Check that record matches external system
122
+ external_data = @external_service.find(record.external_id)
123
+
124
+ # Return false if inconsistent
125
+ return false if external_data.nil?
126
+ return false if external_data["status"] != record.status
127
+
128
+ # Return true if consistent
129
+ true
130
+ end
131
+
132
+ # Checks can automatically fix issues (use carefully!)
133
+ def check_and_fix(record)
134
+ # Check if there's an issue
135
+ if record.calculated_total != record.stored_total
136
+ # Fix the issue
137
+ record.stored_total = record.calculated_total
138
+ record.save!
139
+
140
+ # Log the fix
141
+ LoggingService.info("Fixed total for record #{record.id}")
142
+ end
143
+
144
+ # Return true since we've fixed the issue
145
+ true
146
+ end
147
+
148
+ # Checks can handle different record types from different queries
149
+ def check_config_is_valid(record)
150
+ # Handle records from query_composite
151
+ if record[:type] == "config"
152
+ # Check config settings
153
+ return record[:value].valid?
154
+ elsif record[:type] == "status"
155
+ # Check system status
156
+ return record[:value].ok?
157
+ end
158
+
159
+ # Skip records this check doesn't understand
160
+ true
161
+ end
162
+
163
+ #
164
+ # METADATA METHODS
165
+ # ===============
166
+ #
167
+ # These optional methods provide metadata about your checker
168
+ # for use by reporters and the Recheck runner.
169
+ #
170
+
171
+ # Team responsible for this checker
172
+ # Used by reporters to route notifications
173
+ def team
174
+ :data_integrity
175
+ end
176
+
177
+ # Priority of this checker
178
+ # Used by reporters to prioritize notifications
179
+ def priority
180
+ :high # :high, :medium, :low
181
+ end
182
+
183
+ # Tags for this checker
184
+ # Used for filtering and categorization
185
+ def tags
186
+ [:critical, :customer_facing]
187
+ end
188
+
189
+ # Documentation URL
190
+ # Link to runbook or documentation
191
+ def documentation_url
192
+ "https://internal-docs.example.com/data-integrity/sample-checker"
193
+ end
194
+
195
+ # Slack channel for notifications
196
+ # Used by SlackReporter
197
+ def slack_channel
198
+ "#data-alerts"
199
+ end
200
+
201
+ # Email recipients for notifications
202
+ # Perhaps used by EmailReporter
203
+ def email_recipients
204
+ ["data-team@example.com", "oncall@example.com"]
205
+ end
206
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reporters are how you turn failing checks into emails, bug tracker tickets,
4
+ # or any other useful notification or report. You can notify different teams
5
+ # however they most "enjoy" hearing about bad data.
6
+
7
+ class SampleReporter < Recheck::Reporter::Base
8
+ # Optional but strongly recommended: Provide help text that appears when running `recheck
9
+ # reporters`. This should briefly explain what your reporter does and any argument it takes.
10
+ def self.help
11
+ "A template reporter showing how to implement your own custom reporter. Takes an optional argument."
12
+ end
13
+
14
+ # Optional: Initialize with the argument string from the command line.
15
+ # The arg is passed as a single string, or nil if no arg is provided
16
+ # Arguments:
17
+ # * arg: The string argument passed after the colon in the reporter specification
18
+ # For example: `--reporter SampleReporter:api_key_123` -> `api_key_123`
19
+ def initialize(arg:)
20
+ # Process your argument here if needed
21
+ @config = arg || "default_config"
22
+ end
23
+
24
+ # Important: If you define an around_ hook, it must 'yield' to run the next step.
25
+ # A hook method's return value is not used.
26
+
27
+ # around_run: Fires around the entire run.
28
+ # This is a good place to set up resources, send summary emails, etc.
29
+ # Arguments:
30
+ # * checkers: Array of checker instances that will be run
31
+ def around_run(checkers: [])
32
+ # Setup before the run
33
+ start_time = Time.now
34
+
35
+ # yield returns: a CountStats about the entire run
36
+ # CountStats tracks counts of passes, failures, and other result types
37
+ # It provides methods like #all_pass?, #any_errors?, #summary, etc.
38
+ total_counts = yield
39
+
40
+ # Teardown/reporting after the run
41
+ duration = Time.now - start_time
42
+
43
+ # Example of how you might report results
44
+ if total_counts.any_errors?
45
+ puts "SampleReporter: Found errors in #{duration.round(2)}s: #{total_counts.summary}"
46
+ # In a real reporter, you might:
47
+ # - Send an email
48
+ # - Post to Slack
49
+ # - Create a ticket in your issue tracker
50
+ # - Log to a monitoring service
51
+ end
52
+
53
+ # Return the counts (optional)
54
+ total_counts
55
+ end
56
+
57
+ # around_checker: Fires around each checker.
58
+ # Arguments:
59
+ # * checker: The checker instance being run
60
+ # * queries: Array of query method names defined on the checker
61
+ # * checks: Array of check method names defined on the checker
62
+ def around_checker(checker:, queries: [], checks: [])
63
+ # Before running this checker
64
+ checker_name = checker.class.name
65
+
66
+ # yields returns a CountStats for the checker
67
+ counts = yield
68
+
69
+ # After running this checker
70
+ if counts.any_errors?
71
+ puts "SampleReporter: Checker #{checker_name} had issues: #{counts.summary}"
72
+ # In a real reporter, you might group errors by checker
73
+ end
74
+ end
75
+
76
+ # around_query: Fires around each query
77
+ # This is useful for tracking which queries are slow or problematic
78
+ # Arguments:
79
+ # * checker: The checker instance being run
80
+ # * query: The name of the query method being executed
81
+ # * checks: Array of check method names that will be run against query results
82
+ def around_query(checker:, query:, checks: [])
83
+ # Before running this query
84
+ query_start = Time.now
85
+
86
+ # yield does not return anything for this hook
87
+ yield
88
+
89
+ # After running this query
90
+ query_duration = Time.now - query_start
91
+ if query_duration > 5 # seconds
92
+ puts "SampleReporter: Slow query #{checker.class.name}##{query} took #{query_duration.round(2)}s"
93
+ end
94
+ end
95
+
96
+ # The around_check and halt hooks both receive one of two result objects.
97
+ #
98
+ # Recheck::Pass: A successful check.
99
+ # Attributes:
100
+ # #type: always :pass
101
+ #
102
+ # Recheck::Error: A failed check.
103
+ # Attributes:
104
+ # #type: One of the following symbols
105
+ # fail: The check returned a falsey value
106
+ # exception: The check raised an exception
107
+ # blanket: The first 20 checks all failed or raised; the runner skips
108
+ # no_query_methods: The checker does not define a query_methods
109
+ # no_queries: The checker defines query_methods, but did not return any
110
+ # no_check_methods: The checker does not define a check_methods
111
+ # no_checks: The checker defines check_methods, but did not return any
112
+ # #checker: Checker instance
113
+ # #query: Query method name
114
+ # #check: Check method name
115
+ # #record: The record being checked
116
+ # #exception: rescued Exception
117
+ #
118
+
119
+ # around_check: Fires for each call to a check_ method on each record
120
+ # This is where you can collect detailed information about failures.
121
+ # Arguments:
122
+ # * checker: The checker instance being run
123
+ # * query: The name of the query method that produced this record
124
+ # * check: The name of the check method being executed
125
+ # * record: The individual record being checked
126
+ def around_check(checker:, query:, check:, record:)
127
+ # Returns a result object, see comment above
128
+ result = yield
129
+
130
+ # Process the result
131
+ if result.is_a?(Recheck::Error)
132
+ case result.type
133
+ when :fail
134
+ record_id = fetch_record_id(result.record)
135
+ puts "SampleReporter: #{checker.class.name}##{check} failed for record: #{record_id}"
136
+ when :exception
137
+ puts "SampleReporter: #{checker.class.name}##{check} raised exception: #{result.exception.message}"
138
+ end
139
+
140
+ # In a real reporter, you might:
141
+ # - Collect failures to report in a batch
142
+ # - Send immediate alerts for critical failures
143
+ # - Log to a monitoring system
144
+ end
145
+ end
146
+
147
+ # halt: Called when a checker is halted due to an error.
148
+ # This is useful for reporting fatal errors that prevent checks from running
149
+ # Arguments:
150
+ # * checker: The checker instance that was halted
151
+ # * query: The name of the query method that was running (if any)
152
+ # * check: The name of the check method that was running (if any)
153
+ # * error: The error result, see comment above around_check
154
+ def halt(checker:, query:, error:, check: nil)
155
+ puts "SampleReporter: Halted #{checker.class.name}##{query} due to #{error.type}"
156
+
157
+ # In a real reporter, you might:
158
+ # - Send an urgent alert
159
+ # - Create a high-priority ticket
160
+ # - Log a critical error
161
+ end
162
+
163
+ # You can add any helper methods, include modules, etc.
164
+
165
+ # Example of a helper method to format data for reporting
166
+ def format_error_message(error)
167
+ case error.type
168
+ when :fail
169
+ "Record is invalid"
170
+ when :exception
171
+ "Exception occurred: #{error.exception.message}"
172
+ when :blanket
173
+ "Blanket failure - first 20 checks all failed"
174
+ else
175
+ "Other error: #{error.type}"
176
+ end
177
+ end
178
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recheck
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Bhat Harkins
@@ -71,6 +71,8 @@ files:
71
71
  - lib/recheck/runner.rb
72
72
  - lib/recheck/version.rb
73
73
  - template/recheck_helper.rb
74
+ - template/regression_checker_sample.rb
75
+ - template/reporter_sample.rb
74
76
  - template/site/dns_checker.rb
75
77
  - template/site/tls_checker.rb
76
78
  - template/site/whois_checker.rb