whodunit-chronicles 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/CHANGELOG.md +48 -1
- data/CONTRIBUTING.md +186 -0
- data/README.md +7 -7
- data/docker/mysql/init.sql +33 -0
- data/docker/postgres/init.sql +40 -0
- data/docker-compose.yml +138 -0
- data/lib/whodunit/chronicles/adapter_loader.rb +69 -0
- data/lib/whodunit/chronicles/adapters/mysql.rb +2 -2
- data/lib/whodunit/chronicles/adapters/postgresql.rb +1 -1
- data/lib/whodunit/chronicles/change_event.rb +2 -2
- data/lib/whodunit/chronicles/composite_processor.rb +86 -0
- data/lib/whodunit/chronicles/errors.rb +43 -0
- data/lib/whodunit/chronicles/service.rb +4 -4
- data/lib/whodunit/chronicles/version.rb +1 -1
- data/lib/whodunit/chronicles.rb +3 -8
- data/whodunit-chronicles.gemspec +24 -10
- metadata +44 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa4438fb3140657b7b9d92db2988c0ca511c9fc0138ba42cfad061ab91dc0ac4
|
|
4
|
+
data.tar.gz: e3c5339e3ecce1b882be854bc906f9e1644bc812eb16302266d7b8ef68ba0f12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '03590c69049ab8670411b6beb0e5bbb8d3fcf0989b163541fa606be5fc1c7b41f20c4e6cb5160632ed411b056664f16aa153499c947cd24c612912ff58504354'
|
|
7
|
+
data.tar.gz: 26d9cbc11e8efbf0aa716034b9ec46604d5bb78e066584f3e9674ef00732daf60fc7ba6a5506c3f9524b0ad216fcfb52e4418b2452451787b6c4498e84bab08a
|
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ruby version
|
|
2
2
|
AllCops:
|
|
3
|
-
TargetRubyVersion: 3.
|
|
3
|
+
TargetRubyVersion: 3.2
|
|
4
4
|
NewCops: enable
|
|
5
5
|
Exclude:
|
|
6
6
|
- 'vendor/**/*'
|
|
@@ -54,8 +54,8 @@ Style/FrozenStringLiteralComment:
|
|
|
54
54
|
Enabled: true
|
|
55
55
|
EnforcedStyle: always
|
|
56
56
|
|
|
57
|
-
Style/StringLiterals:
|
|
58
|
-
|
|
57
|
+
# Style/StringLiterals:
|
|
58
|
+
# EnforcedStyle: double_quotes
|
|
59
59
|
|
|
60
60
|
Style/HashSyntax:
|
|
61
61
|
EnforcedStyle: ruby19
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,54 @@ 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
|
-
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-05-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`CompositeProcessor`** — fans out a single change event to multiple processors
|
|
14
|
+
in sequence, enabling pipeline architectures (e.g. simultaneously storing audit
|
|
15
|
+
records, streaming to Grafana, and triggering alerts) without coupling processors
|
|
16
|
+
to each other. Fail-open by default: if one processor raises, the error is logged
|
|
17
|
+
and the remaining processors still execute. Set `fail_fast: true` to halt the
|
|
18
|
+
chain on the first error.
|
|
19
|
+
- **Typed error hierarchy** — base `Whodunit::Chronicles::Error` class with typed
|
|
20
|
+
subclasses: `ConfigurationError`, `AdapterLoadError`, `ConnectionError`,
|
|
21
|
+
`ProcessingError`, `PersistenceError`. Callers can now rescue the base class to
|
|
22
|
+
catch all gem errors, or rescue specific subclasses for targeted handling.
|
|
23
|
+
- **Lazy adapter loading** — `pg` and `trilogy` are now required only when the
|
|
24
|
+
matching adapter is actually used. A friendly `AdapterLoadError` with install
|
|
25
|
+
instructions is raised if the gem is missing, instead of a cryptic `LoadError`.
|
|
26
|
+
- **`bin/console`** — loads the gem with a sane test config and drops into Pry
|
|
27
|
+
for interactive development.
|
|
28
|
+
- **Docker Compose test environment** — spins up PostgreSQL 16 (`wal_level=logical`),
|
|
29
|
+
MySQL 8, and MariaDB 11 with matching audit databases, init SQL scripts, test
|
|
30
|
+
tables, publication setup, and replication role grants.
|
|
31
|
+
- Development environment setup guide added to docs.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Gemspec `homepage_uri` metadata corrected.
|
|
36
|
+
- `pg`, `trilogy`, and `bigdecimal` moved from runtime to `add_development_dependency`
|
|
37
|
+
— consuming apps are no longer forced to install database adapters they don't use.
|
|
38
|
+
- `rubocop-rake` and `rubocop-thread_safety` added to development dependencies.
|
|
39
|
+
|
|
40
|
+
### Chore
|
|
41
|
+
|
|
42
|
+
- `Gemfile.lock` removed from repo.
|
|
43
|
+
- `.gitignore` updated with recommended exclusions.
|
|
44
|
+
|
|
45
|
+
### Docs
|
|
46
|
+
|
|
47
|
+
- Broken links in README fixed.
|
|
48
|
+
- Development environment setup guide added.
|
|
49
|
+
|
|
50
|
+
### ⚠️ Breaking Changes
|
|
51
|
+
|
|
52
|
+
- `pg`, `trilogy`, and `bigdecimal` are no longer runtime dependencies. If your
|
|
53
|
+
application relies on them being pulled in transitively via `whodunit-chronicles`,
|
|
54
|
+
add them explicitly to your own Gemfile.
|
|
55
|
+
- remove ruby@3.1 from the supported versions of Ruby
|
|
9
56
|
|
|
10
57
|
## [0.2.0] - 2025-01-28
|
|
11
58
|
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Contributing to Whodunit Chronicles
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing! This document walks you through getting a working development environment set up, running the test suite, and submitting a pull request.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Prerequisites](#prerequisites)
|
|
10
|
+
2. [Setting Up Your Environment](#setting-up-your-environment)
|
|
11
|
+
3. [Database Setup](#database-setup)
|
|
12
|
+
4. [Running the Tests](#running-the-tests)
|
|
13
|
+
5. [Code Style](#code-style)
|
|
14
|
+
6. [Opening a Pull Request](#opening-a-pull-request)
|
|
15
|
+
7. [Contributing Custom Processors](#contributing-custom-processors)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
| Tool | Minimum Version | Notes |
|
|
22
|
+
|------|----------------|-------|
|
|
23
|
+
| Ruby | 3.1.0 | `rbenv` or `asdf` recommended |
|
|
24
|
+
| Docker | 24+ | For running test databases |
|
|
25
|
+
| Docker Compose | v2 plugin | Bundled with Docker Desktop |
|
|
26
|
+
| Git | Any recent | |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Setting Up Your Environment
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/kanutocd/whodunit-chronicles.git
|
|
34
|
+
cd whodunit-chronicles
|
|
35
|
+
|
|
36
|
+
# Install dependencies
|
|
37
|
+
bundle install
|
|
38
|
+
|
|
39
|
+
# Verify the console works
|
|
40
|
+
bundle exec bin/console
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Database Setup
|
|
46
|
+
|
|
47
|
+
Chronicles streams directly from PostgreSQL logical replication and MySQL/MariaDB binary logs. The test suite needs real database processes — no SQLite or in-memory substitute.
|
|
48
|
+
|
|
49
|
+
**Start all test databases with Docker Compose:**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
docker compose up -d
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This starts:
|
|
56
|
+
|
|
57
|
+
| Service | Port | Purpose |
|
|
58
|
+
|---------|------|---------|
|
|
59
|
+
| PostgreSQL 16 (wal_level=logical) | 5432 | Source DB for pg tests |
|
|
60
|
+
| PostgreSQL 16 (audit store) | 5433 | Audit DB for pg tests |
|
|
61
|
+
| MySQL 8.0 (binlog ROW format) | 3306 | Source DB for MySQL tests |
|
|
62
|
+
| MySQL 8.0 (audit store) | 3307 | Audit DB for MySQL tests |
|
|
63
|
+
| MariaDB 11 (binlog ROW format) | 3308 | Source DB for MariaDB tests |
|
|
64
|
+
|
|
65
|
+
Wait for all services to be healthy before running tests:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
docker compose ps # all should show "(healthy)"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### PostgreSQL — enabling logical replication
|
|
72
|
+
|
|
73
|
+
The Docker image is pre-configured (`wal_level=logical`). If you're using a system Postgres instead, add to `postgresql.conf`:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
wal_level = logical
|
|
77
|
+
max_replication_slots = 10
|
|
78
|
+
max_wal_senders = 10
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then restart Postgres and grant the replication role to your user:
|
|
82
|
+
|
|
83
|
+
```sql
|
|
84
|
+
ALTER ROLE your_user WITH REPLICATION;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### MySQL — enabling binary logging
|
|
88
|
+
|
|
89
|
+
The Docker image launches with `--binlog-format=ROW --binlog-row-image=FULL`. If using a system MySQL, add to `my.cnf`:
|
|
90
|
+
|
|
91
|
+
```ini
|
|
92
|
+
[mysqld]
|
|
93
|
+
server-id = 1
|
|
94
|
+
log-bin = mysql-bin
|
|
95
|
+
binlog-format = ROW
|
|
96
|
+
binlog-row-image = FULL
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Running the Tests
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Full suite
|
|
105
|
+
bundle exec rake test
|
|
106
|
+
|
|
107
|
+
# Single file
|
|
108
|
+
bundle exec ruby test/whodunit/chronicles/composite_processor_test.rb
|
|
109
|
+
|
|
110
|
+
# With coverage report
|
|
111
|
+
bundle exec rake test
|
|
112
|
+
open coverage/index.html
|
|
113
|
+
|
|
114
|
+
# Code style
|
|
115
|
+
bundle exec rubocop
|
|
116
|
+
|
|
117
|
+
# Security scan
|
|
118
|
+
bundle exec bundler-audit check --update
|
|
119
|
+
bundle exec brakeman
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Environment variables for test databases
|
|
123
|
+
|
|
124
|
+
The test suite reads these environment variables (defaults match the Docker Compose setup):
|
|
125
|
+
|
|
126
|
+
| Variable | Default |
|
|
127
|
+
|----------|---------|
|
|
128
|
+
| `CHRONICLES_PG_URL` | `postgresql://chronicles:chronicles@localhost/chronicles_test` |
|
|
129
|
+
| `CHRONICLES_PG_AUDIT_URL` | `postgresql://chronicles:chronicles@localhost:5433/chronicles_audit_test` |
|
|
130
|
+
| `CHRONICLES_MYSQL_URL` | `mysql://chronicles:chronicles@localhost:3306/chronicles_test` |
|
|
131
|
+
| `CHRONICLES_MYSQL_AUDIT_URL` | `mysql://chronicles:chronicles@localhost:3307/chronicles_audit_test` |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Code Style
|
|
136
|
+
|
|
137
|
+
- All files must have `# frozen_string_literal: true`
|
|
138
|
+
- Public methods require YARD documentation
|
|
139
|
+
- Follow the existing RuboCop configuration (`.rubocop.yml`)
|
|
140
|
+
- Tests live in `test/` and use Minitest
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Opening a Pull Request
|
|
145
|
+
|
|
146
|
+
1. Fork the repository and create a feature branch:
|
|
147
|
+
```bash
|
|
148
|
+
git checkout -b feature/my-improvement
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
2. Make your changes with tests. Coverage should not drop below 90%.
|
|
152
|
+
|
|
153
|
+
3. Run the full quality check before pushing:
|
|
154
|
+
```bash
|
|
155
|
+
bundle exec rake test
|
|
156
|
+
bundle exec rubocop
|
|
157
|
+
bundle exec bundler-audit check --update
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
4. Push and open a PR with:
|
|
161
|
+
- A clear description of what changed and why
|
|
162
|
+
- Any migration steps if you changed configuration
|
|
163
|
+
- A note on which databases you tested against
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Contributing Custom Processors
|
|
168
|
+
|
|
169
|
+
The best contributions are custom processors for real-world domains. Here are domains we'd love to see:
|
|
170
|
+
|
|
171
|
+
- **E-commerce** — order tracking, inventory changes, cart events
|
|
172
|
+
- **Financial services** — transaction monitoring, compliance audit trails
|
|
173
|
+
- **Healthcare** — patient record changes, HIPAA-relevant audit events
|
|
174
|
+
- **SaaS** — feature flag changes, subscription events, seat management
|
|
175
|
+
- **Education** — student progress, grade changes, enrollment events
|
|
176
|
+
|
|
177
|
+
A good processor contribution includes:
|
|
178
|
+
|
|
179
|
+
1. The processor class in `lib/whodunit/chronicles/processors/`
|
|
180
|
+
2. Tests in `test/whodunit/chronicles/processors/`
|
|
181
|
+
3. A usage example in `examples/`
|
|
182
|
+
4. An entry in `CHANGELOG.md`
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
Questions? Open an issue or start a discussion on GitHub.
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 📜 Whodunit Chronicles
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/whodunit-chronicles)
|
|
4
4
|
[](https://github.com/kanutocd/whodunit-chronicles/actions)
|
|
@@ -446,7 +446,7 @@ end
|
|
|
446
446
|
# Chain multiple processors for different purposes
|
|
447
447
|
service = Whodunit::Chronicles::Service.new(
|
|
448
448
|
adapter: Adapters::PostgreSQL.new,
|
|
449
|
-
processor: CompositeProcessor.new([
|
|
449
|
+
processor: Whodunit::Chronicles::CompositeProcessor.new([
|
|
450
450
|
AnalyticsProcessor.new, # For business intelligence
|
|
451
451
|
AlertingProcessor.new, # For real-time monitoring
|
|
452
452
|
ComplianceProcessor.new, # For regulatory requirements
|
|
@@ -641,11 +641,11 @@ We especially welcome custom processors for different business domains. Consider
|
|
|
641
641
|
## 📚 Documentation
|
|
642
642
|
|
|
643
643
|
- **[API Documentation](https://kanutocd.github.io/whodunit-chronicles/)**
|
|
644
|
-
- **
|
|
645
|
-
- **
|
|
646
|
-
- **
|
|
647
|
-
- **
|
|
648
|
-
- **
|
|
644
|
+
- [ ] TODO: **Configuration Guide** _(docs/configuration-todo.md)_
|
|
645
|
+
- [ ] TODO: **Architecture Deep Dive** _(docs/architecture-todo.md)_
|
|
646
|
+
- [ ] TODO: **PostgreSQL Setup** _(docs/postgresql-setup-todo.md)_
|
|
647
|
+
- [ ] TODO: **MySQL/MariaDB Setup** _(docs/mysql-setup.md)_
|
|
648
|
+
- [ ] TODO: **Production Deployment** _(docs/production-todo.md)_
|
|
649
649
|
|
|
650
650
|
## 📄 License
|
|
651
651
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- docker/mysql/init.sql
|
|
2
|
+
-- Run once when the MySQL/MariaDB container is first created.
|
|
3
|
+
|
|
4
|
+
USE chronicles_test;
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
7
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
8
|
+
name VARCHAR(255) NOT NULL,
|
|
9
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
10
|
+
creator_id INT DEFAULT NULL,
|
|
11
|
+
updater_id INT DEFAULT NULL,
|
|
12
|
+
deleter_id INT DEFAULT NULL,
|
|
13
|
+
created_at DATETIME(6) NOT NULL DEFAULT NOW(6),
|
|
14
|
+
updated_at DATETIME(6) NOT NULL DEFAULT NOW(6) ON UPDATE NOW(6)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
18
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
19
|
+
user_id INT NOT NULL,
|
|
20
|
+
title VARCHAR(500) NOT NULL,
|
|
21
|
+
body TEXT,
|
|
22
|
+
status VARCHAR(50) DEFAULT 'draft',
|
|
23
|
+
creator_id INT DEFAULT NULL,
|
|
24
|
+
updater_id INT DEFAULT NULL,
|
|
25
|
+
deleter_id INT DEFAULT NULL,
|
|
26
|
+
created_at DATETIME(6) NOT NULL DEFAULT NOW(6),
|
|
27
|
+
updated_at DATETIME(6) NOT NULL DEFAULT NOW(6) ON UPDATE NOW(6),
|
|
28
|
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- Grant replication privileges to the chronicles user
|
|
32
|
+
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'chronicles'@'%';
|
|
33
|
+
FLUSH PRIVILEGES;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- docker/postgres/init.sql
|
|
2
|
+
-- Run once when the container is first created.
|
|
3
|
+
-- Sets up the replication role and test tables used by the test suite.
|
|
4
|
+
|
|
5
|
+
-- Allow the chronicles user to create replication slots and use logical replication
|
|
6
|
+
ALTER ROLE chronicles WITH REPLICATION;
|
|
7
|
+
|
|
8
|
+
-- Grant superuser for test convenience (never do this in production)
|
|
9
|
+
-- Remove this line and grant only necessary permissions for a tighter setup.
|
|
10
|
+
ALTER ROLE chronicles WITH SUPERUSER;
|
|
11
|
+
|
|
12
|
+
-- Create the test tables Chronicles will stream from
|
|
13
|
+
\c chronicles_test
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
16
|
+
id SERIAL PRIMARY KEY,
|
|
17
|
+
name VARCHAR(255) NOT NULL,
|
|
18
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
19
|
+
creator_id INTEGER,
|
|
20
|
+
updater_id INTEGER,
|
|
21
|
+
deleter_id INTEGER,
|
|
22
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
23
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
27
|
+
id SERIAL PRIMARY KEY,
|
|
28
|
+
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
29
|
+
title VARCHAR(500) NOT NULL,
|
|
30
|
+
body TEXT,
|
|
31
|
+
status VARCHAR(50) DEFAULT 'draft',
|
|
32
|
+
creator_id INTEGER,
|
|
33
|
+
updater_id INTEGER,
|
|
34
|
+
deleter_id INTEGER,
|
|
35
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
36
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- Publication used by the Chronicles PostgreSQL adapter
|
|
40
|
+
CREATE PUBLICATION chronicles_test_pub FOR TABLE users, posts;
|
data/docker-compose.yml
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
name: whodunit-chronicles
|
|
2
|
+
|
|
3
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
# Local development + CI test databases
|
|
5
|
+
#
|
|
6
|
+
# Quickstart:
|
|
7
|
+
# docker compose up -d
|
|
8
|
+
# bundle exec rake test
|
|
9
|
+
#
|
|
10
|
+
# Stop + wipe volumes:
|
|
11
|
+
# docker compose down -v
|
|
12
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
services:
|
|
15
|
+
# ── PostgreSQL (logical replication enabled) ─────────────────────────────
|
|
16
|
+
postgres:
|
|
17
|
+
image: postgres:16-alpine
|
|
18
|
+
environment:
|
|
19
|
+
POSTGRES_USER: chronicles
|
|
20
|
+
POSTGRES_PASSWORD: chronicles
|
|
21
|
+
POSTGRES_DB: chronicles_test
|
|
22
|
+
ports:
|
|
23
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
24
|
+
command: >
|
|
25
|
+
postgres
|
|
26
|
+
-c wal_level=logical
|
|
27
|
+
-c max_replication_slots=10
|
|
28
|
+
-c max_wal_senders=10
|
|
29
|
+
-c log_replication_commands=on
|
|
30
|
+
volumes:
|
|
31
|
+
- pg_data:/var/lib/postgresql/data
|
|
32
|
+
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
33
|
+
healthcheck:
|
|
34
|
+
test: ["CMD-SHELL", "pg_isready -U chronicles -d chronicles_test"]
|
|
35
|
+
interval: 5s
|
|
36
|
+
timeout: 5s
|
|
37
|
+
retries: 10
|
|
38
|
+
start_period: 10s
|
|
39
|
+
|
|
40
|
+
# Separate audit database (mirrors production split)
|
|
41
|
+
postgres_audit:
|
|
42
|
+
image: postgres:16-alpine
|
|
43
|
+
environment:
|
|
44
|
+
POSTGRES_USER: chronicles
|
|
45
|
+
POSTGRES_PASSWORD: chronicles
|
|
46
|
+
POSTGRES_DB: chronicles_audit_test
|
|
47
|
+
ports:
|
|
48
|
+
- "${POSTGRES_AUDIT_PORT:-5433}:5432"
|
|
49
|
+
volumes:
|
|
50
|
+
- pg_audit_data:/var/lib/postgresql/data
|
|
51
|
+
healthcheck:
|
|
52
|
+
test: ["CMD-SHELL", "pg_isready -U chronicles -d chronicles_audit_test"]
|
|
53
|
+
interval: 5s
|
|
54
|
+
timeout: 5s
|
|
55
|
+
retries: 10
|
|
56
|
+
start_period: 10s
|
|
57
|
+
|
|
58
|
+
# ── MySQL (binary logging enabled) ───────────────────────────────────────
|
|
59
|
+
mysql:
|
|
60
|
+
image: mysql:8.0
|
|
61
|
+
environment:
|
|
62
|
+
MYSQL_ROOT_PASSWORD: chronicles_root
|
|
63
|
+
MYSQL_USER: chronicles
|
|
64
|
+
MYSQL_PASSWORD: chronicles
|
|
65
|
+
MYSQL_DATABASE: chronicles_test
|
|
66
|
+
ports:
|
|
67
|
+
- "3306:3306"
|
|
68
|
+
command: >
|
|
69
|
+
mysqld
|
|
70
|
+
--server-id=1
|
|
71
|
+
--log-bin=mysql-bin
|
|
72
|
+
--binlog-format=ROW
|
|
73
|
+
--binlog-row-image=FULL
|
|
74
|
+
--expire-logs-days=1
|
|
75
|
+
--gtid-mode=ON
|
|
76
|
+
--enforce-gtid-consistency=ON
|
|
77
|
+
volumes:
|
|
78
|
+
- mysql_data:/var/lib/mysql
|
|
79
|
+
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
80
|
+
healthcheck:
|
|
81
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "chronicles", "-pchronicles"]
|
|
82
|
+
interval: 5s
|
|
83
|
+
timeout: 5s
|
|
84
|
+
retries: 15
|
|
85
|
+
start_period: 30s
|
|
86
|
+
|
|
87
|
+
# Separate audit database for MySQL
|
|
88
|
+
mysql_audit:
|
|
89
|
+
image: mysql:8.0
|
|
90
|
+
environment:
|
|
91
|
+
MYSQL_ROOT_PASSWORD: chronicles_root
|
|
92
|
+
MYSQL_USER: chronicles
|
|
93
|
+
MYSQL_PASSWORD: chronicles
|
|
94
|
+
MYSQL_DATABASE: chronicles_audit_test
|
|
95
|
+
ports:
|
|
96
|
+
- "3307:3306"
|
|
97
|
+
volumes:
|
|
98
|
+
- mysql_audit_data:/var/lib/mysql
|
|
99
|
+
healthcheck:
|
|
100
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "chronicles", "-pchronicles"]
|
|
101
|
+
interval: 5s
|
|
102
|
+
timeout: 5s
|
|
103
|
+
retries: 15
|
|
104
|
+
start_period: 30s
|
|
105
|
+
|
|
106
|
+
# ── MariaDB (binary logging enabled) ─────────────────────────────────────
|
|
107
|
+
mariadb:
|
|
108
|
+
image: mariadb:11
|
|
109
|
+
environment:
|
|
110
|
+
MARIADB_ROOT_PASSWORD: chronicles_root
|
|
111
|
+
MARIADB_USER: chronicles
|
|
112
|
+
MARIADB_PASSWORD: chronicles
|
|
113
|
+
MARIADB_DATABASE: chronicles_test
|
|
114
|
+
ports:
|
|
115
|
+
- "3308:3306"
|
|
116
|
+
command: >
|
|
117
|
+
mariadbd
|
|
118
|
+
--server-id=2
|
|
119
|
+
--log-bin=mariadb-bin
|
|
120
|
+
--binlog-format=ROW
|
|
121
|
+
--binlog-row-image=FULL
|
|
122
|
+
--expire-logs-days=1
|
|
123
|
+
volumes:
|
|
124
|
+
- mariadb_data:/var/lib/mysql
|
|
125
|
+
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
126
|
+
healthcheck:
|
|
127
|
+
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
|
128
|
+
interval: 5s
|
|
129
|
+
timeout: 5s
|
|
130
|
+
retries: 15
|
|
131
|
+
start_period: 30s
|
|
132
|
+
|
|
133
|
+
volumes:
|
|
134
|
+
pg_data:
|
|
135
|
+
pg_audit_data:
|
|
136
|
+
mysql_data:
|
|
137
|
+
mysql_audit_data:
|
|
138
|
+
mariadb_data:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whodunit
|
|
4
|
+
module Chronicles
|
|
5
|
+
# Lazily loads the correct database adapter gem at runtime.
|
|
6
|
+
#
|
|
7
|
+
# This avoids forcing all users to install both `pg` and `trilogy`
|
|
8
|
+
# regardless of which database they actually use.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# adapter = AdapterLoader.load(:postgresql)
|
|
12
|
+
# adapter = AdapterLoader.load(:mysql)
|
|
13
|
+
# adapter = AdapterLoader.load(:mariadb)
|
|
14
|
+
module AdapterLoader
|
|
15
|
+
# Map of adapter type symbols to their required gem and class path.
|
|
16
|
+
ADAPTER_REGISTRY = {
|
|
17
|
+
postgresql: {
|
|
18
|
+
gem: 'pg',
|
|
19
|
+
require: 'whodunit/chronicles/adapters/postgresql',
|
|
20
|
+
class: 'Whodunit::Chronicles::Adapters::PostgreSQL',
|
|
21
|
+
hint: "Add `gem 'pg', '~> 1.5'` to your Gemfile.",
|
|
22
|
+
},
|
|
23
|
+
mysql: {
|
|
24
|
+
gem: 'trilogy',
|
|
25
|
+
require: 'whodunit/chronicles/adapters/mysql',
|
|
26
|
+
class: 'Whodunit::Chronicles::Adapters::MySQL',
|
|
27
|
+
hint: "Add `gem 'trilogy', '~> 2.9'` to your Gemfile.",
|
|
28
|
+
},
|
|
29
|
+
mariadb: {
|
|
30
|
+
gem: 'trilogy',
|
|
31
|
+
require: 'whodunit/chronicles/adapters/mysql',
|
|
32
|
+
class: 'Whodunit::Chronicles::Adapters::MySQL',
|
|
33
|
+
hint: "Add `gem 'trilogy', '~> 2.9'` to your Gemfile.",
|
|
34
|
+
},
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
# Load and instantiate an adapter by type.
|
|
38
|
+
#
|
|
39
|
+
# @param type [Symbol] one of :postgresql, :mysql, :mariadb
|
|
40
|
+
# @param options [Hash] options forwarded to the adapter constructor
|
|
41
|
+
# @return [Adapters::Base] the instantiated adapter
|
|
42
|
+
# @raise [Whodunit::Chronicles::ConfigurationError] for unknown adapter types
|
|
43
|
+
# @raise [Whodunit::Chronicles::AdapterLoadError] when the required gem is missing
|
|
44
|
+
def self.load(type, **)
|
|
45
|
+
config = ADAPTER_REGISTRY[type.to_sym]
|
|
46
|
+
|
|
47
|
+
unless config
|
|
48
|
+
known = ADAPTER_REGISTRY.keys.map(&:inspect).join(', ')
|
|
49
|
+
raise ConfigurationError,
|
|
50
|
+
"Unknown adapter type #{type.inspect}. Known adapters: #{known}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
load_gem!(config)
|
|
54
|
+
require config[:require]
|
|
55
|
+
Object.const_get(config[:class]).new(**)
|
|
56
|
+
rescue LoadError => e
|
|
57
|
+
raise AdapterLoadError,
|
|
58
|
+
"Could not load the '#{config[:gem]}' gem required for the " \
|
|
59
|
+
"#{type} adapter.\n#{config[:hint]}\nOriginal error: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @api private
|
|
63
|
+
def self.load_gem!(config)
|
|
64
|
+
require config[:gem]
|
|
65
|
+
end
|
|
66
|
+
private_class_method :load_gem!
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -10,7 +10,7 @@ module Whodunit
|
|
|
10
10
|
#
|
|
11
11
|
# Uses MySQL's binary log replication to stream database changes
|
|
12
12
|
# without impacting application performance.
|
|
13
|
-
class MySQL < StreamAdapter
|
|
13
|
+
class MySQL < Chronicles::StreamAdapter
|
|
14
14
|
DEFAULT_SERVER_ID = 1001
|
|
15
15
|
|
|
16
16
|
attr_reader :connection, :database_url, :server_id, :binlog_file, :binlog_position
|
|
@@ -126,7 +126,7 @@ module Whodunit
|
|
|
126
126
|
database: parsed_url[:database])
|
|
127
127
|
rescue StandardError => e
|
|
128
128
|
log(:error, 'Failed to establish connection', error: e.message)
|
|
129
|
-
raise
|
|
129
|
+
raise AdapterLoadError, "Connection failed: #{e.message}"
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def close_connection
|
|
@@ -9,7 +9,7 @@ module Whodunit
|
|
|
9
9
|
#
|
|
10
10
|
# Uses PostgreSQL's logical replication functionality to stream
|
|
11
11
|
# database changes via WAL decoding without impacting application performance.
|
|
12
|
-
class PostgreSQL < StreamAdapter
|
|
12
|
+
class PostgreSQL < Chronicles::StreamAdapter
|
|
13
13
|
DEFAULT_PLUGIN = 'pgoutput'
|
|
14
14
|
|
|
15
15
|
attr_reader :connection, :replication_connection, :publication_name, :slot_name
|
|
@@ -95,8 +95,8 @@ module Whodunit
|
|
|
95
95
|
def changes
|
|
96
96
|
return {} unless update? && old_data && new_data
|
|
97
97
|
|
|
98
|
-
changed_columns.
|
|
99
|
-
|
|
98
|
+
changed_columns.to_h do |column|
|
|
99
|
+
[column, [old_data[column], new_data[column]]]
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whodunit
|
|
4
|
+
module Chronicles
|
|
5
|
+
# Fans out a single change event to multiple processors in sequence.
|
|
6
|
+
#
|
|
7
|
+
# Use this to build pipelines — e.g. simultaneously storing audit records,
|
|
8
|
+
# streaming to Grafana, and triggering alerts — without coupling any single
|
|
9
|
+
# processor to the others.
|
|
10
|
+
#
|
|
11
|
+
# Each processor runs independently. If one raises, the error is logged and
|
|
12
|
+
# the remaining processors still execute (fail-open by default). Set
|
|
13
|
+
# `fail_fast: true` to instead halt the chain on the first error.
|
|
14
|
+
#
|
|
15
|
+
# @example Basic pipeline
|
|
16
|
+
# service = Whodunit::Chronicles::Service.new(
|
|
17
|
+
# adapter: adapter,
|
|
18
|
+
# processor: Whodunit::Chronicles::CompositeProcessor.new([
|
|
19
|
+
# AuditStoreProcessor.new,
|
|
20
|
+
# AlertingProcessor.new,
|
|
21
|
+
# GrafanaProcessor.new
|
|
22
|
+
# ])
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @example Halt on first error
|
|
26
|
+
# CompositeProcessor.new(processors, fail_fast: true)
|
|
27
|
+
#
|
|
28
|
+
class CompositeProcessor
|
|
29
|
+
# @param processors [Array<#process>] ordered list of processors to invoke
|
|
30
|
+
# @param fail_fast [Boolean] when true, halt the chain on the first error
|
|
31
|
+
# @param logger [Logger, nil] optional logger; defaults to Chronicles logger
|
|
32
|
+
def initialize(processors, fail_fast: false, logger: nil)
|
|
33
|
+
raise ArgumentError, 'processors must be an Array' unless processors.is_a?(Array)
|
|
34
|
+
raise ArgumentError, 'processors cannot be empty' if processors.empty?
|
|
35
|
+
|
|
36
|
+
@processors = processors
|
|
37
|
+
@fail_fast = fail_fast
|
|
38
|
+
@logger = logger || Whodunit::Chronicles.logger
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Process a change event through every processor in the chain.
|
|
42
|
+
#
|
|
43
|
+
# @param change_event [ChangeEvent] the event to process
|
|
44
|
+
# @return [void]
|
|
45
|
+
# @raise [ProcessingError] only when fail_fast is true and a child raises
|
|
46
|
+
def process(change_event)
|
|
47
|
+
errors = []
|
|
48
|
+
|
|
49
|
+
@processors.each do |processor|
|
|
50
|
+
processor.process(change_event)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise ProcessingError, "#{processor.class} failed: #{e.message}" if @fail_fast
|
|
53
|
+
|
|
54
|
+
@logger.error { "CompositeProcessor: #{processor.class} raised #{e.class}: #{e.message}" }
|
|
55
|
+
errors << e
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return if errors.empty?
|
|
59
|
+
|
|
60
|
+
@logger.warn do
|
|
61
|
+
"CompositeProcessor: #{errors.size} processor(s) failed for #{change_event.table_name}##{change_event.action}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Integer] the number of processors in the chain
|
|
66
|
+
def size
|
|
67
|
+
@processors.size
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<Class>] the processor classes in chain order
|
|
71
|
+
def processor_classes
|
|
72
|
+
@processors.map(&:class)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Append a processor to the end of the chain.
|
|
76
|
+
#
|
|
77
|
+
# @param processor [#process] the processor to add
|
|
78
|
+
# @return [self]
|
|
79
|
+
def add(processor)
|
|
80
|
+
@processors << processor
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
alias << add
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Whodunit
|
|
4
|
+
module Chronicles
|
|
5
|
+
# Base error class for all whodunit-chronicles errors.
|
|
6
|
+
#
|
|
7
|
+
# Rescuing this class catches every error raised by the gem:
|
|
8
|
+
#
|
|
9
|
+
# begin
|
|
10
|
+
# Whodunit::Chronicles.service.start
|
|
11
|
+
# rescue Whodunit::Chronicles::Error => e
|
|
12
|
+
# logger.error("Chronicles failure: #{e.message}")
|
|
13
|
+
# end
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Raised when the gem is configured with invalid or missing settings.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Whodunit::Chronicles.configure do |c|
|
|
20
|
+
# c.adapter = :unknown_db # => raises ConfigurationError
|
|
21
|
+
# end
|
|
22
|
+
class ConfigurationError < Error; end
|
|
23
|
+
|
|
24
|
+
# Raised when a required database driver gem is not installed.
|
|
25
|
+
#
|
|
26
|
+
# @example message
|
|
27
|
+
# "Could not load the 'pg' gem required for the postgresql adapter.
|
|
28
|
+
# Add `gem 'pg', '~> 1.5'` to your Gemfile."
|
|
29
|
+
class AdapterLoadError < Error; end
|
|
30
|
+
|
|
31
|
+
# Raised when a streaming or replication connection fails.
|
|
32
|
+
class ConnectionError < Error; end
|
|
33
|
+
|
|
34
|
+
# Raised when processing a change event fails.
|
|
35
|
+
class ProcessingError < Error; end
|
|
36
|
+
|
|
37
|
+
# Raised when writing an audit record to the audit database fails.
|
|
38
|
+
class PersistenceError < Error; end
|
|
39
|
+
|
|
40
|
+
# Raised when replication related error occurs
|
|
41
|
+
class ReplicationError < Error; end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -118,9 +118,9 @@ module Whodunit
|
|
|
118
118
|
def build_adapter
|
|
119
119
|
case Chronicles.config.adapter
|
|
120
120
|
when :postgresql
|
|
121
|
-
|
|
121
|
+
Whodunit::Chronicles::AdapterLoader.load(:postgresql, logger:)
|
|
122
122
|
when :mysql
|
|
123
|
-
|
|
123
|
+
Whodunit::Chronicles::AdapterLoader.load(:mysql, logger:)
|
|
124
124
|
else
|
|
125
125
|
raise ConfigurationError, "Unsupported adapter: #{Chronicles.config.adapter}"
|
|
126
126
|
end
|
|
@@ -131,7 +131,7 @@ module Whodunit
|
|
|
131
131
|
|
|
132
132
|
return if adapter.test_connection
|
|
133
133
|
|
|
134
|
-
raise
|
|
134
|
+
raise AdapterLoadError, 'Failed to connect to source database'
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def test_connections!
|
|
@@ -139,7 +139,7 @@ module Whodunit
|
|
|
139
139
|
# Test processor connection by creating a dummy connection
|
|
140
140
|
processor.send(:ensure_connection)
|
|
141
141
|
rescue StandardError => e
|
|
142
|
-
raise
|
|
142
|
+
raise AdapterLoadError, "Connection test failed: #{e.message}"
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
def start_streaming_with_retry
|
data/lib/whodunit/chronicles.rb
CHANGED
|
@@ -14,9 +14,9 @@ require_relative 'chronicles/persistence'
|
|
|
14
14
|
require_relative 'chronicles/processor'
|
|
15
15
|
require_relative 'chronicles/service'
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
require_relative 'chronicles/
|
|
19
|
-
require_relative 'chronicles/
|
|
17
|
+
require_relative 'chronicles/errors'
|
|
18
|
+
require_relative 'chronicles/adapter_loader'
|
|
19
|
+
require_relative 'chronicles/composite_processor'
|
|
20
20
|
|
|
21
21
|
module Whodunit
|
|
22
22
|
# Chronicles - The complete historical record of `whodunit did what?` data
|
|
@@ -41,11 +41,6 @@ module Whodunit
|
|
|
41
41
|
setting :max_retry_attempts, default: 3
|
|
42
42
|
setting :retry_delay, default: 5
|
|
43
43
|
|
|
44
|
-
class Error < StandardError; end
|
|
45
|
-
class ConfigurationError < Error; end
|
|
46
|
-
class AdapterError < Error; end
|
|
47
|
-
class ReplicationError < Error; end
|
|
48
|
-
|
|
49
44
|
# Configure Chronicles
|
|
50
45
|
#
|
|
51
46
|
# @example
|
data/whodunit-chronicles.gemspec
CHANGED
|
@@ -12,9 +12,9 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = 'While Whodunit tracks who made changes, Chronicles captures ' \
|
|
13
13
|
'what changed by streaming database events into comprehensive ' \
|
|
14
14
|
'audit trails with zero Rails application overhead.'
|
|
15
|
-
spec.homepage = 'https://github.com/
|
|
15
|
+
spec.homepage = 'https://github.com/kanutocd/whodunit-chronicles'
|
|
16
16
|
spec.license = 'MIT'
|
|
17
|
-
spec.required_ruby_version = '>= 3.
|
|
17
|
+
spec.required_ruby_version = '>= 3.2.0'
|
|
18
18
|
|
|
19
19
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
20
20
|
spec.metadata['homepage_uri'] = spec.homepage
|
|
@@ -33,19 +33,31 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
34
34
|
spec.require_paths = ['lib']
|
|
35
35
|
|
|
36
|
-
# Core dependencies
|
|
36
|
+
# ── Core runtime dependencies ───────────────────────────────
|
|
37
37
|
spec.add_dependency 'concurrent-ruby', '~> 1.2'
|
|
38
38
|
spec.add_dependency 'dry-configurable', '~> 1.0'
|
|
39
39
|
spec.add_dependency 'dry-logger', '~> 1.0'
|
|
40
40
|
|
|
41
|
-
# Database
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
|
|
41
|
+
# ── Database adapters — OPTIONAL at runtime ───────────────────────────────
|
|
42
|
+
#
|
|
43
|
+
# Chronicles lazy-loads the driver that matches your configured adapter.
|
|
44
|
+
# You only need to install the gem for the database(s) you actually use:
|
|
45
|
+
#
|
|
46
|
+
# PostgreSQL → gem 'pg', '~> 1.5'
|
|
47
|
+
# MySQL/MariaDB → gem 'trilogy', '~> 2.9'
|
|
48
|
+
#
|
|
49
|
+
# Both are listed here so `bundle install` in development installs them,
|
|
50
|
+
# but they are NOT required at gem load time — only when the adapter loads.
|
|
51
|
+
spec.add_development_dependency 'pg', '~> 1.5'
|
|
52
|
+
spec.add_development_dependency 'trilogy', '~> 2.9'
|
|
53
|
+
|
|
54
|
+
# bigdecimal: required by trilogy on Ruby 3.4+ (removed from stdlib).
|
|
55
|
+
# Declared here so CI on Ruby 3.4+ doesn't break. Trilogy should own this
|
|
56
|
+
# dependency — track https://github.com/trilogy-libraries/trilogy/issues
|
|
45
57
|
# Required for Ruby 3.4+ compatibility (trilogy dependency)
|
|
46
|
-
spec.
|
|
58
|
+
spec.add_development_dependency 'bigdecimal', '~> 3.1'
|
|
47
59
|
|
|
48
|
-
# Development
|
|
60
|
+
# ── Development tooling ───────────────────────────────
|
|
49
61
|
spec.add_development_dependency 'kramdown', '~> 2.5'
|
|
50
62
|
spec.add_development_dependency 'minitest', '~> 5.20'
|
|
51
63
|
spec.add_development_dependency 'mocha', '~> 2.1'
|
|
@@ -55,11 +67,13 @@ Gem::Specification.new do |spec|
|
|
|
55
67
|
spec.add_development_dependency 'rubocop', '~> 1.60'
|
|
56
68
|
spec.add_development_dependency 'rubocop-minitest', '~> 0.34'
|
|
57
69
|
spec.add_development_dependency 'rubocop-performance', '~> 1.19'
|
|
70
|
+
spec.add_development_dependency 'rubocop-rake', '~> 0.6'
|
|
71
|
+
spec.add_development_dependency 'rubocop-thread_safety', '~> 0.5'
|
|
58
72
|
spec.add_development_dependency 'simplecov', '~> 0.22'
|
|
59
73
|
spec.add_development_dependency 'simplecov-cobertura', '~> 3.0'
|
|
60
74
|
spec.add_development_dependency 'yard', '~> 0.9'
|
|
61
75
|
|
|
62
|
-
# Security scanning dependencies
|
|
76
|
+
# ── Security scanning dependencies ──────────────────────────────
|
|
63
77
|
spec.add_development_dependency 'brakeman', '~> 7.1'
|
|
64
78
|
spec.add_development_dependency 'bundler-audit', '~> 0.9'
|
|
65
79
|
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ken C. Demanawa
|
|
@@ -59,7 +59,7 @@ dependencies:
|
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '1.5'
|
|
62
|
-
type: :
|
|
62
|
+
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
@@ -73,7 +73,7 @@ dependencies:
|
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '2.9'
|
|
76
|
-
type: :
|
|
76
|
+
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
@@ -87,7 +87,7 @@ dependencies:
|
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
89
|
version: '3.1'
|
|
90
|
-
type: :
|
|
90
|
+
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
@@ -220,6 +220,34 @@ dependencies:
|
|
|
220
220
|
- - "~>"
|
|
221
221
|
- !ruby/object:Gem::Version
|
|
222
222
|
version: '1.19'
|
|
223
|
+
- !ruby/object:Gem::Dependency
|
|
224
|
+
name: rubocop-rake
|
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
|
226
|
+
requirements:
|
|
227
|
+
- - "~>"
|
|
228
|
+
- !ruby/object:Gem::Version
|
|
229
|
+
version: '0.6'
|
|
230
|
+
type: :development
|
|
231
|
+
prerelease: false
|
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
233
|
+
requirements:
|
|
234
|
+
- - "~>"
|
|
235
|
+
- !ruby/object:Gem::Version
|
|
236
|
+
version: '0.6'
|
|
237
|
+
- !ruby/object:Gem::Dependency
|
|
238
|
+
name: rubocop-thread_safety
|
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
|
240
|
+
requirements:
|
|
241
|
+
- - "~>"
|
|
242
|
+
- !ruby/object:Gem::Version
|
|
243
|
+
version: '0.5'
|
|
244
|
+
type: :development
|
|
245
|
+
prerelease: false
|
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
247
|
+
requirements:
|
|
248
|
+
- - "~>"
|
|
249
|
+
- !ruby/object:Gem::Version
|
|
250
|
+
version: '0.5'
|
|
223
251
|
- !ruby/object:Gem::Dependency
|
|
224
252
|
name: simplecov
|
|
225
253
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -304,20 +332,27 @@ files:
|
|
|
304
332
|
- ".yardopts"
|
|
305
333
|
- CHANGELOG.md
|
|
306
334
|
- CODE_OF_CONDUCT.md
|
|
335
|
+
- CONTRIBUTING.md
|
|
307
336
|
- LICENSE
|
|
308
337
|
- README.md
|
|
309
338
|
- Rakefile
|
|
339
|
+
- docker-compose.yml
|
|
340
|
+
- docker/mysql/init.sql
|
|
341
|
+
- docker/postgres/init.sql
|
|
310
342
|
- examples/images/campaign-performance-analytics.png
|
|
311
343
|
- examples/images/candidate-journey-analytics.png
|
|
312
344
|
- examples/images/recruitment-funnel-analytics.png
|
|
313
345
|
- lib/.gitkeep
|
|
314
346
|
- lib/whodunit-chronicles.rb
|
|
315
347
|
- lib/whodunit/chronicles.rb
|
|
348
|
+
- lib/whodunit/chronicles/adapter_loader.rb
|
|
316
349
|
- lib/whodunit/chronicles/adapters/mysql.rb
|
|
317
350
|
- lib/whodunit/chronicles/adapters/postgresql.rb
|
|
318
351
|
- lib/whodunit/chronicles/change_event.rb
|
|
352
|
+
- lib/whodunit/chronicles/composite_processor.rb
|
|
319
353
|
- lib/whodunit/chronicles/configuration.rb
|
|
320
354
|
- lib/whodunit/chronicles/connection.rb
|
|
355
|
+
- lib/whodunit/chronicles/errors.rb
|
|
321
356
|
- lib/whodunit/chronicles/persistence.rb
|
|
322
357
|
- lib/whodunit/chronicles/processor.rb
|
|
323
358
|
- lib/whodunit/chronicles/service.rb
|
|
@@ -325,14 +360,14 @@ files:
|
|
|
325
360
|
- lib/whodunit/chronicles/table.rb
|
|
326
361
|
- lib/whodunit/chronicles/version.rb
|
|
327
362
|
- whodunit-chronicles.gemspec
|
|
328
|
-
homepage: https://github.com/
|
|
363
|
+
homepage: https://github.com/kanutocd/whodunit-chronicles
|
|
329
364
|
licenses:
|
|
330
365
|
- MIT
|
|
331
366
|
metadata:
|
|
332
367
|
allowed_push_host: https://rubygems.org
|
|
333
|
-
homepage_uri: https://github.com/
|
|
334
|
-
source_code_uri: https://github.com/
|
|
335
|
-
changelog_uri: https://github.com/
|
|
368
|
+
homepage_uri: https://github.com/kanutocd/whodunit-chronicles
|
|
369
|
+
source_code_uri: https://github.com/kanutocd/whodunit-chronicles
|
|
370
|
+
changelog_uri: https://github.com/kanutocd/whodunit-chronicles/blob/main/CHANGELOG.md
|
|
336
371
|
rubygems_mfa_required: 'true'
|
|
337
372
|
rdoc_options: []
|
|
338
373
|
require_paths:
|
|
@@ -341,7 +376,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
341
376
|
requirements:
|
|
342
377
|
- - ">="
|
|
343
378
|
- !ruby/object:Gem::Version
|
|
344
|
-
version: 3.
|
|
379
|
+
version: 3.2.0
|
|
345
380
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
346
381
|
requirements:
|
|
347
382
|
- - ">="
|