๐ OmniEvent
"One Gem to rule them all, One Gem to find them, One Gem to bring all logs, and in the shadows, trace them."
OmniEvent is a production-ready Rails Engine that unifies your system's entire event lifecycle โ from secure external webhook ingestion to detailed internal process auditing โ through a single, traceable pipeline.
Table of Contents
- Key Features
- Installation
- Configuration
- Receiving Webhooks
- Processing Pipeline
- Polymorphic Logging
- Security
- Database Maintenance
- Testing
- Docker Development
๐ Key Features
- Secure Webhook Receiver โ token auth, IP whitelisting, HMAC signature verification, and replay attack protection out of the box.
- Step Pipeline โ organize complex business logic into traceable steps with automatic error capturing and context logging.
-
Polymorphic Logging โ attach structured logs to any model (
Order,User,Payment, etc.) with a unified API. - Processor Registry โ map each webhook source (Notifier) to its own processor class via configuration.
- Async Monitoring โ non-blocking New Relic Insights integration via ActiveJob.
-
Smart Cleanup โ built-in Rake task for data retention based on configurable
retention_days. - Zero-Boilerplate DX โ Devise-like installation, Rails callback-inspired syntax.
๐ ๏ธ Installation
1. Add to your Gemfile:
gem 'omni_event'2. Run the installer:
bundle install
rails generate omni_event:install
rails db:migrateThe generator creates:
-
config/initializers/omni_event.rbโ your configuration file -
app/models/log.rbโ local proxy forOmniEvent::Log -
app/models/webhook_event.rbโ local proxy forOmniEvent::WebhookEvent
3. Mount the engine in config/routes.rb:
mount OmniEvent::Engine => "/omni_events"
# Exposes: POST /omni_events/receiver/:tokenโ๏ธ Configuration
All options live in config/initializers/omni_event.rb:
OmniEvent.configure do |config|
# โโ Monitoring โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
config.new_relic_enabled = true
config.new_relic_api_key = ENV['NEW_RELIC_KEY']
config.new_relic_account_id = ENV['NEW_RELIC_ACCOUNT_ID']
# โโ Processing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
config.process_async = true # false = synchronous (useful for testing)
config.retention_days = 30 # used by rake omni_event:cleanup
# โโ Custom log types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Define domain-specific action types for your business context.
config.custom_log_types = {
system_info: 0,
system_error: 1,
payment_received: 10,
user_update: 20,
fiscal_validation: 30
}
# โโ Processor registry โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Maps each Notifier name to the processor class that handles its events.
config.processors = {
"PaymentGateway" => Webhooks::PaymentGatewayProcessor,
"BillingService" => Webhooks::BillingServiceProcessor,
"CrmSystem" => Webhooks::CrmSystemProcessor
}
end๐ก Receiving Webhooks
1. Create a Notifier
A Notifier represents one external webhook source (e.g. a payment gateway, a payment processor). Each has its own security configuration.
# Minimal โ token auth only
notifier = OmniEvent::Notifier.create!(name: "Stripe")
# => token is auto-generated: SecureRandom.hex(24)
# Full security configuration
notifier = OmniEvent::Notifier.create!(
name: "Payment Gateway",
secret_key: ENV['WEBHOOK_SECRET'], # enables HMAC verification
timestamp_tolerance: 300, # 5-minute replay window (seconds)
check_ip: true,
allowed_ips: ["185.60.216.35", "185.60.218.35"]
)
# The webhook endpoint for this notifier:
# POST /omni_events/receiver/#{notifier.token}
puts notifier.token # => "a3f9c2b1e4d7..."2. Register the processor
In config/initializers/omni_event.rb, map the notifier name to a processor class:
config.processors = {
"Payment Gateway" => Webhooks::PaymentGatewayProcessor
}3. Send a webhook
The partner sends a POST request to your endpoint:
curl -X POST https://yourapp.com/omni_events/receiver/a3f9c2b1e4d7... \
-H "Content-Type: application/json" \
-H "X-OmniEvent-Timestamp: $(date +%s)" \
-H "X-OmniEvent-Signature: sha256=$(echo -n '{"event":"payment.confirmed"}' | openssl dgst -sha256 -hmac 'your_secret')" \
-d '{"event":"payment.confirmed","charge_id":"ch_abc123","status":"paid"}'The receiver will:
- Validate payload size (max 1MB)
- Authenticate via token
- Check IP whitelist (if enabled)
- Verify HMAC signature + timestamp (if
secret_keyis set) - Persist the
WebhookEvent - Dispatch to the registered processor (async or sync)
๐ Processing Pipeline
Define your business logic as a sequence of named steps. OmniEvent automatically logs any step failure with full context (step name, error class, backtrace).
# app/services/webhooks/payment_gateway_processor.rb
class Webhooks::PaymentGatewayProcessor < OmniEvent::BaseProcessor
steps :validate_payload,
:update_payment_status,
:notify_customer,
:record_audit_log
def validate_payload
raise "Missing charge ID" if event.payload[:charge_id].blank?
raise "Unknown status '#{event.payload[:status]}'" unless valid_status?
end
def update_payment_status
payment.update!(status: event.payload[:status])
end
def notify_customer
CustomerMailer.payment_update(payment).deliver_later
end
def record_audit_log
Log.create!(
loggable: payment,
action_type: :payment_processed,
content: "Payment status updated to '#{event.payload[:status]}'",
metadata: { gateway: "Stripe", source: "webhook", timestamp: Time.current.iso8601 }
)
end
private
def payment
@payment ||= Payment.find_by!(charge_id: event.payload[:charge_id])
end
def valid_status?
%w[paid pending failed refunded disputed].include?(event.payload[:status])
end
endWhen a step raises an error, OmniEvent automatically creates a system_error log with the context and re-raises so the job can retry:
# Auto-created by OmniEvent on step failure:
OmniEvent::Log.create!(
loggable: event,
action_type: :system_error,
content: "FAILURE in step [Validate payload]: Missing charge ID",
metadata: {
error_class: "RuntimeError",
method: :validate_payload,
backtrace: [...]
}
)๐ Polymorphic Logging
Use Log (the local proxy generated by the installer) to attach structured log entries to any model.
# Attach to any ActiveRecord model
Log.create!(
loggable: @order,
action_type: :payment_received,
content: "Payment of R$ 1.250,00 confirmed via PIX",
metadata: { gateway: "Stripe", charge_id: "ch_abc123", amount_cents: 125_000 }
)
# Query logs for a specific record
@order.logs.where(action_type: :system_error).order(created_at: :desc)
# Custom scopes on your local Log model (app/models/log.rb)
class Log < OmniEvent::Log
scope :recent_errors, -> { where(action_type: :system_error).where('created_at > ?', 24.hours.ago) }
scope :for_gateway, ->(gw) { where("metadata->>'gateway' = ?", gw) }
endCustom log types
Define your domain vocabulary in the initializer:
config.custom_log_types = {
system_info: 0,
system_error: 1,
payment_received: 10,
payment_failed: 11,
payment_processed: 20,
fiscal_validation: 30
}๐ Security
OmniEvent provides 4 independent security layers, all configurable per Notifier. Each layer is opt-in and backward compatible.
Layer 1 โ Token Authentication
Every webhook endpoint is identified by a unique, cryptographically random token (48-char hex). Requests without a valid token receive 401 Unauthorized.
notifier = OmniEvent::Notifier.create!(name: "Partner")
# Endpoint: POST /omni_events/receiver/#{notifier.token}Layer 2 โ IP Whitelisting
Restrict which IPs can send requests to each notifier.
OmniEvent::Notifier.create!(
name: "Stripe",
check_ip: true,
allowed_ips: ["54.187.174.169", "54.187.205.235"]
)Requests from non-whitelisted IPs receive 403 Forbidden.
Layer 3 โ HMAC Signature Verification
The gold standard for webhook security. The sender signs the raw request body with a shared secret using HMAC-SHA256. OmniEvent verifies the signature using constant-time comparison (preventing timing attacks).
OmniEvent::Notifier.create!(
name: "Stripe",
secret_key: ENV['STRIPE_WEBHOOK_SECRET'] # e.g. "whsec_abc123..."
)Required header from the sender:
X-OmniEvent-Signature: sha256=<HMAC-SHA256(secret_key, raw_body)>
Example โ generating the signature (sender side):
# Ruby
signature = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_body)}"
# Node.js
const sig = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
# Python
import hmac, hashlib
sig = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()Layer 4 โ Replay Attack Protection
When secret_key is set, OmniEvent also validates a timestamp header to reject requests that are too old โ preventing replay attacks where a valid captured request is re-sent.
OmniEvent::Notifier.create!(
name: "Stripe",
secret_key: ENV['STRIPE_WEBHOOK_SECRET'],
timestamp_tolerance: 300 # reject requests older than 5 minutes (default)
)Required header from the sender:
X-OmniEvent-Timestamp: <Unix timestamp, e.g. 1711800000>
Set timestamp_tolerance: 0 to disable timestamp checking while keeping signature verification.
Layer 5 โ Payload Size Limit
All requests are automatically capped at 1MB. Oversized payloads receive 413 Payload Too Large before any processing occurs.
Complete security setup example
# Notifier with all layers active
notifier = OmniEvent::Notifier.create!(
name: "Stripe Payments",
secret_key: ENV['STRIPE_WEBHOOK_SECRET'],
timestamp_tolerance: 300,
check_ip: true,
allowed_ips: ["185.60.216.35"]
)
# Check which security features are active
notifier.signature_verification? # => true
notifier.check_ip? # => trueSecurity response codes
| Condition | HTTP Status |
|---|---|
| Payload > 1MB | 413 Payload Too Large |
| Invalid or missing token | 401 Unauthorized |
| IP not whitelisted | 403 Forbidden |
| Invalid/missing signature | 401 Unauthorized |
| Timestamp outside window | 401 Unauthorized |
๐๏ธ Database Maintenance
Prevent database bloating by periodically deleting old records:
rake omni_event:cleanup
# => [OmniEvent] Cleanup complete: 1543 logs and 892 webhook events deleted (older than 30 days).Configure the retention period in your initializer:
config.retention_days = 90 # keep records for 90 daysSchedule it in production (e.g. with whenever or Heroku Scheduler):
# config/schedule.rb (whenever gem)
every 1.day, at: '2:00 am' do
rake "omni_event:cleanup"
end๐งช Testing
Unit tests (no database required)
bundle exec rspecIntegration tests (requires the dummy Rails app)
INTEGRATION=1 bundle exec rspecTesting your processors
RSpec.describe Webhooks::PaymentGatewayProcessor do
let(:notifier) { create(:omni_event_notifier) }
let(:event) { create(:omni_event_webhook_event, webhook_notifier: notifier, payload: { charge_id: "ch_abc123", status: "paid" }) }
it "updates the payment status" do
payment = create(:payment, charge_id: "ch_abc123")
described_class.new(event).process!
expect(payment.reload.status).to eq("paid")
end
it "creates an audit log" do
create(:payment, charge_id: "ch_abc123")
expect { described_class.new(event).process! }.to change(Log, :count).by(1)
end
it "creates a system_error log when a step fails" do
allow_any_instance_of(described_class).to receive(:validate_payload).and_raise("boom")
expect { described_class.new(event).process! }.to raise_error("boom")
expect(OmniEvent::Log.last.action_type).to eq("system_error")
end
end๐ณ Docker Development
docker compose up -d
docker compose exec app bash
bundle exec rspec
rake omni_event:cleanup๐ License
The gem is available as open source under the terms of the MIT License.
Developed with โค๏ธ by Antonio Neto