pigeon-rb 0.1.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +343 -0
  3. data/lib/pigeon/active_job_integration.rb +32 -0
  4. data/lib/pigeon/api.rb +200 -0
  5. data/lib/pigeon/configuration.rb +161 -0
  6. data/lib/pigeon/core.rb +104 -0
  7. data/lib/pigeon/encryption.rb +213 -0
  8. data/lib/pigeon/generators/hanami/migration_generator.rb +89 -0
  9. data/lib/pigeon/generators/rails/install_generator.rb +32 -0
  10. data/lib/pigeon/generators/rails/migration_generator.rb +20 -0
  11. data/lib/pigeon/generators/rails/templates/create_outbox_messages.rb.erb +34 -0
  12. data/lib/pigeon/generators/rails/templates/initializer.rb.erb +88 -0
  13. data/lib/pigeon/hanami_integration.rb +78 -0
  14. data/lib/pigeon/health_check/kafka.rb +37 -0
  15. data/lib/pigeon/health_check/processor.rb +70 -0
  16. data/lib/pigeon/health_check/queue.rb +69 -0
  17. data/lib/pigeon/health_check.rb +63 -0
  18. data/lib/pigeon/logging/structured_logger.rb +181 -0
  19. data/lib/pigeon/metrics/collector.rb +200 -0
  20. data/lib/pigeon/mock_producer.rb +18 -0
  21. data/lib/pigeon/models/adapters/active_record_adapter.rb +133 -0
  22. data/lib/pigeon/models/adapters/rom_adapter.rb +150 -0
  23. data/lib/pigeon/models/outbox_message.rb +182 -0
  24. data/lib/pigeon/monitoring.rb +113 -0
  25. data/lib/pigeon/outbox.rb +61 -0
  26. data/lib/pigeon/processor/background_processor.rb +109 -0
  27. data/lib/pigeon/processor.rb +798 -0
  28. data/lib/pigeon/publisher.rb +524 -0
  29. data/lib/pigeon/railtie.rb +29 -0
  30. data/lib/pigeon/schema.rb +35 -0
  31. data/lib/pigeon/security.rb +30 -0
  32. data/lib/pigeon/serializer.rb +77 -0
  33. data/lib/pigeon/tasks/pigeon.rake +64 -0
  34. data/lib/pigeon/trace_api.rb +37 -0
  35. data/lib/pigeon/tracing/core.rb +119 -0
  36. data/lib/pigeon/tracing/messaging.rb +144 -0
  37. data/lib/pigeon/tracing.rb +107 -0
  38. data/lib/pigeon/version.rb +5 -0
  39. data/lib/pigeon.rb +52 -0
  40. metadata +127 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1232e3eec460a373578ba9c8add7e6ae10d6c9db29e6b6cf017cbe019933b06c
4
+ data.tar.gz: 5427465799048a3ef0e9b4e1f72afdbf1001960f217fbd919cb6111a524bcbb6
5
+ SHA512:
6
+ metadata.gz: 4c8ae8ca31274ac1327c940d2c6b9d17254572e6a2b4ff8ccdce9135663e2ec809b690d2190b6dd742ca2f488bd079d3b793365d0491af67354b4e96a4244ed1
7
+ data.tar.gz: 0e0c5c09c14610f688b62612d9dd6a1e920aed952594003e8cb998c8e416fefb57002e54d93dc08e2530eb26ffbb2ae2a0207e44270091d7bc6726240ac2e58d
data/README.md ADDED
@@ -0,0 +1,343 @@
1
+ # Pigeon
2
+
3
+ A robust Ruby gem that implements the outbox pattern for Kafka message publishing, ensuring reliable message delivery with enterprise features like encryption, tracing, monitoring, and framework integrations.
4
+
5
+ ## Features
6
+
7
+ - **Outbox Pattern**: Reliable message delivery with database-backed persistence
8
+ - **Encryption**: AES-256-GCM encryption for sensitive payloads
9
+ - **Tracing**: OpenTelemetry integration for distributed tracing
10
+ - **Monitoring**: Comprehensive metrics and health checks
11
+ - **Framework Integration**: Rails and Hanami support with generators
12
+ - **ActiveJob Integration**: Background job processing for message handling
13
+ - **Security**: Payload masking and sensitive data protection
14
+ - **Health Checks**: Processor, queue, and Kafka connectivity monitoring
15
+ - **Structured Logging**: Context-aware logging with correlation IDs
16
+ - **Schema Validation**: Message schema registration and validation
17
+ - **Retry Logic**: Configurable retry policies with exponential backoff
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'pigeon'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ ```bash
30
+ $ bundle install
31
+ ```
32
+
33
+ Or install it yourself as:
34
+
35
+ ```bash
36
+ $ gem install pigeon
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ### Rails Integration
42
+
43
+ Install Pigeon in your Rails application:
44
+
45
+ ```bash
46
+ $ rake pigeon:install[rails]
47
+ ```
48
+
49
+ This will create:
50
+ - Configuration file at `config/initializers/pigeon.rb`
51
+ - Database migration for the outbox messages table
52
+
53
+ Run the migration:
54
+
55
+ ```bash
56
+ $ rails db:migrate
57
+ ```
58
+
59
+ ### Hanami Integration
60
+
61
+ For Hanami applications, generate the migration:
62
+
63
+ ```bash
64
+ $ rake pigeon:install[hanami]
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ Configure the gem with your Kafka and application details:
70
+
71
+ ```ruby
72
+ Pigeon.configure do |config|
73
+ # Basic configuration
74
+ config.client_id = "my-application"
75
+ config.kafka_brokers = ["kafka1:9092", "kafka2:9092"]
76
+
77
+ # Retry configuration
78
+ config.max_retries = 5
79
+ config.retry_delay = 60 # seconds
80
+ config.max_retry_delay = 3600 # 1 hour
81
+
82
+ # Encryption (optional)
83
+ config.encrypt_payload = false
84
+ config.encryption_key = ENV["PIGEON_ENCRYPTION_KEY"]
85
+
86
+ # Retention and cleanup
87
+ config.retention_period = 7 # days
88
+
89
+ # Karafka-specific configuration
90
+ config.karafka_config = {
91
+ delivery: :async,
92
+ kafka: {
93
+ 'bootstrap.servers': 'kafka1:9092,kafka2:9092',
94
+ 'request.required.acks': 1
95
+ }
96
+ }
97
+ end
98
+ ```
99
+
100
+ ### Environment Variables
101
+
102
+ Pigeon supports configuration via environment variables:
103
+
104
+ ```bash
105
+ PIGEON_CLIENT_ID=my-app
106
+ PIGEON_KAFKA_BROKERS=kafka1:9092,kafka2:9092
107
+ PIGEON_ENCRYPTION_KEY=your-32-byte-encryption-key
108
+ PIGEON_MAX_RETRIES=5
109
+ PIGEON_RETRY_DELAY=60
110
+ ```
111
+
112
+ ## Usage
113
+
114
+ ### Publishing Messages
115
+
116
+ ```ruby
117
+ # Simple publishing
118
+ Pigeon.publisher.publish(
119
+ topic: "user-events",
120
+ payload: { user_id: 123, action: "signup" }
121
+ )
122
+
123
+ # With encryption and sensitive field masking
124
+ Pigeon.publisher.publish(
125
+ topic: "user-events",
126
+ payload: { user_id: 123, email: "user@example.com", password: "secret" },
127
+ key: "user-123",
128
+ headers: { "source" => "web-app" },
129
+ encrypt: true,
130
+ sensitive_fields: [:password, :credit_card]
131
+ )
132
+
133
+ # Synchronous publishing (immediate attempt)
134
+ Pigeon.publisher.publish(
135
+ topic: "critical-events",
136
+ payload: { event: "payment_processed" },
137
+ sync: true
138
+ )
139
+ ```
140
+
141
+ ### Processing Messages
142
+
143
+ ```ruby
144
+ # Process pending messages
145
+ stats = Pigeon.processor.process_pending(batch_size: 100)
146
+ puts "Processed: #{stats[:processed]}, Succeeded: #{stats[:succeeded]}, Failed: #{stats[:failed]}"
147
+
148
+ # Process a specific message
149
+ success = Pigeon.processor.process_message("message-id")
150
+
151
+ # Clean up old processed messages
152
+ cleaned = Pigeon.processor.cleanup_processed(older_than: 14) # days
153
+ puts "Cleaned up #{cleaned} messages"
154
+
155
+ # Start background processing
156
+ Pigeon.start_processing(batch_size: 100, interval: 5, thread_count: 2)
157
+
158
+ # Stop background processing
159
+ Pigeon.stop_processing
160
+ ```
161
+
162
+ ### Encryption
163
+
164
+ ```ruby
165
+ # Enable encryption in configuration
166
+ Pigeon.configure do |config|
167
+ config.encrypt_payload = true
168
+ config.encryption_key = "your-32-byte-encryption-key"
169
+ end
170
+
171
+ # Encrypt a payload
172
+ encrypted = Pigeon.encrypt(payload.to_json)
173
+ decrypted = Pigeon.decrypt(encrypted)
174
+
175
+ # Mask sensitive fields for logging
176
+ masked = Pigeon.mask_payload(payload, [:password, :credit_card])
177
+ ```
178
+
179
+ ### Tracing
180
+
181
+ ```ruby
182
+ # Initialize OpenTelemetry tracing
183
+ Pigeon.init_tracing(service_name: "my-app")
184
+
185
+ # Trace message publishing
186
+ Pigeon.with_span("publish_message", attributes: { topic: "user-events" }) do
187
+ Pigeon.publisher.publish(topic: "user-events", payload: data)
188
+ end
189
+
190
+ # Trace message processing
191
+ Pigeon.trace_process(message) do |span|
192
+ # Process the message
193
+ span.set_attribute("message.id", message.id)
194
+ end
195
+ ```
196
+
197
+ ### Health Checks
198
+
199
+ ```ruby
200
+ # Check overall health
201
+ health = Pigeon.health_status
202
+ puts "Status: #{health[:status]}"
203
+
204
+ # Check individual components
205
+ processor_health = Pigeon.processor_health
206
+ queue_health = Pigeon.queue_health
207
+ kafka_health = Pigeon.kafka_health
208
+ ```
209
+
210
+ ### Monitoring
211
+
212
+ ```ruby
213
+ # Get metrics
214
+ metrics = Pigeon.metrics
215
+ puts "Messages published: #{metrics[:messages_published]}"
216
+ puts "Messages processed: #{metrics[:messages_processed]}"
217
+
218
+ # Get monitoring-friendly metrics
219
+ monitoring_metrics = Pigeon.metrics_for_monitoring
220
+
221
+ # Reset metrics
222
+ Pigeon.reset_metrics
223
+ ```
224
+
225
+ ### Schema Validation
226
+
227
+ ```ruby
228
+ # Register a schema
229
+ Pigeon.register_schema("user_event", {
230
+ type: "object",
231
+ properties: {
232
+ user_id: { type: "integer" },
233
+ action: { type: "string" }
234
+ },
235
+ required: ["user_id", "action"]
236
+ })
237
+
238
+ # Register sensitive fields
239
+ Pigeon.register_sensitive_fields([:password, :credit_card, :ssn])
240
+ ```
241
+
242
+ ### ActiveJob Integration
243
+
244
+ ```ruby
245
+ # Process messages in background
246
+ Pigeon::ActiveJobIntegration::ProcessorJob.perform_later(100)
247
+
248
+ # Clean up old messages in background
249
+ Pigeon::ActiveJobIntegration::CleanupJob.perform_later(7)
250
+ ```
251
+
252
+ ## API Reference
253
+
254
+ ### Core API
255
+
256
+ ```ruby
257
+ # Configuration
258
+ Pigeon.configure(&block)
259
+ Pigeon.config
260
+
261
+ # Publishing
262
+ Pigeon.publisher.publish(topic:, payload:, **options)
263
+
264
+ # Processing
265
+ Pigeon.processor.process_pending(batch_size: 100)
266
+ Pigeon.processor.process_message(id)
267
+ Pigeon.processor.cleanup_processed(older_than: 7)
268
+
269
+ # Background processing
270
+ Pigeon.start_processing(batch_size: 100, interval: 5, thread_count: 2)
271
+ Pigeon.stop_processing
272
+ Pigeon.processing?
273
+ ```
274
+
275
+ ### Security API
276
+
277
+ ```ruby
278
+ # Encryption
279
+ Pigeon.encrypt(payload, encryption_key = nil)
280
+ Pigeon.decrypt(encrypted_payload, encryption_key = nil)
281
+ Pigeon.mask_payload(payload, sensitive_fields = nil)
282
+ ```
283
+
284
+ ### Monitoring API
285
+
286
+ ```ruby
287
+ # Metrics
288
+ Pigeon.metrics
289
+ Pigeon.metrics_for_monitoring
290
+ Pigeon.reset_metrics
291
+
292
+ # Health checks
293
+ Pigeon.health_status
294
+ Pigeon.processor_health
295
+ Pigeon.queue_health
296
+ Pigeon.kafka_health
297
+
298
+ # Logging
299
+ Pigeon.logger_with_context(context = {})
300
+ Pigeon.log_level = :info
301
+ ```
302
+
303
+ ### Tracing API
304
+
305
+ ```ruby
306
+ # Tracing setup
307
+ Pigeon.init_tracing(service_name: "my-app", exporter: nil)
308
+ Pigeon.tracing_available?
309
+
310
+ # Spans
311
+ Pigeon.with_span(name, attributes: {}, kind: :internal, &block)
312
+ Pigeon.tracer(name = "pigeon")
313
+ ```
314
+
315
+ ### Outbox API
316
+
317
+ ```ruby
318
+ # Message management
319
+ Pigeon.create_outbox_message(attributes = {})
320
+ Pigeon.find_outbox_message(id)
321
+ Pigeon.find_outbox_messages_by_status(status, limit = 100)
322
+ Pigeon.find_outbox_messages_ready_for_retry(limit = 100)
323
+ Pigeon.count_outbox_messages_by_status(status)
324
+ Pigeon.find_oldest_outbox_message_by_status(status)
325
+ ```
326
+
327
+ ## Examples
328
+
329
+ See the `examples/` directory for complete working examples:
330
+
331
+ - `encryption_example.rb` - Demonstrates AES-256-GCM encryption and payload masking
332
+
333
+ ## Development
334
+
335
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
336
+
337
+ ## Contributing
338
+
339
+ Bug reports and pull requests are welcome on GitHub.
340
+
341
+ ## License
342
+
343
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_job"
4
+
5
+ module Pigeon
6
+ # ActiveJob integration for Pigeon
7
+ module ActiveJobIntegration
8
+ # Job for processing pending outbox messages
9
+ class ProcessorJob < ActiveJob::Base
10
+ queue_as :default
11
+
12
+ # Process pending outbox messages
13
+ # @param batch_size [Integer] Number of messages to process in one batch
14
+ # @return [Hash] Processing statistics
15
+ def perform(batch_size = 100)
16
+ Pigeon.processor.process_pending(batch_size: batch_size)
17
+ end
18
+ end
19
+
20
+ # Job for cleaning up processed outbox messages
21
+ class CleanupJob < ActiveJob::Base
22
+ queue_as :pigeon_cleanup
23
+
24
+ # Clean up processed outbox messages
25
+ # @param older_than [Integer] Age threshold for cleanup in days
26
+ # @return [Integer] Number of records cleaned up
27
+ def perform(older_than = 7)
28
+ Pigeon.processor.cleanup_processed(older_than: older_than)
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/pigeon/api.rb ADDED
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pigeon
4
+ # API methods for the Pigeon module
5
+ module API
6
+ # Delegate methods to Core module
7
+ module CoreAPI
8
+ def configure(&)
9
+ Core.configure(&)
10
+ end
11
+
12
+ def config
13
+ Core.config
14
+ end
15
+
16
+ def initialize_karafka
17
+ Core.initialize_karafka
18
+ end
19
+
20
+ def karafka_producer
21
+ Core.karafka_producer
22
+ end
23
+
24
+ def publisher
25
+ Core.publisher
26
+ end
27
+
28
+ def processor(auto_start: false)
29
+ Core.processor(auto_start: auto_start)
30
+ end
31
+
32
+ def start_processing(batch_size: 100, interval: 5, thread_count: 2)
33
+ Core.start_processing(batch_size: batch_size, interval: interval, thread_count: thread_count)
34
+ end
35
+
36
+ def stop_processing
37
+ Core.stop_processing
38
+ end
39
+
40
+ def processing?
41
+ Core.processing?
42
+ end
43
+ end
44
+
45
+ # Delegate methods to Schema module
46
+ module SchemaAPI
47
+ def register_schema(name, schema)
48
+ Schema.register_schema(name, schema)
49
+ end
50
+
51
+ def schema(name)
52
+ Schema.schema(name)
53
+ end
54
+
55
+ def register_sensitive_field(field)
56
+ Schema.register_sensitive_field(field)
57
+ end
58
+
59
+ def register_sensitive_fields(fields)
60
+ Schema.register_sensitive_fields(fields)
61
+ end
62
+ end
63
+
64
+ # Delegate methods to Security module
65
+ module SecurityAPI
66
+ def encrypt(payload, encryption_key = nil)
67
+ Security.encrypt(payload, encryption_key)
68
+ end
69
+
70
+ def decrypt(encrypted_payload, encryption_key = nil)
71
+ Security.decrypt(encrypted_payload, encryption_key)
72
+ end
73
+
74
+ def mask_payload(payload, sensitive_fields = nil)
75
+ Security.mask_payload(payload, sensitive_fields)
76
+ end
77
+ end
78
+
79
+ # Delegate methods to Outbox module
80
+ module OutboxAPI
81
+ def outbox_message_adapter
82
+ Outbox.outbox_message_adapter
83
+ end
84
+
85
+ def create_outbox_message(attributes = {})
86
+ Outbox.create_outbox_message(attributes)
87
+ end
88
+
89
+ def find_outbox_message(id)
90
+ Outbox.find_outbox_message(id)
91
+ end
92
+
93
+ def find_outbox_messages_by_status(status, limit = 100)
94
+ Outbox.find_outbox_messages_by_status(status, limit)
95
+ end
96
+
97
+ def find_outbox_messages_ready_for_retry(limit = 100)
98
+ Outbox.find_outbox_messages_ready_for_retry(limit)
99
+ end
100
+
101
+ def count_outbox_messages_by_status(status)
102
+ Outbox.count_outbox_messages_by_status(status)
103
+ end
104
+
105
+ def find_oldest_outbox_message_by_status(status)
106
+ Outbox.find_oldest_outbox_message_by_status(status)
107
+ end
108
+ end
109
+
110
+ # Delegate methods to Monitoring module
111
+ module MonitoringAPI
112
+ def metrics_collector
113
+ Monitoring.metrics_collector
114
+ end
115
+
116
+ def metrics
117
+ Monitoring.metrics
118
+ end
119
+
120
+ def metrics_for_monitoring
121
+ Monitoring.metrics_for_monitoring
122
+ end
123
+
124
+ def reset_metrics
125
+ Monitoring.reset_metrics
126
+ end
127
+
128
+ def logger_with_context(context = {})
129
+ Monitoring.logger_with_context(context)
130
+ end
131
+
132
+ def log_level=(level)
133
+ Monitoring.log_level = level
134
+ end
135
+
136
+ def log_level
137
+ Monitoring.log_level
138
+ end
139
+
140
+ def processor_start_time
141
+ Monitoring.processor_start_time
142
+ end
143
+
144
+ def last_processing_run
145
+ Monitoring.last_processing_run
146
+ end
147
+
148
+ def last_successful_processing_run
149
+ Monitoring.last_successful_processing_run
150
+ end
151
+
152
+ def processor_start_time=(time)
153
+ Monitoring.processor_start_time = time
154
+ end
155
+
156
+ def last_processing_run=(time)
157
+ Monitoring.last_processing_run = time
158
+ end
159
+
160
+ def last_successful_processing_run=(time)
161
+ Monitoring.last_successful_processing_run = time
162
+ end
163
+
164
+ def processor_health
165
+ Monitoring.processor_health
166
+ end
167
+
168
+ def queue_health
169
+ Monitoring.queue_health
170
+ end
171
+
172
+ def kafka_health
173
+ Monitoring.kafka_health
174
+ end
175
+
176
+ def health_status
177
+ Monitoring.health_status
178
+ end
179
+ end
180
+
181
+ # Delegate methods to TraceAPI module
182
+ module TracingAPI
183
+ def init_tracing(service_name: "pigeon", exporter: nil)
184
+ TraceAPI.init_tracing(service_name: service_name, exporter: exporter)
185
+ end
186
+
187
+ def tracing_available?
188
+ TraceAPI.tracing_available?
189
+ end
190
+
191
+ def tracer(name = "pigeon")
192
+ TraceAPI.tracer(name)
193
+ end
194
+
195
+ def with_span(name, attributes: {}, kind: :internal, &)
196
+ TraceAPI.with_span(name, attributes: attributes, kind: kind, &)
197
+ end
198
+ end
199
+ end
200
+ end