класс ActiveSupport::EventReporter
Репортёр событий Active Support
ActiveSupport::EventReporter предоставляет интерфейс для передачи структурированных событий подписчикам.
Для передачи события можно использовать метод notify:
Rails.event.notify("user_created", { id: 123 })
# Emits event:
# {
# name: "user_created",
# payload: { id: 123 },
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
API notify принимает либо имя события и хеш с полезной нагрузкой, либо объект события. Имена преобразуются в строки.
Объекты событий
Если API notify передан объект события, он будет передан подписчикам без изменений, а имя класса объекта будет использовано в качестве имени события.
class UserCreatedEvent
def initialize(id:, name:)
@id = id
@name = name
end
def serialize
{
id: @id,
name: @name
}
end
end
Rails.event.notify(UserCreatedEvent.new(id: 123, name: "John Doe"))
# Emits event:
# {
# name: "UserCreatedEvent",
# payload: #<UserCreatedEvent:0x111>,
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
Событием может быть любой объект Ruby, представляющий событие с определённой схемой. Хеши с полезной нагрузкой позволяют передавать произвольные данные с неявной структурой, тогда как объекты событий предназначены для обеспечения соблюдения конкретной схемы.
Подписчики отвечают за сериализацию объектов событий.
Подписчики
Подписчики должны реализовать метод emit, который будет вызван с хешем события.
Хеш события содержит следующие ключи:
name: String (The name of the event) payload: Hash, Object (The payload of the event, or the event object itself) tags: Hash (The tags of the event) context: Hash (The context of the event) timestamp: Float (The timestamp of the event, in nanoseconds) source_location: Hash (The source location of the event, containing the filepath, lineno, and label)
Подписчики отвечают за кодирование событий в нужный им формат перед отправкой в целевое место назначения, например на стриминговую платформу, в устройство журналирования или службу оповещений.
class JSONEventSubscriber
def emit(event)
json_data = JSON.generate(event)
LogExporter.export(json_data)
end
end
class LogSubscriber
def emit(event)
payload = event[:payload].map { |key, value| "#{key}=#{value}" }.join(" ")
source_location = event[:source_location]
log = "[#{event[:name]}] #{payload} at #{source_location[:filepath]}:#{source_location[:lineno]}"
Rails.logger.info(log)
end
end
Обратите внимание: объекты событий передаются подписчикам без изменений, и перед кодированием их может потребоваться сериализовать:
class UserCreatedEvent
def initialize(id:, name:)
@id = id
@name = name
end
def serialize
{
id: @id,
name: @name
}
end
end
class LogSubscriber
def emit(event)
payload = event[:payload]
json_data = JSON.generate(payload.serialize)
LogExporter.export(json_data)
end
end
Подписки с фильтрацией
Для подписчиков можно настроить необязательный proc-фильтр, чтобы они получали только часть событий:
# Only receive events with names starting with "user."
Rails.event.subscribe(user_subscriber) { |event| event[:name].start_with?("user.") }
# Only receive events with specific payload types
Rails.event.subscribe(audit_subscriber) { |event| event[:payload].is_a?(AuditEvent) }
Отладочные события
Можно использовать метод debug для передачи события, которое будет передано, только если репортёр событий находится в режиме отладки:
Rails.event.debug("my_debug_event", { foo: "bar" })
Теги
Чтобы добавить к событию дополнительный контекст отдельно от полезной нагрузки, можно добавить теги с помощью метода tagged:
Rails.event.tagged("graphql") do
Rails.event.notify("user_created", { id: 123 })
end
# Emits event:
# {
# name: "user_created",
# payload: { id: 123 },
# tags: { graphql: true },
# context: {},
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
Хранилище контекста
Может потребоваться прикрепить метаданные к каждому событию, передаваемому репортёром. Теги задают предметно-ориентированный контекст для группы событий, тогда как контекст ограничен заданием или запросом и предназначен для метаданных, связанных с контекстом выполнения. Контекст можно задать с помощью метода set_context:
Rails.event.set_context(request_id: "abcd123", user_agent: "TestAgent")
Rails.event.notify("user_created", { id: 123 })
# Emits event:
# {
# name: "user_created",
# payload: { id: 123 },
# tags: {},
# context: { request_id: "abcd123", user_agent: "TestAgent" },
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
Контекст автоматически сбрасывается до и после каждого запроса.
Пользовательское хранилище контекста можно настроить с помощью config.active_support.event_reporter_context_store.
# config/application.rb
config.active_support.event_reporter_context_store = CustomContextStore
class CustomContextStore
class << self
def context
# Return the context.
end
def set_context(context_hash)
# Append context_hash to the existing context store.
end
def clear
# Delete the stored context.
end
end
end
Репортёр событий использует ключи-символы для всех данных полезной нагрузки, тегов и записей хранилища контекста. Для единообразия ключи String автоматически преобразуются в символы.
Rails.event.notify("user.created", { "id" => 123 })
# Emits event:
# {
# name: "user.created",
# payload: { id: 123 },
# }
Безопасность
При передаче событий полезные нагрузки на основе Hash автоматически фильтруются для удаления конфиденциальных данных согласно настройке Rails.application.filter_parameters.
Если вместо этого передан объект события, подписчикам потребуется самостоятельно фильтровать конфиденциальные данные, например с помощью ActiveSupport::ParameterFilter.
Атрибуты
Указывает, следует ли возбуждать исключение, если при передаче события подписчик возбуждает исключение или если в notify переданы неожиданные аргументы.
Открытые методы класса
# File activesupport/lib/active_support/event_reporter.rb, line 286
def initialize(*subscribers, raise_on_error: false)
@subscribers = []
subscribers.each { |subscriber| subscribe(subscriber) }
@debug_mode = false
@raise_on_error = raise_on_error
end Открытые методы экземпляра
# File activesupport/lib/active_support/event_reporter.rb, line 525 def clear_context context_store.clear end
Очищает все данные контекста.
# File activesupport/lib/active_support/event_reporter.rb, line 530 def context context_store.context end
Возвращает текущие данные контекста.
# File activesupport/lib/active_support/event_reporter.rb, line 435
def debug(name_or_object, payload = nil, caller_depth: 1, **kwargs)
if debug_mode?
if block_given?
notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs.merge(yield))
else
notify(name_or_object, payload, caller_depth: caller_depth + 1, **kwargs)
end
end
end Передаёт событие только в режиме отладки. Например:
Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
Аргументы
-
:payload— полезная нагрузка события при использовании строковых имён или имён-символов. -
:caller_depth— глубина стека вызовов, используемая для определения местоположения в исходном коде (по умолчанию: 1). -
:kwargs— дополнительные данные полезной нагрузки при использовании строковых имён или имён-символов.
# File activesupport/lib/active_support/event_reporter.rb, line 420 def debug_mode? @debug_mode || Fiber[:event_reporter_debug_mode] end
Проверяет, включён ли сейчас режим отладки. Режим отладки включается для репортёра с помощью with_debug, а также в локальных окружениях.
# File activesupport/lib/active_support/event_reporter.rb, line 363
def notify(name_or_object, payload = nil, caller_depth: 1, **kwargs)
name = resolve_name(name_or_object)
payload = resolve_payload(name_or_object, payload, **kwargs)
event = {
name: name,
payload: payload,
tags: TagStack.tags,
context: context_store.context,
timestamp: Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond),
}
caller_location = caller_locations(caller_depth, 1)&.first
if caller_location
source_location = {
filepath: caller_location.path,
lineno: caller_location.lineno,
label: caller_location.label,
}
event[:source_location] = source_location
end
@subscribers.each do |subscriber_entry|
subscriber = subscriber_entry[:subscriber]
filter = subscriber_entry[:filter]
next if filter && !filter.call(event)
subscriber.emit(event)
rescue => subscriber_error
if raise_on_error?
raise
else
ActiveSupport.error_reporter.report(subscriber_error, handled: true)
end
end
nil
end Передаёт событие всем зарегистрированным подписчикам. Можно указать имя события и полезную нагрузку:
Rails.event.notify("user.created", { id: 123 })
# Emits event:
# {
# name: "user.created",
# payload: { id: 123 },
# tags: {},
# context: {},
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
Вместо этого можно передать объект события:
Rails.event.notify(UserCreatedEvent.new(id: 123))
# Emits event:
# {
# name: "UserCreatedEvent",
# payload: #<UserCreatedEvent:0x111>,
# tags: {},
# context: {},
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
Аргументы
-
:payload— полезная нагрузка события при использовании строковых имён или имён-символов. -
:caller_depth— глубина стека вызовов, используемая для определения местоположения в исходном коде (по умолчанию: 1). -
:kwargs— дополнительные данные полезной нагрузки при использовании строковых имён или имён-символов.
# File activesupport/lib/active_support/event_reporter.rb, line 520 def set_context(context) context_store.set_context(context) end
Задаёт данные контекста, которые будут включены во все события, передаваемые репортёром. Данные контекста должны быть ограничены заданием или запросом; они автоматически сбрасываются до и после каждого запроса и задания.
Rails.event.set_context(user_agent: "TestAgent")
Rails.event.set_context(job_id: "abc123")
Rails.event.tagged("graphql") do
Rails.event.notify("user_created", { id: 123 })
end
# Emits event:
# {
# name: "user_created",
# payload: { id: 123 },
# tags: { graphql: true },
# context: { user_agent: "TestAgent", job_id: "abc123" },
# timestamp: 1738964843208679035
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
# File activesupport/lib/active_support/event_reporter.rb, line 311
def subscribe(subscriber, &filter)
unless subscriber.respond_to?(:emit)
raise ArgumentError, "Event subscriber #{subscriber.class.name} must respond to #emit"
end
@subscribers << { subscriber: subscriber, filter: filter }
end Регистрирует нового подписчика на события. Подписчик должен отвечать на вызов
emit(event: Hash)
Хеш события будет содержать следующие ключи:
name: String (The name of the event) payload: Hash, Object (The payload of the event, or the event object itself) tags: Hash (The tags of the event) context: Hash (The context of the event) timestamp: Float (The timestamp of the event, in nanoseconds) source_location: Hash (The source location of the event, containing the filepath, lineno, and label)
Можно указать необязательный proc-фильтр, чтобы получать только часть событий:
Rails.event.subscribe(subscriber) { |event| event[:name].start_with?("user.") }
Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }
# File activesupport/lib/active_support/event_reporter.rb, line 497 def tagged(*args, **kwargs, &block) TagStack.with_tags(*args, **kwargs, &block) end
Добавляет к событиям теги, чтобы предоставить дополнительный контекст. Теги работают по принципу стека, поэтому все события, переданные внутри блока, наследуют один и тот же набор тегов. Например:
Rails.event.tagged("graphql") do
Rails.event.notify("user.created", { id: 123 })
end
# Emits event:
# {
# name: "user.created",
# payload: { id: 123 },
# tags: { graphql: true },
# context: {},
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
Теги можно передавать в качестве аргументов или именованных аргументов, а также вкладывать друг в друга:
Rails.event.tagged("graphql") do
# Other code here...
Rails.event.tagged(section: "admin") do
Rails.event.notify("user.created", { id: 123 })
end
end
# Emits event:
# {
# name: "user.created",
# payload: { id: 123 },
# tags: { section: "admin", graphql: true },
# context: {},
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
API tagged также может принимать объект тега:
graphql_tag = GraphqlTag.new(operation_name: "user_created", operation_type: "mutation")
Rails.event.tagged(graphql_tag) do
Rails.event.notify("user.created", { id: 123 })
end
# Emits event:
# {
# name: "user.created",
# payload: { id: 123 },
# tags: { "GraphqlTag": #<GraphqlTag:0x111> },
# context: {},
# timestamp: 1738964843208679035,
# source_location: { filepath: "path/to/file.rb", lineno: 123, label: "UserService#create" }
# }
# File activesupport/lib/active_support/event_reporter.rb, line 326
def unsubscribe(subscriber)
@subscribers.delete_if { |s| subscriber === s[:subscriber] }
end Отменяет регистрацию подписчика на события. Принимает подписчика или класс.
subscriber = MyEventSubscriber.new Rails.event.subscribe(subscriber) Rails.event.unsubscribe(subscriber) # or Rails.event.unsubscribe(MyEventSubscriber)
# File activesupport/lib/active_support/event_reporter.rb, line 410 def with_debug prior = Fiber[:event_reporter_debug_mode] Fiber[:event_reporter_debug_mode] = true yield ensure Fiber[:event_reporter_debug_mode] = prior end
Временно включает режим отладки на время выполнения блока. Вызовы debug будут передавать события, только если включён режим отладки.
Rails.event.with_debug do
Rails.event.debug("sql.query", { sql: "SELECT * FROM users" })
end
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.