whodunit-chronicles 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +50 -0
- data/.yardopts +7 -5
- data/CHANGELOG.md +78 -1
- data/README.md +28 -16
- data/lib/whodunit/chronicles/adapters/mysql.rb +261 -0
- data/lib/whodunit/chronicles/configuration.rb +23 -12
- data/lib/whodunit/chronicles/connection.rb +88 -0
- data/lib/whodunit/chronicles/persistence.rb +129 -0
- data/lib/whodunit/chronicles/processor.rb +127 -0
- data/lib/whodunit/chronicles/service.rb +23 -21
- data/lib/whodunit/chronicles/table.rb +120 -0
- data/lib/whodunit/chronicles/version.rb +1 -1
- data/lib/whodunit/chronicles.rb +11 -1
- data/whodunit-chronicles.gemspec +6 -2
- metadata +65 -4
- data/lib/whodunit/chronicles/audit_processor.rb +0 -270
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b961b73fe4f8a8b195a51a4c37078b87ebd55effbb47bef6c1128896fc567b2
|
4
|
+
data.tar.gz: ec689969f3bc3acea94f92eeca518d756b06313f8833e8af0f29066e0574199c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f32308b39334f4fc2b40fc178ce06d49cc98cd20eb284a6c45affc3561397b738781d20395284ff81b79d93c35daa2eceda598a6f057df1c655b1cea0e6dc59
|
7
|
+
data.tar.gz: 85719789afd7e4de120ec6534473d39238d9b4d58e0355cdf00e983bd51af9292e9b23cb89a7d7acce086f610527dc41fb01bd699169520c77881ffee32d185c
|
data/.codeclimate.yml
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
version: "2"
|
2
|
+
checks:
|
3
|
+
argument-count:
|
4
|
+
config:
|
5
|
+
threshold: 4
|
6
|
+
complex-logic:
|
7
|
+
config:
|
8
|
+
threshold: 4
|
9
|
+
file-lines:
|
10
|
+
config:
|
11
|
+
threshold: 250
|
12
|
+
method-complexity:
|
13
|
+
config:
|
14
|
+
threshold: 5
|
15
|
+
method-count:
|
16
|
+
config:
|
17
|
+
threshold: 20
|
18
|
+
method-lines:
|
19
|
+
config:
|
20
|
+
threshold: 25
|
21
|
+
nested-control-flow:
|
22
|
+
config:
|
23
|
+
threshold: 4
|
24
|
+
return-statements:
|
25
|
+
config:
|
26
|
+
threshold: 4
|
27
|
+
similar-code:
|
28
|
+
config:
|
29
|
+
threshold: # language-specific defaults. an integer indicates the minimum number of lines within a block of similar code.
|
30
|
+
identical-code:
|
31
|
+
config:
|
32
|
+
threshold: # language-specific defaults. an integer indicates the minimum number of lines within a block of identical code.
|
33
|
+
plugins:
|
34
|
+
rubocop:
|
35
|
+
enabled: true
|
36
|
+
config:
|
37
|
+
file: .rubocop.yml
|
38
|
+
exclude_patterns:
|
39
|
+
- "config/"
|
40
|
+
- "db/"
|
41
|
+
- "dist/"
|
42
|
+
- "features/"
|
43
|
+
- "**/node_modules/"
|
44
|
+
- "script/"
|
45
|
+
- "**/spec/"
|
46
|
+
- "**/test/"
|
47
|
+
- "**/tests/"
|
48
|
+
- "**/vendor/"
|
49
|
+
- "**/*_test.rb"
|
50
|
+
- "**/*_spec.rb"
|
data/.yardopts
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
--markup markdown
|
2
2
|
--markup-provider kramdown
|
3
|
-
--output-dir docs
|
4
|
-
--exclude spec/
|
5
|
-
--exclude vendor/
|
6
3
|
--main README.md
|
7
|
-
--
|
8
|
-
--
|
4
|
+
--output-dir docs
|
5
|
+
--protected
|
6
|
+
--private
|
7
|
+
--title "Whodunit Chronicles API Documentation"
|
8
|
+
--readme README.md
|
9
|
+
--files CHANGELOG.md,LICENSE
|
9
10
|
lib/**/*.rb
|
10
11
|
-
|
11
12
|
README.md
|
12
13
|
CHANGELOG.md
|
14
|
+
LICENSE
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,83 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [0.2.0] - 2025-01-28
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- **MySQL/MariaDB Support**: Complete multi-database adapter architecture
|
15
|
+
- MySQL adapter using trilogy gem for high-performance connections
|
16
|
+
- Binary log streaming support for MySQL change capture
|
17
|
+
- Cross-database compatibility testing
|
18
|
+
- **Enhanced Testing Suite**: Comprehensive test coverage improvements
|
19
|
+
- New test files: `table_test.rb`, `connection_test.rb`, `persistence_test.rb`
|
20
|
+
- Enhanced PostgreSQL adapter tests with connection and replication scenarios
|
21
|
+
- Increased line coverage from 91.28% to 97.29% (+6.01 percentage points)
|
22
|
+
- 227 tests with 552 assertions providing robust validation
|
23
|
+
- **Ruby 3.4+ Compatibility**: Forward compatibility improvements
|
24
|
+
- Added `bigdecimal` dependency for Ruby 3.4+ support
|
25
|
+
- Explicit dependency management for removed stdlib components
|
26
|
+
- **CI/CD Enhancements**: Improved automation and quality gates
|
27
|
+
- Matrix testing across PostgreSQL and MySQL databases
|
28
|
+
- Enhanced MySQL integration testing with proper connection handling
|
29
|
+
- Security scanning integration and automated dependency updates
|
30
|
+
|
31
|
+
### Changed
|
32
|
+
|
33
|
+
- **Architecture Refactoring**: Modular component extraction
|
34
|
+
- Extracted AuditProcessor into separate, focused components
|
35
|
+
- Improved service layer with multi-adapter support patterns
|
36
|
+
- Enhanced configuration system supporting both PostgreSQL and MySQL
|
37
|
+
- **Database Adapter Pattern**: Extensible multi-database support
|
38
|
+
- Abstract adapter base class for consistent interface
|
39
|
+
- Database-specific implementations with optimized performance
|
40
|
+
- Unified change event system across different database types
|
41
|
+
- **Test Infrastructure**: Comprehensive testing improvements
|
42
|
+
- Enhanced mock-based testing for complex database operations
|
43
|
+
- Improved test organization with better separation of concerns
|
44
|
+
- Integration test scenarios for real-world usage patterns
|
45
|
+
|
46
|
+
### Fixed
|
47
|
+
|
48
|
+
- **MySQL CI Integration**: Resolved connection and setup issues
|
49
|
+
- Fixed MySQL container configuration and health checks
|
50
|
+
- Improved database readiness detection and timeout handling
|
51
|
+
- Enhanced error reporting and debugging for CI environments
|
52
|
+
- **Dependency Management**: Ruby version compatibility
|
53
|
+
- Added explicit `bigdecimal ~> 3.1` dependency for Ruby 3.4+
|
54
|
+
- Resolved trilogy gem loading issues in newer Ruby versions
|
55
|
+
- Improved gem specification with proper version constraints
|
56
|
+
|
57
|
+
### Technical Improvements
|
58
|
+
|
59
|
+
- **Code Coverage**: Significant testing improvements
|
60
|
+
- Line coverage: 97.29% (647/665 lines covered)
|
61
|
+
- Branch coverage: 83.6% (158/189 branches covered)
|
62
|
+
- Comprehensive unit tests for all core modules
|
63
|
+
- **Performance Optimizations**: Multi-adapter efficiency
|
64
|
+
- Database-specific SQL generation and parameter binding
|
65
|
+
- Optimized connection management across different adapters
|
66
|
+
- Efficient batch processing for both PostgreSQL and MySQL
|
67
|
+
- **Error Handling**: Enhanced resilience and debugging
|
68
|
+
- Improved error messages and stack trace reporting
|
69
|
+
- Better handling of database-specific error conditions
|
70
|
+
- Enhanced logging for troubleshooting and monitoring
|
71
|
+
|
72
|
+
### Development Experience
|
73
|
+
|
74
|
+
- **Documentation**: Enhanced developer resources
|
75
|
+
- Updated README with MySQL/MariaDB configuration examples
|
76
|
+
- Improved inline documentation for multi-adapter usage
|
77
|
+
- Better error messages and troubleshooting guides
|
78
|
+
- **Testing Framework**: Improved development workflow
|
79
|
+
- Faster test execution with better mock strategies
|
80
|
+
- More reliable CI/CD pipeline with matrix testing
|
81
|
+
- Enhanced debugging capabilities for test failures
|
82
|
+
|
83
|
+
## [0.1.0] - 2025-01-21
|
84
|
+
|
8
85
|
### Added
|
9
86
|
|
10
87
|
- Comprehensive GitHub Actions CI/CD pipeline with multi-Ruby testing
|
@@ -30,7 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
30
107
|
- **Core Architecture**: Complete zero-latency audit streaming implementation
|
31
108
|
- **PostgreSQL Adapter**: Logical replication streaming with WAL decoding
|
32
109
|
- **ChangeEvent System**: Unified change representation across database adapters
|
33
|
-
- **
|
110
|
+
- **Processor**: Intelligent transformation of changes into audit records
|
34
111
|
- **Configuration Management**: Comprehensive settings with validation using dry-configurable
|
35
112
|
- **Service Orchestration**: Thread-safe service with error handling and retry logic
|
36
113
|
- **Abstract Adapter Pattern**: Extensible design supporting multiple database systems
|
data/README.md
CHANGED
@@ -14,9 +14,9 @@ While [Whodunit](https://github.com/kanutocd/whodunit) tracks _who_ made changes
|
|
14
14
|
|
15
15
|
## ✨ Features
|
16
16
|
|
17
|
-
- **🚄 Zero-Latency Streaming**: PostgreSQL logical replication
|
17
|
+
- **🚄 Zero-Latency Streaming**: PostgreSQL logical replication + MySQL/MariaDB binary log streaming
|
18
18
|
- **🔄 Zero Application Overhead**: No Rails callbacks or Active Record hooks required
|
19
|
-
- **🏗️ Database Agnostic**: Abstract adapter pattern supports PostgreSQL
|
19
|
+
- **🏗️ Database Agnostic**: Abstract adapter pattern supports PostgreSQL and MySQL/MariaDB
|
20
20
|
- **⚡ Thread-Safe**: Concurrent processing with configurable thread pools
|
21
21
|
- **🛡️ Resilient**: Built-in error handling, retry logic, and monitoring
|
22
22
|
- **📊 Complete Audit Trail**: Captures INSERT, UPDATE, DELETE with full before/after data
|
@@ -36,7 +36,7 @@ Perfect for applications that need comprehensive change tracking alongside Whodu
|
|
36
36
|
|
37
37
|
```ruby
|
38
38
|
# Basic setup for user activity tracking
|
39
|
-
class
|
39
|
+
class BasicProcessor < Whodunit::Chronicles::Processor
|
40
40
|
def build_chronicles_record(change_event)
|
41
41
|
super.tap do |record|
|
42
42
|
# Add basic business context
|
@@ -66,7 +66,7 @@ Sophisticated business intelligence for talent acquisition platforms:
|
|
66
66
|
|
67
67
|
```ruby
|
68
68
|
# Advanced processor for recruitment metrics
|
69
|
-
class RecruitmentAnalyticsProcessor < Whodunit::Chronicles::
|
69
|
+
class RecruitmentAnalyticsProcessor < Whodunit::Chronicles::Processor
|
70
70
|
def build_chronicles_record(change_event)
|
71
71
|
super.tap do |record|
|
72
72
|
# Add recruitment-specific business metrics
|
@@ -151,19 +151,19 @@ The recruitment analytics processor creates comprehensive Grafana dashboards for
|
|
151
151
|
<a href="examples/images/campaign-performance-analytics.png" title="Click to view full size image">
|
152
152
|
<img src="examples/images/campaign-performance-analytics.png" width="300" />
|
153
153
|
</a>
|
154
|
-
|
154
|
+
_Track campaign ROI, cost-per-hire by channel, and conversion rates across marketing sources_
|
155
155
|
|
156
|
-
**Candidate Journey Analytics**
|
156
|
+
**Candidate Journey Analytics**
|
157
157
|
<a href="examples/images/candidate-journey-analytics.png" title="Click to view full size image">
|
158
158
|
<img src="examples/images/candidate-journey-analytics.png" width="300" />
|
159
159
|
</a>
|
160
|
-
|
160
|
+
_Monitor candidate engagement, funnel conversion rates, and application completion patterns_
|
161
161
|
|
162
162
|
**Recruitment Funnel Analytics**
|
163
163
|
<a href="examples/images/recruitment-funnel-analytics.png" title="Click to view full size image">
|
164
164
|
<img src="examples/images/recruitment-funnel-analytics.png" width="300" />
|
165
165
|
</a>
|
166
|
-
|
166
|
+
_Analyze hiring pipeline progression, department performance, and time-series trends_
|
167
167
|
|
168
168
|
</div>
|
169
169
|
|
@@ -188,7 +188,9 @@ gem install whodunit-chronicles
|
|
188
188
|
```ruby
|
189
189
|
require 'whodunit/chronicles'
|
190
190
|
|
191
|
-
#
|
191
|
+
# Database Configuration
|
192
|
+
|
193
|
+
## PostgreSQL Configuration
|
192
194
|
Whodunit::Chronicles.configure do |config|
|
193
195
|
config.adapter = :postgresql
|
194
196
|
config.database_url = 'postgresql://localhost/myapp_production'
|
@@ -197,6 +199,14 @@ Whodunit::Chronicles.configure do |config|
|
|
197
199
|
config.replication_slot_name = 'myapp_chronicles_slot'
|
198
200
|
end
|
199
201
|
|
202
|
+
## MySQL/MariaDB Configuration
|
203
|
+
Whodunit::Chronicles.configure do |config|
|
204
|
+
config.adapter = :mysql
|
205
|
+
config.database_url = 'mysql://user:password@localhost/myapp_production'
|
206
|
+
config.audit_database_url = 'mysql://user:password@localhost/myapp_audit'
|
207
|
+
config.mysql_server_id = 1001 # Unique server ID for replication
|
208
|
+
end
|
209
|
+
|
200
210
|
# Create and start the service
|
201
211
|
service = Whodunit::Chronicles.service
|
202
212
|
service.setup! # Create publication/replication setup
|
@@ -212,7 +222,7 @@ service.teardown! # Clean up database objects
|
|
212
222
|
|
213
223
|
## 🏗️ Architecture
|
214
224
|
|
215
|
-
Chronicles uses **PostgreSQL logical replication**
|
225
|
+
Chronicles uses **PostgreSQL logical replication** and **MySQL/MariaDB binary log streaming** to capture database changes without impacting your application:
|
216
226
|
|
217
227
|
```
|
218
228
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
@@ -233,9 +243,9 @@ Chronicles uses **PostgreSQL logical replication** (TODO: **MySQL/MariaDB binary
|
|
233
243
|
|
234
244
|
### Core Components
|
235
245
|
|
236
|
-
- **StreamAdapter**: Database-specific change streaming (PostgreSQL, MySQL/MariaDB)
|
246
|
+
- **StreamAdapter**: Database-specific change streaming (PostgreSQL logical replication, MySQL/MariaDB binary log streaming)
|
237
247
|
- **ChangeEvent**: Unified change representation across adapters
|
238
|
-
- **
|
248
|
+
- **Processor**: Transforms changes into searchable audit records
|
239
249
|
- **Service**: Orchestrates streaming with error handling and retry logic
|
240
250
|
|
241
251
|
## ⚙️ Configuration
|
@@ -311,7 +321,7 @@ Transform database changes into actionable business intelligence with features l
|
|
311
321
|
#### Analytics-Focused Processor
|
312
322
|
|
313
323
|
```ruby
|
314
|
-
class AnalyticsProcessor < Whodunit::Chronicles::
|
324
|
+
class AnalyticsProcessor < Whodunit::Chronicles::Processor
|
315
325
|
def build_chronicles_record(change_event)
|
316
326
|
super.tap do |record|
|
317
327
|
# Add business metrics
|
@@ -365,7 +375,7 @@ end
|
|
365
375
|
#### Grafana Dashboard Ready
|
366
376
|
|
367
377
|
```ruby
|
368
|
-
class GrafanaProcessor < Whodunit::Chronicles::
|
378
|
+
class GrafanaProcessor < Whodunit::Chronicles::Processor
|
369
379
|
def build_chronicles_record(change_event)
|
370
380
|
{
|
371
381
|
# Core metrics for Grafana time series
|
@@ -399,7 +409,7 @@ end
|
|
399
409
|
#### Real-Time Alerts Processor
|
400
410
|
|
401
411
|
```ruby
|
402
|
-
class AlertingProcessor < Whodunit::Chronicles::
|
412
|
+
class AlertingProcessor < Whodunit::Chronicles::Processor
|
403
413
|
def process(change_event)
|
404
414
|
record = build_chronicles_record(change_event)
|
405
415
|
|
@@ -615,13 +625,14 @@ We especially welcome custom processors for different business domains. Consider
|
|
615
625
|
|
616
626
|
- **Ruby**: 3.1.0 or higher
|
617
627
|
- **PostgreSQL**: 10.0 or higher (with logical replication enabled)
|
628
|
+
- **MySQL/MariaDB**: 5.6+ (with binary logging enabled)
|
618
629
|
|
619
630
|
## 🗺️ Roadmap
|
620
631
|
|
621
632
|
- [ ] **Prometheus Metrics**: Production monitoring integration (with complete codebase included in examples/)
|
622
633
|
- [ ] **Advanced Example Apps**: Real-world use cases with complete monitoring stack (with complete codebase included in examples/)
|
623
634
|
- [ ] **Custom Analytics Processors**: Business intelligence and real-time monitoring (with complete codebase included in examples/)
|
624
|
-
- [
|
635
|
+
- [x] **MySQL/MariaDB Support**: MySQL/MariaDB databases binlog streaming adapter
|
625
636
|
- [ ] **Redis Streams**: Alternative lightweight streaming backend
|
626
637
|
- [ ] **Compression**: Optional audit record compression
|
627
638
|
- [ ] **Retention Policies**: Automated audit record cleanup
|
@@ -633,6 +644,7 @@ We especially welcome custom processors for different business domains. Consider
|
|
633
644
|
- **[Configuration Guide](docs/configuration-todo.md)**
|
634
645
|
- **[Architecture Deep Dive](docs/architecture-todo.md)**
|
635
646
|
- **[PostgreSQL Setup](docs/postgresql-setup-todo.md)**
|
647
|
+
- **[MySQL/MariaDB Setup](docs/mysql-setup.md)**
|
636
648
|
- **[Production Deployment](docs/production-todo.md)**
|
637
649
|
|
638
650
|
## 📄 License
|
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'trilogy'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Whodunit
|
7
|
+
module Chronicles
|
8
|
+
module Adapters
|
9
|
+
# MySQL/MariaDB binary log streaming adapter
|
10
|
+
#
|
11
|
+
# Uses MySQL's binary log replication to stream database changes
|
12
|
+
# without impacting application performance.
|
13
|
+
class MySQL < StreamAdapter
|
14
|
+
DEFAULT_SERVER_ID = 1001
|
15
|
+
|
16
|
+
attr_reader :connection, :database_url, :server_id, :binlog_file, :binlog_position
|
17
|
+
|
18
|
+
def initialize(
|
19
|
+
database_url: Chronicles.config.database_url,
|
20
|
+
server_id: DEFAULT_SERVER_ID,
|
21
|
+
logger: Chronicles.logger
|
22
|
+
)
|
23
|
+
super(logger: logger)
|
24
|
+
@database_url = database_url
|
25
|
+
@server_id = server_id
|
26
|
+
@connection = nil
|
27
|
+
@binlog_file = nil
|
28
|
+
@binlog_position = nil
|
29
|
+
@binlog_checksum = true
|
30
|
+
end
|
31
|
+
|
32
|
+
# Start streaming binary log changes
|
33
|
+
def start_streaming(&)
|
34
|
+
raise ArgumentError, 'Block required for processing events' unless block_given?
|
35
|
+
|
36
|
+
log(:info, 'Starting MySQL binary log streaming')
|
37
|
+
|
38
|
+
establish_connection
|
39
|
+
ensure_setup
|
40
|
+
|
41
|
+
self.running = true
|
42
|
+
fetch_current_position
|
43
|
+
|
44
|
+
log(:info, 'Starting replication from position',
|
45
|
+
file: @binlog_file, position: @binlog_position)
|
46
|
+
|
47
|
+
begin
|
48
|
+
stream_binlog_events(&)
|
49
|
+
rescue StandardError => e
|
50
|
+
log(:error, 'Streaming error', error: e.message, backtrace: e.backtrace.first(5))
|
51
|
+
raise ReplicationError, "Failed to stream changes: #{e.message}"
|
52
|
+
ensure
|
53
|
+
self.running = false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Stop streaming
|
58
|
+
def stop_streaming
|
59
|
+
log(:info, 'Stopping MySQL binary log streaming')
|
60
|
+
self.running = false
|
61
|
+
close_connection
|
62
|
+
end
|
63
|
+
|
64
|
+
# Get current replication position
|
65
|
+
def current_position
|
66
|
+
return "#{@binlog_file}:#{@binlog_position}" if @binlog_file && @binlog_position
|
67
|
+
|
68
|
+
fetch_current_position
|
69
|
+
"#{@binlog_file}:#{@binlog_position}"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set up binary log replication
|
73
|
+
def setup
|
74
|
+
log(:info, 'Setting up MySQL binary log replication')
|
75
|
+
|
76
|
+
establish_connection
|
77
|
+
validate_binlog_format
|
78
|
+
validate_server_id
|
79
|
+
enable_binlog_checksum
|
80
|
+
|
81
|
+
log(:info, 'MySQL setup completed successfully')
|
82
|
+
end
|
83
|
+
|
84
|
+
# Remove binary log replication setup (minimal cleanup needed)
|
85
|
+
def teardown
|
86
|
+
log(:info, 'Tearing down MySQL binary log replication')
|
87
|
+
close_connection
|
88
|
+
log(:info, 'MySQL teardown completed')
|
89
|
+
end
|
90
|
+
|
91
|
+
# Test database connection
|
92
|
+
def test_connection
|
93
|
+
establish_connection
|
94
|
+
result = @connection.query('SELECT @@hostname, @@version, @@server_id')
|
95
|
+
info = result.first
|
96
|
+
|
97
|
+
log(:info, 'Connection test successful',
|
98
|
+
hostname: info['@@hostname'],
|
99
|
+
version: info['@@version'],
|
100
|
+
server_id: info['@@server_id'])
|
101
|
+
|
102
|
+
true
|
103
|
+
rescue StandardError => e
|
104
|
+
log(:error, 'Connection test failed', error: e.message)
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def establish_connection
|
111
|
+
return if @connection&.ping
|
112
|
+
|
113
|
+
parsed_url = parse_database_url(@database_url)
|
114
|
+
|
115
|
+
@connection = Trilogy.new(
|
116
|
+
host: parsed_url[:host],
|
117
|
+
port: parsed_url[:port] || 3306,
|
118
|
+
username: parsed_url[:username],
|
119
|
+
password: parsed_url[:password],
|
120
|
+
database: parsed_url[:database],
|
121
|
+
ssl: parsed_url[:ssl],
|
122
|
+
)
|
123
|
+
|
124
|
+
log(:debug, 'Established MySQL connection',
|
125
|
+
host: parsed_url[:host],
|
126
|
+
database: parsed_url[:database])
|
127
|
+
rescue StandardError => e
|
128
|
+
log(:error, 'Failed to establish connection', error: e.message)
|
129
|
+
raise AdapterError, "Connection failed: #{e.message}"
|
130
|
+
end
|
131
|
+
|
132
|
+
def close_connection
|
133
|
+
@connection&.close
|
134
|
+
@connection = nil
|
135
|
+
end
|
136
|
+
|
137
|
+
def parse_database_url(url)
|
138
|
+
uri = URI.parse(url)
|
139
|
+
{
|
140
|
+
host: uri.host,
|
141
|
+
port: uri.port,
|
142
|
+
username: uri.user,
|
143
|
+
password: uri.password,
|
144
|
+
database: uri.path&.sub('/', ''),
|
145
|
+
ssl: uri.query&.include?('ssl=true'),
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def ensure_setup
|
150
|
+
validate_binlog_format
|
151
|
+
validate_server_id
|
152
|
+
end
|
153
|
+
|
154
|
+
def validate_binlog_format
|
155
|
+
result = @connection.query('SELECT @@binlog_format')
|
156
|
+
format = result.first['@@binlog_format']
|
157
|
+
|
158
|
+
unless %w[ROW MIXED].include?(format)
|
159
|
+
raise ReplicationError,
|
160
|
+
"Binary log format must be ROW or MIXED, currently: #{format}"
|
161
|
+
end
|
162
|
+
|
163
|
+
log(:debug, 'Binary log format validated', format: format)
|
164
|
+
end
|
165
|
+
|
166
|
+
def validate_server_id
|
167
|
+
result = @connection.query('SELECT @@server_id')
|
168
|
+
current_server_id = result.first['@@server_id'].to_i
|
169
|
+
|
170
|
+
if current_server_id == @server_id
|
171
|
+
raise ReplicationError,
|
172
|
+
"Server ID conflict: #{@server_id} is already in use"
|
173
|
+
end
|
174
|
+
|
175
|
+
log(:debug, 'Server ID validated',
|
176
|
+
current: current_server_id,
|
177
|
+
replication: @server_id)
|
178
|
+
end
|
179
|
+
|
180
|
+
def enable_binlog_checksum
|
181
|
+
@connection.query('SET @master_binlog_checksum = @@global.binlog_checksum')
|
182
|
+
log(:debug, 'Binary log checksum enabled')
|
183
|
+
end
|
184
|
+
|
185
|
+
def fetch_current_position
|
186
|
+
result = @connection.query('SHOW MASTER STATUS')
|
187
|
+
status = result.first
|
188
|
+
|
189
|
+
raise ReplicationError, 'Unable to fetch master status - binary logging may be disabled' unless status
|
190
|
+
|
191
|
+
@binlog_file = status['File']
|
192
|
+
@binlog_position = status['Position']
|
193
|
+
log(:debug, 'Fetched master position',
|
194
|
+
file: @binlog_file,
|
195
|
+
position: @binlog_position)
|
196
|
+
end
|
197
|
+
|
198
|
+
def stream_binlog_events(&)
|
199
|
+
# Register as replica server
|
200
|
+
register_replica_server
|
201
|
+
|
202
|
+
# Request binary log dump
|
203
|
+
request_binlog_dump
|
204
|
+
|
205
|
+
# Process binary log events
|
206
|
+
process_binlog_stream(&)
|
207
|
+
rescue StandardError => e
|
208
|
+
log(:error, 'Binary log streaming error', error: e.message)
|
209
|
+
raise
|
210
|
+
end
|
211
|
+
|
212
|
+
def register_replica_server
|
213
|
+
# This would typically use COM_REGISTER_SLAVE MySQL protocol command
|
214
|
+
# For now, we'll use a simplified approach
|
215
|
+
log(:debug, 'Registering as replica server', server_id: @server_id)
|
216
|
+
|
217
|
+
# NOTE: Full implementation would require low-level MySQL protocol handling
|
218
|
+
# This is a placeholder for the binary log streaming setup
|
219
|
+
end
|
220
|
+
|
221
|
+
def request_binlog_dump
|
222
|
+
log(:debug, 'Requesting binary log dump',
|
223
|
+
file: @binlog_file,
|
224
|
+
position: @binlog_position)
|
225
|
+
|
226
|
+
# This would use COM_BINLOG_DUMP MySQL protocol command
|
227
|
+
# Full implementation requires binary protocol handling
|
228
|
+
end
|
229
|
+
|
230
|
+
def process_binlog_stream(&)
|
231
|
+
# This would process the binary log event stream
|
232
|
+
# Each event would be parsed and converted to a ChangeEvent
|
233
|
+
|
234
|
+
log(:info, 'Processing binary log stream (placeholder implementation)')
|
235
|
+
|
236
|
+
# Placeholder: In a real implementation, this would:
|
237
|
+
# 1. Read binary log events from the stream
|
238
|
+
# 2. Parse event headers and data
|
239
|
+
# 3. Convert to ChangeEvent objects
|
240
|
+
# 4. Yield each event to the block
|
241
|
+
|
242
|
+
# For now, we'll simulate with a warning
|
243
|
+
log(:warn, 'MySQL binary log streaming requires full protocol implementation')
|
244
|
+
|
245
|
+
# Yield a placeholder change event to demonstrate the interface
|
246
|
+
change_event = ChangeEvent.new(
|
247
|
+
table_name: 'example_table',
|
248
|
+
action: 'INSERT',
|
249
|
+
primary_key: { id: 1 },
|
250
|
+
new_data: { id: 1, name: 'test' },
|
251
|
+
old_data: nil,
|
252
|
+
timestamp: Time.now,
|
253
|
+
metadata: { position: current_position },
|
254
|
+
)
|
255
|
+
|
256
|
+
yield(change_event) if block_given?
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -30,21 +30,20 @@ module Whodunit
|
|
30
30
|
# @raise [ConfigurationError] if configuration is invalid
|
31
31
|
def validate!
|
32
32
|
raise ConfigurationError, 'database_url is required' if database_url.nil?
|
33
|
-
raise ConfigurationError, 'adapter must be :postgresql' unless adapter
|
33
|
+
raise ConfigurationError, 'adapter must be :postgresql or :mysql' unless %i[postgresql mysql].include?(adapter)
|
34
34
|
raise ConfigurationError, 'batch_size must be positive' unless batch_size.positive?
|
35
35
|
raise ConfigurationError, 'max_retry_attempts must be positive' unless max_retry_attempts.positive?
|
36
36
|
raise ConfigurationError, 'retry_delay must be positive' unless retry_delay.positive?
|
37
37
|
|
38
|
-
|
39
|
-
validate_slot_name!
|
38
|
+
validate_adapter_specific_settings!
|
40
39
|
end
|
41
40
|
|
42
|
-
# Check if a table should be
|
41
|
+
# Check if a table should be chronicled based on filters
|
43
42
|
#
|
44
43
|
# @param table_name [String] The table name to check
|
45
44
|
# @param schema_name [String] The schema name to check
|
46
|
-
# @return [Boolean] true if the table should be
|
47
|
-
def
|
45
|
+
# @return [Boolean] true if the table should be chronicled
|
46
|
+
def chronicle_table?(table_name, schema_name = 'public')
|
48
47
|
return false if filtered_by_schema?(schema_name)
|
49
48
|
return false if filtered_by_table?(table_name)
|
50
49
|
|
@@ -53,18 +52,30 @@ module Whodunit
|
|
53
52
|
|
54
53
|
private
|
55
54
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
|
55
|
+
def validate_adapter_specific_settings!
|
56
|
+
case adapter
|
57
|
+
when :postgresql
|
58
|
+
validate_postgresql_settings!
|
59
|
+
when :mysql
|
60
|
+
validate_mysql_settings!
|
61
|
+
end
|
60
62
|
end
|
61
63
|
|
62
|
-
def
|
63
|
-
|
64
|
+
def validate_postgresql_settings!
|
65
|
+
if publication_name && !/\A[a-zA-Z_][a-zA-Z0-9_]*\z/.match?(publication_name)
|
66
|
+
raise ConfigurationError, 'publication_name must be a valid PostgreSQL identifier'
|
67
|
+
end
|
68
|
+
|
69
|
+
return unless replication_slot_name && !/\A[a-zA-Z_][a-zA-Z0-9_]*\z/.match?(replication_slot_name)
|
64
70
|
|
65
71
|
raise ConfigurationError, 'replication_slot_name must be a valid PostgreSQL identifier'
|
66
72
|
end
|
67
73
|
|
74
|
+
def validate_mysql_settings!
|
75
|
+
# MySQL-specific validations can be added here in the future
|
76
|
+
# For now, MySQL settings are less restrictive
|
77
|
+
end
|
78
|
+
|
68
79
|
def filtered_by_schema?(schema_name)
|
69
80
|
return false unless schema_filter
|
70
81
|
|