feat: add Prism, Instrumentation and Jobs
This commit is contained in:
1 parent
069b348302
commit
6825941c0b
3 files changed
+203
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user