Spec-Zone.ru › Ruby on Rails 8.1

класс ActiveSupport::EventReporter

Родительский класс:
Object

Репортёр событий 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.

Атрибуты

raise_on_error [W]

Указывает, следует ли возбуждать исключение, если при передаче события подписчик возбуждает исключение или если в notify переданы неожиданные аргументы.

subscribers [R]

Открытые методы класса

new (*subscribers, raise_on_error: false) Показать исходный код
# 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

Открытые методы экземпляра

clear_context () Показать исходный код
# File activesupport/lib/active_support/event_reporter.rb, line 525
def clear_context
  context_store.clear
end

Очищает все данные контекста.

context () Показать исходный код
# File activesupport/lib/active_support/event_reporter.rb, line 530
def context
  context_store.context
end

Возвращает текущие данные контекста.

debug (name_or_object, payload = nil, caller_depth: 1, **kwargs) { || ... } Показать исходный код
# 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 — дополнительные данные полезной нагрузки при использовании строковых имён или имён-символов.

debug_mode? () Показать исходный код
# File activesupport/lib/active_support/event_reporter.rb, line 420
def debug_mode?
  @debug_mode || Fiber[:event_reporter_debug_mode]
end

Проверяет, включён ли сейчас режим отладки. Режим отладки включается для репортёра с помощью with_debug, а также в локальных окружениях.

notify (name_or_object, payload = nil, caller_depth: 1, **kwargs) Показать исходный код
# 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 — дополнительные данные полезной нагрузки при использовании строковых имён или имён-символов.

set_context (context) Показать исходный код
# 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" }
#  }
subscribe (subscriber, &filter) Показать исходный код
# 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) }
tagged (*args, **kwargs, &block) Показать исходный код
# 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" }
#  }
unsubscribe (subscriber) Показать исходный код
# 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)
with_debug () { || ... } Показать исходный код
# 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.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API