feat: add Prism, Instrumentation and Jobs

This commit is contained in:
dualfroz committed 2026-09-06 21:11:47 +02:00
1 parent 069b348302
commit 6825941c0b
3 files changed
+203

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
module Prism
# ActiveSupport::Notifications-lite: a process-wide publish/subscribe bus
# for cross-cutting events (queries, request dispatch, template renders).
# Instrumented code owns naming and payload shape; subscribers (loggers,
# metrics collectors, tests) opt into a subset by exact name or Regexp.
module Instrumentation
Event = Struct.new(:name, :payload, :duration, :time) do
def duration_ms
duration * 1000.0
end
end
class Subscriber
def initialize(pattern, block)
@pattern = pattern
@block = block
end
def match?(name)
case @pattern
when nil then true
when Regexp then @pattern.match?(name)
else @pattern.to_s == name
end
end
def call(event)
@block.call(event)
end
end
class Bus
def initialize
@subscribers = []
@mutex = Mutex.new
end
def subscribe(pattern = nil, &block)
raise ArgumentError, 'block required' unless block
subscriber = Subscriber.new(pattern, block)
@mutex.synchronize { @subscribers = @subscribers + [subscriber] }
subscriber
end
def unsubscribe(subscriber)
@mutex.synchronize { @subscribers = @subscribers - [subscriber] }
end
def subscribers
@mutex.synchronize { @subscribers.dup }
end
# Yields a mutable payload hash so instrumented code (and nested
# instrument calls further down the stack) can enrich it before the
# event is published. Always publishes, even when the block raises -
# the exception is recorded on the payload and re-raised afterward.
def instrument(name, payload = {})
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
time = Time.now
result = nil
begin
result = block_given? ? yield(payload) : nil
result
rescue StandardError => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
raise
ensure
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
publish(Event.new(name, payload, duration, time))
end
end
private
def publish(event)
subscribers.each do |subscriber|
next unless subscriber.match?(event.name)
subscriber.call(event)
end
end
end
class << self
def bus
@bus ||= Bus.new
end
def instrument(name, payload = {}, &block)
bus.instrument(name, payload, &block)
end
def subscribe(pattern = nil, &block)
bus.subscribe(pattern, &block)
end
def unsubscribe(subscriber)
bus.unsubscribe(subscriber)
end
end
# Rails-style one-line-per-event logger: " sql.prism (0.3ms) SELECT posts".
# Attach once (typically in an initializer / Application setup); returns
# the underlying Subscriber so callers can #detach it later.
class LogSubscriber
def self.attach(io = $stdout, patterns: [nil])
patterns.map do |pattern|
Instrumentation.subscribe(pattern) { |event| io.puts(format_event(event)) }
end
end
def self.format_event(event)
ms = format('%.1f', event.duration_ms)
details = event.payload.reject { |key, _| key == :exception_object }
.map { |key, value| "#{key}=#{value.inspect}" }.join(' ')
line = " #{event.name} (#{ms}ms)"
details.empty? ? line : "#{line} #{details}"
end
end
end
end
+15
View File
@@ -0,0 +1,15 @@
require_relative 'jobs/job'
require_relative 'jobs/queue'
require_relative 'jobs/worker'
module Prism
module Jobs
class << self
attr_writer :default_queue
def default_queue
@default_queue ||= Queue.new('db/jobs.pstore')
end
end
end
end
+67
View File
@@ -0,0 +1,67 @@
require_relative 'mailer/message'
require_relative 'mailer/delivery'
require_relative 'mailer/delivery_job'
require_relative 'mailer/base'
require_relative 'mailer/preview'
require_relative 'mailer/preview_app'
module Prism
module Mailer
class << self
attr_accessor :smtp_settings
# Router used by Mailer::Base#url_for to build absolute links inside
# mail bodies, where there is no incoming request to pull a router off
# of. Set once, normally via Application#enable_mailer, e.g.
# `Prism::Mailer.router = BlogApp.router`.
attr_accessor :router
attr_writer :delivery_method, :default_url_options
def delivery_method
@delivery_method ||= :test
end
# App-wide fallback for Mailer::Base.default_url_options (host,
# protocol, port); a mailer class's own default_url_options take
# precedence when both are set.
def default_url_options
@default_url_options ||= {}
end
# Messages handed to Mailer.deliver! while delivery_method == :test
# pile up here instead of going out over the network, mirroring
# ActionMailer::Base.deliveries for assertions in tests.
def deliveries
@deliveries ||= []
end
def deliver!(message)
Instrumentation.instrument('deliver_mail.prism', subject: message.subject,
to: message.to,
delivery_method: delivery_method) do
case delivery_method
when :test
deliveries << message
when :smtp
deliver_via_smtp(message)
else
raise ArgumentError, "unknown Prism::Mailer.delivery_method #{delivery_method.inspect}"
end
end
message
end
private
def deliver_via_smtp(message)
require 'net/smtp'
settings = smtp_settings || {}
Net::SMTP.start(settings[:address] || 'localhost', settings[:port] || 25,
settings[:domain] || 'localhost', settings[:user_name],
settings[:password], settings[:authentication]) do |smtp|
smtp.send_message(message.to_s, message.from, message.all_recipients)
end
end
end
end
end