Module: ActiveSanction::Instrumentation

Extended by:
T::Sig
Defined in:
lib/active_sanction/instrumentation.rb,
lib/active_sanction/instrumentation/event.rb,
lib/active_sanction/instrumentation/notifications.rb

Overview

Structured events from every stage, so a host can measure this library without monkeypatching it.

ActiveSanction.configure do |c|
c.instrumenter = ->(event) do
  StatsD.timing("sanctions.#{event.name}", event.duration_ms, tags: ["source:#{event.source}"])
end
end

Six events, and they are the operational questions a compliance installation is actually asked: is the data fresh, did a fetch fail, how long did screening take, which source is degrading.

Event Emitted by Asks
:fetch Fetcher Did bytes move, and what did the publisher answer?
:parse Sources::Base How many records came out, and how many rows could not be read?
:store Sync, Client#import Which list version was written, and how big is it?
:"index.build" Matcher.build What did building the index cost, and how much is resident?
:screen Matcher How long did a query take, and what did it consult?
:sync Sync What did a whole run do?

The payload keys of each are enumerated in docs/api_stability.md and are public API: a dashboard built on them is held to the same promise as a method call, and a key is not removed or repurposed without the deprecation path. Keys may be added to an event, so a subscriber reads the keys it knows and ignores the rest.

A subscriber is anything answering #call(event)

A lambda, a Method, an object with a call. There is no registry and no base class to inherit, because the whole interface is one method and a registry would be a second thing to configure. A host wanting several subscribers composes them itself -- ->(event) { subscribers.each { |s| s.call(event) } } -- which is one line and is exactly what a fan-out registry here would be.

Rails hosts have one already: see Notifications, which republishes every event into ActiveSupport::Notifications under <name>.active_sanction. It is an adapter rather than a dependency -- nothing here requires ActiveSupport, and the class refuses to build in a process that has not loaded it.

Nothing is listening by default, and that costs nothing

instrumenter defaults to nil, and a nil instrumenter is not a no-op object that gets called and returns -- it is a branch taken before anything is allocated. .instrument with no instrumenter calls the block with a payload that discards writes and returns, so a stage that fills in fifteen fields allocates no Hash and builds no Event. This is what keeps the #37 benchmarks where they were.

A subscriber must be safe to call from several threads

sync!(concurrency: 3) fetches from three publishers at once, and the :fetch, :parse and :store events of those three arrive on three threads. Nothing here serializes them -- a lock around a subscriber would make instrumentation a source of contention in the one place this library deliberately fans out. A subscriber that appends to a plain Array wants a Mutex of its own; one that hands an event to a metrics client is already fine, because those are.

The :screen event is the same statement from the other direction: a Matcher is screened from every thread a host has, so a subscriber counting queries is counting them concurrently.

A raising subscriber cannot break a sync

Instrumentation is a measurement of the work and is never part of it. A subscriber that raises has its exception caught, reported once, and dropped; the stage it was measuring carries on and returns what it was going to return. The converse is equally deliberate: instrumentation never swallows the library's own exceptions. An event is emitted for a stage that raised, carrying error:, and then the exception continues exactly as if nothing were listening.

See Also:

Defined Under Namespace

Classes: Event, Notifications

Constant Summary collapse

EVENTS =

Every event name this library emits. Enumerated so a host can assert against the list rather than discovering a name in production, and frozen because it is the published vocabulary -- see the class comment on what may and may not change about it.

T.let(%i[fetch parse store index.build screen sync].freeze, T::Array[Symbol])

Class Method Summary collapse

Class Method Details

.reset! ⇒ void

This method returns an undefined value.

Forget which subscriber failures have already been reported, so the next one is reported again. For a spec that asserts a broken subscriber is reported; nothing in a running application should call it.



197
198
199
# File 'lib/active_sanction/instrumentation.rb', line 197

def reset!
  @mutex.synchronize { @failures.clear }
end