whodunit-chronicles 0.1.0.pre → 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.
@@ -4,10 +4,10 @@ require 'concurrent-ruby'
4
4
 
5
5
  module Whodunit
6
6
  module Chronicles
7
- # Main service orchestrator for audit streaming
7
+ # Main service orchestrator for chronicle streaming
8
8
  #
9
- # Coordinates the stream adapter and audit processor to provide
10
- # a complete audit streaming solution with error handling and monitoring.
9
+ # Coordinates the stream adapter and processor to provide
10
+ # a complete chronicle streaming solution with error handling and monitoring.
11
11
  class Service
12
12
  attr_reader :adapter, :processor, :logger, :executor
13
13
 
@@ -17,7 +17,7 @@ module Whodunit
17
17
  logger: Chronicles.logger
18
18
  )
19
19
  @adapter = adapter || build_adapter
20
- @processor = processor || AuditProcessor.new(logger: logger)
20
+ @processor = processor || Processor.new(logger: logger)
21
21
  @logger = logger
22
22
  @executor = Concurrent::ThreadPoolExecutor.new(
23
23
  min_threads: 1,
@@ -29,13 +29,13 @@ module Whodunit
29
29
  @retry_count = 0
30
30
  end
31
31
 
32
- # Start the audit streaming service
32
+ # Start the chronicle streaming service
33
33
  #
34
34
  # @return [self]
35
35
  def start
36
36
  return self if running?
37
37
 
38
- log(:info, 'Starting Chronicles audit streaming service')
38
+ log(:info, 'Starting Chronicles streaming service')
39
39
 
40
40
  validate_setup!
41
41
  test_connections!
@@ -45,7 +45,7 @@ module Whodunit
45
45
 
46
46
  start_streaming_with_retry
47
47
 
48
- log(:info, 'Chronicles audit streaming service started successfully')
48
+ log(:info, 'Chronicles streaming service started successfully')
49
49
  self
50
50
  rescue StandardError => e
51
51
  log(:error, 'Failed to start service', error: e.message)
@@ -53,13 +53,13 @@ module Whodunit
53
53
  raise
54
54
  end
55
55
 
56
- # Stop the audit streaming service
56
+ # Stop the chronicle streaming service
57
57
  #
58
58
  # @return [void]
59
59
  def stop
60
60
  return unless running?
61
61
 
62
- log(:info, 'Stopping Chronicles audit streaming service')
62
+ log(:info, 'Stopping Chronicles streaming service')
63
63
  @running = false
64
64
 
65
65
  adapter.stop_streaming if adapter.streaming?
@@ -67,7 +67,7 @@ module Whodunit
67
67
  @executor.wait_for_termination(timeout: 30)
68
68
 
69
69
  processor.close
70
- log(:info, 'Chronicles audit streaming service stopped')
70
+ log(:info, 'Chronicles streaming service stopped')
71
71
  end
72
72
 
73
73
  # Check if service is running
@@ -94,23 +94,23 @@ module Whodunit
94
94
  }
95
95
  end
96
96
 
97
- # Set up the audit streaming infrastructure
97
+ # Set up the chronicle streaming infrastructure
98
98
  #
99
99
  # @return [void]
100
100
  def setup!
101
- log(:info, 'Setting up audit streaming infrastructure')
101
+ log(:info, 'Setting up chronicle streaming infrastructure')
102
102
  adapter.setup
103
- log(:info, 'Audit streaming infrastructure setup completed')
103
+ log(:info, 'Chronicle streaming infrastructure setup completed')
104
104
  end
105
105
 
106
- # Tear down the audit streaming infrastructure
106
+ # Tear down the chronicle streaming infrastructure
107
107
  #
108
108
  # @return [void]
109
109
  def teardown!
110
- log(:info, 'Tearing down audit streaming infrastructure')
110
+ log(:info, 'Tearing down chronicle streaming infrastructure')
111
111
  stop if running?
112
112
  adapter.teardown
113
- log(:info, 'Audit streaming infrastructure teardown completed')
113
+ log(:info, 'Chronicle streaming infrastructure teardown completed')
114
114
  end
115
115
 
116
116
  private
@@ -119,6 +119,8 @@ module Whodunit
119
119
  case Chronicles.config.adapter
120
120
  when :postgresql
121
121
  Adapters::PostgreSQL.new(logger: logger)
122
+ when :mysql
123
+ Adapters::MySQL.new(logger: logger)
122
124
  else
123
125
  raise ConfigurationError, "Unsupported adapter: #{Chronicles.config.adapter}"
124
126
  end
@@ -134,8 +136,8 @@ module Whodunit
134
136
 
135
137
  def test_connections!
136
138
  adapter.test_connection
137
- # Test audit processor connection by creating a dummy connection
138
- processor.send(:ensure_audit_connection)
139
+ # Test processor connection by creating a dummy connection
140
+ processor.send(:ensure_connection)
139
141
  rescue StandardError => e
140
142
  raise AdapterError, "Connection test failed: #{e.message}"
141
143
  end
@@ -159,7 +161,7 @@ module Whodunit
159
161
 
160
162
  def process_change_event(change_event)
161
163
  return unless change_event
162
- return unless should_audit_table?(change_event)
164
+ return unless should_chronicle_table?(change_event)
163
165
 
164
166
  log(:debug, 'Processing change event',
165
167
  table: change_event.qualified_table_name,
@@ -174,8 +176,8 @@ module Whodunit
174
176
  )
175
177
  end
176
178
 
177
- def should_audit_table?(change_event)
178
- Chronicles.config.audit_table?(
179
+ def should_chronicle_table?(change_event)
180
+ Chronicles.config.chronicle_table?(
179
181
  change_event.table_name,
180
182
  change_event.schema_name,
181
183
  )
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Whodunit
4
+ module Chronicles
5
+ # Handles table creation for different database adapters
6
+ #
7
+ # Provides adapter-specific SQL for creating chronicles tables
8
+ module Table
9
+ private
10
+
11
+ def ensure_table_exists
12
+ db_type = detect_database_type(@audit_database_url || Chronicles.config.database_url)
13
+
14
+ case db_type
15
+ when :postgresql
16
+ create_postgresql_table
17
+ when :mysql
18
+ create_mysql_table
19
+ end
20
+ end
21
+
22
+ def create_postgresql_table
23
+ create_sql = <<~SQL
24
+ CREATE TABLE IF NOT EXISTS whodunit_chronicles_audits (
25
+ id BIGSERIAL PRIMARY KEY,
26
+ table_name TEXT NOT NULL,
27
+ schema_name TEXT NOT NULL DEFAULT 'public',
28
+ record_id JSONB,
29
+ action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
30
+ old_data JSONB,
31
+ new_data JSONB,
32
+ changes JSONB,
33
+ user_id BIGINT,
34
+ user_type TEXT,
35
+ transaction_id TEXT,
36
+ sequence_number INTEGER,
37
+ occurred_at TIMESTAMP WITH TIME ZONE NOT NULL,
38
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
39
+ metadata JSONB DEFAULT '{}'::jsonb,
40
+ #{' '}
41
+ -- Indexes for performance
42
+ CONSTRAINT valid_data_for_action CHECK (
43
+ (action = 'INSERT' AND old_data IS NULL AND new_data IS NOT NULL) OR
44
+ (action = 'UPDATE' AND old_data IS NOT NULL AND new_data IS NOT NULL) OR#{' '}
45
+ (action = 'DELETE' AND old_data IS NOT NULL AND new_data IS NULL)
46
+ )
47
+ );
48
+
49
+ -- Performance indexes
50
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_table_record#{' '}
51
+ ON whodunit_chronicles_audits (table_name, (record_id->>'id'));
52
+
53
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_occurred_at#{' '}
54
+ ON whodunit_chronicles_audits (occurred_at DESC);
55
+
56
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_user#{' '}
57
+ ON whodunit_chronicles_audits (user_id, user_type);
58
+
59
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_action#{' '}
60
+ ON whodunit_chronicles_audits (action);
61
+
62
+ -- GIN index for JSONB columns
63
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_record_id_gin#{' '}
64
+ ON whodunit_chronicles_audits USING GIN (record_id);
65
+
66
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chronicles_audits_changes_gin#{' '}
67
+ ON whodunit_chronicles_audits USING GIN (changes);
68
+ SQL
69
+
70
+ @connection.exec(create_sql)
71
+ rescue PG::Error => e
72
+ # Ignore "already exists" errors from CONCURRENTLY
73
+ raise unless e.message.include?('already exists')
74
+ end
75
+
76
+ def create_mysql_table
77
+ create_sql = <<~SQL
78
+ CREATE TABLE IF NOT EXISTS whodunit_chronicles_audits (
79
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
80
+ table_name TEXT NOT NULL,
81
+ schema_name TEXT NOT NULL DEFAULT 'public',
82
+ record_id JSON,
83
+ action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
84
+ old_data JSON,
85
+ new_data JSON,
86
+ changes JSON,
87
+ user_id BIGINT,
88
+ user_type TEXT,
89
+ transaction_id TEXT,
90
+ sequence_number INTEGER,
91
+ occurred_at TIMESTAMP NOT NULL,
92
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
93
+ metadata JSON DEFAULT (JSON_OBJECT()),
94
+ #{' '}
95
+ -- Constraint for data integrity
96
+ CONSTRAINT valid_data_for_action CHECK (
97
+ (action = 'INSERT' AND old_data IS NULL AND new_data IS NOT NULL) OR
98
+ (action = 'UPDATE' AND old_data IS NOT NULL AND new_data IS NOT NULL) OR
99
+ (action = 'DELETE' AND old_data IS NOT NULL AND new_data IS NULL)
100
+ ),
101
+ #{' '}
102
+ -- Performance indexes
103
+ INDEX idx_chronicles_audits_table_record (table_name(255), (JSON_UNQUOTE(JSON_EXTRACT(record_id, '$.id')))),
104
+ INDEX idx_chronicles_audits_occurred_at (occurred_at DESC),
105
+ INDEX idx_chronicles_audits_user (user_id, user_type(255)),
106
+ INDEX idx_chronicles_audits_action (action(50))
107
+ );
108
+ SQL
109
+
110
+ @connection.query(create_sql)
111
+ rescue StandardError => e
112
+ # Ignore "already exists" errors
113
+ unless e.message.include?('already exists') ||
114
+ (e.message.include?('Table') && e.message.include?('already exists'))
115
+ raise
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Whodunit
4
4
  module Chronicles
5
- VERSION = '0.1.0.pre'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -8,11 +8,15 @@ require_relative 'chronicles/version'
8
8
  require_relative 'chronicles/configuration'
9
9
  require_relative 'chronicles/change_event'
10
10
  require_relative 'chronicles/stream_adapter'
11
- require_relative 'chronicles/audit_processor'
11
+ require_relative 'chronicles/connection'
12
+ require_relative 'chronicles/table'
13
+ require_relative 'chronicles/persistence'
14
+ require_relative 'chronicles/processor'
12
15
  require_relative 'chronicles/service'
13
16
 
14
17
  # Adapters
15
18
  require_relative 'chronicles/adapters/postgresql'
19
+ require_relative 'chronicles/adapters/mysql'
16
20
 
17
21
  module Whodunit
18
22
  # Chronicles - The complete historical record of `whodunit did what?` data
@@ -28,8 +32,11 @@ module Whodunit
28
32
  setting :database_url, default: ENV.fetch('DATABASE_URL', nil)
29
33
  setting :audit_database_url, default: ENV.fetch('AUDIT_DATABASE_URL', nil)
30
34
  setting :adapter, default: :postgresql
35
+ # PostgreSQL-specific settings
31
36
  setting :publication_name, default: 'whodunit_audit'
32
37
  setting :replication_slot_name, default: 'whodunit_audit_slot'
38
+ # MySQL-specific settings
39
+ setting :mysql_server_id, default: 1001
33
40
  setting :batch_size, default: 100
34
41
  setting :max_retry_attempts, default: 3
35
42
  setting :retry_delay, default: 5
@@ -46,6 +53,9 @@ module Whodunit
46
53
  # config.database_url = "postgresql://localhost/myapp"
47
54
  # config.audit_database_url = "postgresql://localhost/myapp_audit"
48
55
  # config.adapter = :postgresql
56
+ # # OR for MySQL:
57
+ # config.adapter = :mysql
58
+ # config.mysql_server_id = 1001
49
59
  # end
50
60
  def self.configure
51
61
  yield(config) if block_given?
@@ -41,7 +41,9 @@ Gem::Specification.new do |spec|
41
41
  # Database dependencies
42
42
  spec.add_dependency 'pg', '~> 1.5'
43
43
  # Driver for MySQL-compatible database
44
- # spec.add_dependency 'trilogy', '~> 2.9'
44
+ spec.add_dependency 'trilogy', '~> 2.9'
45
+ # Required for Ruby 3.4+ compatibility (trilogy dependency)
46
+ spec.add_dependency 'bigdecimal', '~> 3.1'
45
47
 
46
48
  # Development dependencies
47
49
  spec.add_development_dependency 'kramdown', '~> 2.5'
@@ -49,13 +51,15 @@ Gem::Specification.new do |spec|
49
51
  spec.add_development_dependency 'mocha', '~> 2.1'
50
52
  spec.add_development_dependency 'pry', '~> 0.14'
51
53
  spec.add_development_dependency 'rake', '~> 13.0'
54
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.6.0'
52
55
  spec.add_development_dependency 'rubocop', '~> 1.60'
53
56
  spec.add_development_dependency 'rubocop-minitest', '~> 0.34'
54
57
  spec.add_development_dependency 'rubocop-performance', '~> 1.19'
55
58
  spec.add_development_dependency 'simplecov', '~> 0.22'
59
+ spec.add_development_dependency 'simplecov-cobertura', '~> 3.0'
56
60
  spec.add_development_dependency 'yard', '~> 0.9'
57
61
 
58
62
  # Security scanning dependencies
59
- spec.add_development_dependency 'brakeman', '~> 6.0'
63
+ spec.add_development_dependency 'brakeman', '~> 7.1'
60
64
  spec.add_development_dependency 'bundler-audit', '~> 0.9'
61
65
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whodunit-chronicles
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: trilogy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.9'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bigdecimal
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.1'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.1'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: kramdown
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -136,6 +164,20 @@ dependencies:
136
164
  - - "~>"
137
165
  - !ruby/object:Gem::Version
138
166
  version: '13.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec_junit_formatter
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.6.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.6.0
139
181
  - !ruby/object:Gem::Dependency
140
182
  name: rubocop
141
183
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +234,20 @@ dependencies:
192
234
  - - "~>"
193
235
  - !ruby/object:Gem::Version
194
236
  version: '0.22'
237
+ - !ruby/object:Gem::Dependency
238
+ name: simplecov-cobertura
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '3.0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '3.0'
195
251
  - !ruby/object:Gem::Dependency
196
252
  name: yard
197
253
  requirement: !ruby/object:Gem::Requirement
@@ -212,14 +268,14 @@ dependencies:
212
268
  requirements:
213
269
  - - "~>"
214
270
  - !ruby/object:Gem::Version
215
- version: '6.0'
271
+ version: '7.1'
216
272
  type: :development
217
273
  prerelease: false
218
274
  version_requirements: !ruby/object:Gem::Requirement
219
275
  requirements:
220
276
  - - "~>"
221
277
  - !ruby/object:Gem::Version
222
- version: '6.0'
278
+ version: '7.1'
223
279
  - !ruby/object:Gem::Dependency
224
280
  name: bundler-audit
225
281
  requirement: !ruby/object:Gem::Requirement
@@ -243,6 +299,7 @@ executables: []
243
299
  extensions: []
244
300
  extra_rdoc_files: []
245
301
  files:
302
+ - ".codeclimate.yml"
246
303
  - ".rubocop.yml"
247
304
  - ".yardopts"
248
305
  - CHANGELOG.md
@@ -250,15 +307,22 @@ files:
250
307
  - LICENSE
251
308
  - README.md
252
309
  - Rakefile
310
+ - examples/images/campaign-performance-analytics.png
311
+ - examples/images/candidate-journey-analytics.png
312
+ - examples/images/recruitment-funnel-analytics.png
253
313
  - lib/.gitkeep
254
314
  - lib/whodunit-chronicles.rb
255
315
  - lib/whodunit/chronicles.rb
316
+ - lib/whodunit/chronicles/adapters/mysql.rb
256
317
  - lib/whodunit/chronicles/adapters/postgresql.rb
257
- - lib/whodunit/chronicles/audit_processor.rb
258
318
  - lib/whodunit/chronicles/change_event.rb
259
319
  - lib/whodunit/chronicles/configuration.rb
320
+ - lib/whodunit/chronicles/connection.rb
321
+ - lib/whodunit/chronicles/persistence.rb
322
+ - lib/whodunit/chronicles/processor.rb
260
323
  - lib/whodunit/chronicles/service.rb
261
324
  - lib/whodunit/chronicles/stream_adapter.rb
325
+ - lib/whodunit/chronicles/table.rb
262
326
  - lib/whodunit/chronicles/version.rb
263
327
  - whodunit-chronicles.gemspec
264
328
  homepage: https://github.com/whodunit-gem/whodunit-chronicles