Class: ActiveSanction::Instrumentation::Event

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/active_sanction/instrumentation/event.rb

Overview

One thing this library did, after it finished doing it.

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

event.name       # => :fetch
event.duration   # => 2.418, seconds
event[:source]   # => :ofac_sdn
event[:bytes]    # => 12_845_056

A subscriber is handed a finished event and never a running one. That is the difference between this and a block-based instrumenter, and it is deliberate: a subscriber that cannot wrap the work cannot retry it, cannot swallow its exception, and cannot leave a begin half-entered if it raises. The cost is that nothing here can time a stage from the outside -- which nothing needs to, since every event already carries its own duration.

Every event carries a duration and enough to correlate it

There are no anonymous timings. duration is monotonic seconds, taken across the stage this event describes, and started_at is the wall clock at its start -- both, because one answers "how long did it take" and the other answers "when, in the log I am reading beside this". Every event but screen names a source; screen names the snapshot checksums it consulted, which is the same question asked of a query.

An event is emitted for work that raised

With error set to the exception, and with whichever payload keys the stage had filled in before it failed. A publisher that starts timing out is exactly what a host is watching for, and an instrumentation layer that only reports successes cannot see it. See Instrumentation.

Frozen, and its payload with it, so a subscriber cannot edit what the next one is handed.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, payload:, started_at:, duration:) ⇒ void

Parameters:

  • name (Symbol)
  • payload (Hash{Symbol => T.untyped})
  • started_at (Time)
  • duration (Float)


70
71
72
73
74
75
76
# File 'lib/active_sanction/instrumentation/event.rb', line 70

def initialize(name:, payload:, started_at:, duration:)
  @name = T.let(name, Symbol)
  @payload = T.let(payload.freeze, T::Hash[Symbol, T.untyped])
  @started_at = T.let(started_at, Time)
  @duration = T.let(duration, Float)
  freeze
end

Instance Attribute Details

#duration ⇒ Float (readonly)

Seconds the stage took, from a monotonic clock, so it is not moved by a clock adjustment mid-stage.

Returns:

  • (Float)


65
66
67
# File 'lib/active_sanction/instrumentation/event.rb', line 65

def duration
  @duration
end

#name ⇒ Symbol (readonly)

The stage this event describes -- one of Instrumentation::EVENTS.

Returns:

  • (Symbol)


50
51
52
# File 'lib/active_sanction/instrumentation/event.rb', line 50

def name
  @name
end

#payload ⇒ Hash{Symbol => T.untyped} (readonly)

What the stage measured, keyed as documented for each event name in docs/api_stability.md. Frozen.

Returns:

  • (Hash{Symbol => T.untyped})


55
56
57
# File 'lib/active_sanction/instrumentation/event.rb', line 55

def payload
  @payload
end

#started_at ⇒ Time (readonly)

The wall clock when the stage started, UTC. For lining an event up against a log; duration is what to measure with.

Returns:

  • (Time)


60
61
62
# File 'lib/active_sanction/instrumentation/event.rb', line 60

def started_at
  @started_at
end

Instance Method Details

#[](key) ⇒ T.untyped

One payload key, or nil for one this event does not carry.

Parameters:

  • key (Symbol)

Returns:

  • (T.untyped)


80
# File 'lib/active_sanction/instrumentation/event.rb', line 80

def [](key) = payload[key]

#duration_ms ⇒ Float

Milliseconds, which is what a metrics backend usually wants.

Returns:

  • (Float)


104
# File 'lib/active_sanction/instrumentation/event.rb', line 104

def duration_ms = (duration * 1_000).round(3).to_f

#error ⇒ StandardError?

The exception the stage raised, or nil for one that finished. An event carrying one is a partial measurement: the keys the stage had not reached are absent rather than zero.

Returns:

  • (StandardError, nil)


91
# File 'lib/active_sanction/instrumentation/event.rb', line 91

def error = payload[:error]

#failed? ⇒ Boolean

Returns:

  • (Boolean)


94
# File 'lib/active_sanction/instrumentation/event.rb', line 94

def failed? = !error.nil?

#finished_at ⇒ Time

The wall clock when the stage finished. Derived from started_at and the monotonic duration rather than read again, so the two cannot disagree about how long this took.

Returns:

  • (Time)


100
# File 'lib/active_sanction/instrumentation/event.rb', line 100

def finished_at = started_at + duration

#inspect ⇒ String

Returns:

  • (String)


119
# File 'lib/active_sanction/instrumentation/event.rb', line 119

def inspect = "#<#{self.class} #{name} #{payload.inspect} #{format("%.3f", duration)}s>"

#source ⇒ Symbol?

Which list this was about, or nil for an event that is not about one -- screen, which is about a query, and sync, which is about a run.

Returns:

  • (Symbol, nil)


85
# File 'lib/active_sanction/instrumentation/event.rb', line 85

def source = payload[:source]

#to_h ⇒ Hash{Symbol => T.untyped}

The whole event as one Hash, for a subscriber that forwards it somewhere structured. The payload is spread in at the top level, and its keys win over nothing -- name, started_at and duration are not payload keys on any event this library emits.

Returns:

  • (Hash{Symbol => T.untyped})


111
112
113
# File 'lib/active_sanction/instrumentation/event.rb', line 111

def to_h
  { name: name, started_at: started_at, duration: duration }.merge(payload)
end

#to_s ⇒ String

Returns:

  • (String)


116
# File 'lib/active_sanction/instrumentation/event.rb', line 116

def to_s = "#{name} #{format("%.3f", duration)}s#{" #{source}" if source}#{" failed" if failed?}"