observability 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.document +5 -0
- data/.rdoc_options +16 -0
- data/.simplecov +9 -0
- data/ChangeLog +139 -0
- data/DevNotes.md +103 -0
- data/History.md +4 -0
- data/LICENSE.txt +20 -0
- data/Manifest.txt +31 -0
- data/README.md +93 -0
- data/Rakefile +102 -0
- data/bin/observability-collector +16 -0
- data/examples/basic-usage.rb +18 -0
- data/lib/observability.rb +122 -0
- data/lib/observability/collector.rb +61 -0
- data/lib/observability/collector/timescale.rb +140 -0
- data/lib/observability/event.rb +103 -0
- data/lib/observability/observer.rb +296 -0
- data/lib/observability/observer_hooks.rb +28 -0
- data/lib/observability/sender.rb +127 -0
- data/lib/observability/sender/logger.rb +37 -0
- data/lib/observability/sender/null.rb +30 -0
- data/lib/observability/sender/testing.rb +56 -0
- data/lib/observability/sender/udp.rb +88 -0
- data/spec/observability/event_spec.rb +106 -0
- data/spec/observability/observer_hooks_spec.rb +47 -0
- data/spec/observability/observer_spec.rb +292 -0
- data/spec/observability/sender/logger_spec.rb +28 -0
- data/spec/observability/sender/udp_spec.rb +86 -0
- data/spec/observability/sender_spec.rb +77 -0
- data/spec/observability_spec.rb +132 -0
- data/spec/spec_helper.rb +155 -0
- metadata +325 -0
- metadata.gz.sig +1 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'observability/sender' unless defined?( Observability::Sender )
|
5
|
+
|
6
|
+
|
7
|
+
# A sender that just logs events to the Observability logger.
|
8
|
+
class Observability::Sender::Logger < Observability::Sender
|
9
|
+
extend Loggability
|
10
|
+
|
11
|
+
|
12
|
+
# Loggability API
|
13
|
+
log_as :observability_events
|
14
|
+
|
15
|
+
|
16
|
+
def start # :nodoc:
|
17
|
+
# No-op
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def stop # :nodoc:
|
22
|
+
# No-op
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
### Output the +event+ to the logger.
|
27
|
+
def enqueue( *events )
|
28
|
+
events.each do |event|
|
29
|
+
data = event.resolve
|
30
|
+
msg = "«%s» %p" % [ data[:@type], data ]
|
31
|
+
self.log.debug( msg )
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end # class Observability::Sender::Log
|
36
|
+
|
37
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'observability/sender' unless defined?( Observability::Sender )
|
5
|
+
|
6
|
+
|
7
|
+
# A Sender that no-ops all observation routines. This effectively disables
|
8
|
+
# Observability.
|
9
|
+
class Observability::Sender::Null < Observability::Sender
|
10
|
+
|
11
|
+
### Overridden: Nothing to start.
|
12
|
+
def start
|
13
|
+
# No-op
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
### Overridden: Nothing to stop.
|
18
|
+
def stop
|
19
|
+
# No-op
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
### Overridden: Drop enqueued events.
|
24
|
+
def enqueue( * )
|
25
|
+
# No-op
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Observability::Sender::Null
|
29
|
+
|
30
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'loggability'
|
5
|
+
|
6
|
+
require 'observability/sender' unless defined?( Observability::Sender )
|
7
|
+
|
8
|
+
|
9
|
+
# A sender that just enqueues events and then lets you make assertions about the
|
10
|
+
# kinds of events that were sent.
|
11
|
+
class Observability::Sender::Testing < Observability::Sender
|
12
|
+
extend Loggability
|
13
|
+
|
14
|
+
|
15
|
+
# Loggability API
|
16
|
+
log_to :observability
|
17
|
+
|
18
|
+
|
19
|
+
### Create a new testing sender.
|
20
|
+
def initialize( * )
|
21
|
+
@enqueued_events = []
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
# The Array of events which were queued.
|
27
|
+
attr_reader :enqueued_events
|
28
|
+
|
29
|
+
|
30
|
+
# No-ops; there is no sending thread, so nothing to start/stop.
|
31
|
+
def start( * ); end
|
32
|
+
def stop( * ); end
|
33
|
+
|
34
|
+
|
35
|
+
### Sender API -- add the specified +events+ to the queue.
|
36
|
+
def enqueue( *events )
|
37
|
+
@enqueued_events.concat( events )
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
### Return any enqueued events that are of the specified +type+.
|
42
|
+
def find_events( type )
|
43
|
+
return @enqueued_events.find_all do |event|
|
44
|
+
event.type == type
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
### Returns +true+ if at least one event of the specified +type+ was enqueued.
|
50
|
+
def event_was_sent?( type )
|
51
|
+
return !self.find_events( type ).empty?
|
52
|
+
end
|
53
|
+
|
54
|
+
end # class Observability::Sender::Testing
|
55
|
+
|
56
|
+
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'socket'
|
5
|
+
require 'configurability'
|
6
|
+
|
7
|
+
require 'observability/sender' unless defined?( Observability::Sender )
|
8
|
+
|
9
|
+
|
10
|
+
# A sender that sends events as JSON over UDP.
|
11
|
+
class Observability::Sender::UDP < Observability::Sender
|
12
|
+
extend Configurability
|
13
|
+
|
14
|
+
|
15
|
+
# Number of seconds to wait between retrying a blocked write
|
16
|
+
RETRY_INTERVAL = 0.25
|
17
|
+
|
18
|
+
# The pipeline to use for turning events into network data
|
19
|
+
SERIALIZE_PIPELINE = :resolve.to_proc >> JSON.method(:generate)
|
20
|
+
|
21
|
+
|
22
|
+
# Declare configurable settings
|
23
|
+
configurability( 'observability.sender.udp' ) do
|
24
|
+
|
25
|
+
##
|
26
|
+
# The host to send events to
|
27
|
+
setting :host, default: 'localhost'
|
28
|
+
|
29
|
+
##
|
30
|
+
# The port to send events to
|
31
|
+
setting :port, default: 15775
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
### Create a new UDP sender
|
37
|
+
def initialize( * )
|
38
|
+
@socket = UDPSocket.new
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
######
|
43
|
+
public
|
44
|
+
######
|
45
|
+
|
46
|
+
##
|
47
|
+
# The socket to send events over
|
48
|
+
attr_reader :socket
|
49
|
+
|
50
|
+
|
51
|
+
### Start sending queued events.
|
52
|
+
def start
|
53
|
+
self.socket.connect( self.class.host, self.class.port )
|
54
|
+
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
### Stop the sender's executor.
|
60
|
+
def stop
|
61
|
+
super
|
62
|
+
|
63
|
+
self.socket.shutdown( :WR )
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
### Serialize each the given +events+ and return the results.
|
68
|
+
def serialize_events( events )
|
69
|
+
return events.map( &SERIALIZE_PIPELINE )
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
### Send the specified +event+.
|
74
|
+
def send_event( data )
|
75
|
+
until data.empty?
|
76
|
+
bytes = self.socket.sendmsg_nonblock( data, 0, exception: false )
|
77
|
+
|
78
|
+
if bytes == :wait_writable
|
79
|
+
IO.select( nil, [self.socket], nil )
|
80
|
+
else
|
81
|
+
self.log.debug "Sent: %p" % [ data[0, bytes] ]
|
82
|
+
data[ 0, bytes ] = ''
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end # class Observability::Sender::UDP
|
88
|
+
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'timecop'
|
6
|
+
require 'observability/event'
|
7
|
+
|
8
|
+
|
9
|
+
describe Observability::Event do
|
10
|
+
|
11
|
+
before( :all ) do
|
12
|
+
@real_tz = ENV['TZ']
|
13
|
+
ENV['TZ'] = 'America/Los_Angeles'
|
14
|
+
end
|
15
|
+
|
16
|
+
after( :all ) do
|
17
|
+
ENV['TZ'] = @real_tz
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
it "can be created with just a type" do
|
22
|
+
event = described_class.new( 'acme.daemon.start' )
|
23
|
+
|
24
|
+
expect( event ).to be_a( described_class )
|
25
|
+
expect( event.type ).to eq( 'acme.daemon.start' )
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
it "can be created with a type and one or more fields" do
|
30
|
+
event = described_class.new( 'acme.user.review', score: 100, time: 48 )
|
31
|
+
|
32
|
+
expect( event ).to be_a( described_class )
|
33
|
+
expect( event.type ).to eq( 'acme.user.review' )
|
34
|
+
expect( event.fields ).to eq( score: 100, time: 48 )
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
it "returns a structured log entry when resolved" do
|
39
|
+
event = described_class.new( 'acme.user.review', score: 100, time: 48 )
|
40
|
+
expect( event.resolve ).to be_a( Hash ).and( include(score: 100, time: 48) )
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
it "prevents adding more fields after it's been resolved" do
|
45
|
+
event = described_class.new( 'acme.user.review', score: 100, time: 48 )
|
46
|
+
|
47
|
+
event.resolve
|
48
|
+
|
49
|
+
expect {
|
50
|
+
event.merge( foo: 1 )
|
51
|
+
}.to raise_error( /already resolved/i )
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
it "adds its type to the resolved fields" do
|
56
|
+
event = described_class.new( 'acme.user.review' )
|
57
|
+
expect( event.resolve ).to include( :@type => event.type )
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
it "adds a timestamp to the resolved fields" do
|
62
|
+
Timecop.freeze( Time.at(1563821278.609382) ) do
|
63
|
+
expect( Time.now.to_f ).to eq( 1563821278.609382 )
|
64
|
+
event = described_class.new( 'acme.user.review' )
|
65
|
+
expect( event.resolve ).to include(
|
66
|
+
:@timestamp => a_string_matching( /2019-07-22T11:47:58\.\d{6}-07:00/ )
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
it "has a monotonic timestamp of when it was created" do
|
73
|
+
event = described_class.new( 'acme.daemon.start' )
|
74
|
+
|
75
|
+
expect( event.start ).to be_a( Float ).and( be < Concurrent.monotonic_time )
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
it "freezes its type" do
|
80
|
+
event = described_class.new( 'acme.daemon.start' )
|
81
|
+
|
82
|
+
expect( event.type ).to be_frozen
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
it "can merge new field data" do
|
87
|
+
event = described_class.new( 'acme.daemon.start', time: 1563379346 )
|
88
|
+
|
89
|
+
expect {
|
90
|
+
event.merge( end_time: 1563379417 )
|
91
|
+
}.to change { event.fields }.to( time: 1563379346, end_time: 1563379417 )
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
it "calls fields which are callables when it is resolved" do
|
96
|
+
event = described_class.new( 'acme.user.speed',
|
97
|
+
start: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
98
|
+
duration: ->(ev){ Process.clock_gettime(Process::CLOCK_MONOTONIC) - ev[:start] } )
|
99
|
+
|
100
|
+
sleep 0.25
|
101
|
+
|
102
|
+
expect( event.resolve[:duration] ).to be_within( 0.01 ).of( 0.25 )
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'observability/observer_hooks'
|
6
|
+
|
7
|
+
|
8
|
+
describe Observability::ObserverHooks do
|
9
|
+
|
10
|
+
let( :hooks_mod ) { described_class.dup }
|
11
|
+
|
12
|
+
let( :observed_class ) do
|
13
|
+
new_class = Class.new do
|
14
|
+
def do_a_thing
|
15
|
+
self.observe( :thing_done ) do
|
16
|
+
self.do_it
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def do_it
|
21
|
+
return :it_is_done
|
22
|
+
end
|
23
|
+
end
|
24
|
+
new_class.prepend( hooks_mod )
|
25
|
+
|
26
|
+
return new_class
|
27
|
+
end
|
28
|
+
let ( :expected_event_prefix ) { "anonymous_class_%d" % [ observed_class.object_id ] }
|
29
|
+
|
30
|
+
|
31
|
+
it "provides an #observe instance method for generating events" do
|
32
|
+
instance = observed_class.new
|
33
|
+
|
34
|
+
expect {
|
35
|
+
instance.do_a_thing
|
36
|
+
}.to emit_event( "#{expected_event_prefix}.do_a_thing.thing_done" )
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
it "provides an instance method alias to the current Observability observer" do
|
41
|
+
instance = observed_class.new
|
42
|
+
|
43
|
+
expect( instance.observability ).to be( Observability.observer )
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,292 @@
|
|
1
|
+
#!/usr/bin/env rspec -cfd
|
2
|
+
|
3
|
+
require_relative '../spec_helper'
|
4
|
+
|
5
|
+
require 'observability/observer'
|
6
|
+
|
7
|
+
|
8
|
+
# A module and class for testing derived event types
|
9
|
+
module FooLibrary
|
10
|
+
module Bar
|
11
|
+
class BazAdapter
|
12
|
+
def start; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
describe Observability::Observer do
|
19
|
+
|
20
|
+
it "can be created with a default sender" do
|
21
|
+
observer = described_class.new
|
22
|
+
|
23
|
+
expect( observer ).to be_a( described_class )
|
24
|
+
expect( observer.sender ).to be_a( Observability::Sender )
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
it "can create a new event" do
|
29
|
+
marker = nil
|
30
|
+
observer = described_class.new
|
31
|
+
|
32
|
+
expect {
|
33
|
+
marker = observer.event( 'acme.daemon.start' )
|
34
|
+
}.to change { observer.pending_event_count }.by( 1 )
|
35
|
+
|
36
|
+
expect( marker ).to be_an( Integer )
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
it "sends an event when it is finished" do
|
41
|
+
observer = described_class.new( :testing )
|
42
|
+
observer.event( 'acme.engine.throttle' )
|
43
|
+
|
44
|
+
expect {
|
45
|
+
observer.finish
|
46
|
+
}.to change { observer.sender.enqueued_events.length }.by( 1 )
|
47
|
+
|
48
|
+
event = observer.sender.enqueued_events.last
|
49
|
+
expect( event.type ).to eq( 'acme.engine.throttle' )
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
it "sends an event when it is finished with the correct marker" do
|
54
|
+
observer = described_class.new( :testing )
|
55
|
+
marker = observer.event( 'acme.startup' )
|
56
|
+
|
57
|
+
expect {
|
58
|
+
observer.finish( marker )
|
59
|
+
}.to change { observer.sender.enqueued_events.length }.by( 1 )
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# :TODO: Should this behavior instead finish all events up to the specified marker instead?
|
64
|
+
it "raises when finished with the incorrect marker" do
|
65
|
+
observer = described_class.new( :testing )
|
66
|
+
first_marker = observer.event( 'acme.startup' )
|
67
|
+
second_marker = observer.event( 'acme.loop' )
|
68
|
+
|
69
|
+
expect {
|
70
|
+
observer.finish( first_marker )
|
71
|
+
}.to raise_error( /event mismatch/i )
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
it "creates and event and finishes it immediately when passed a block" do
|
76
|
+
observer = described_class.new( :testing )
|
77
|
+
|
78
|
+
expect {
|
79
|
+
observer.event( 'acme.gate' ) do |obs|
|
80
|
+
obs.add( state: 'open' )
|
81
|
+
end
|
82
|
+
}.to change { observer.sender.enqueued_events.length }.by( 1 )
|
83
|
+
|
84
|
+
event = observer.sender.enqueued_events.last
|
85
|
+
expect( event.type ).to eq( 'acme.gate' )
|
86
|
+
expect( event.fields ).to eq( state: 'open' )
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
describe "event types" do
|
91
|
+
|
92
|
+
it "can be set directly by passing a String" do
|
93
|
+
observer = described_class.new( :testing )
|
94
|
+
observer.event( 'acme.factory.deploy' )
|
95
|
+
|
96
|
+
event = observer.finish
|
97
|
+
|
98
|
+
expect( event.type ).to eq( 'acme.factory.deploy' )
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
it "can be derived from a named Module" do
|
103
|
+
observer = described_class.new( :testing )
|
104
|
+
observer.event( FooLibrary::Bar )
|
105
|
+
|
106
|
+
event = observer.finish
|
107
|
+
|
108
|
+
expect( event.type ).to eq( 'foo_library.bar' )
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
it "can be derived from an anonymous Module" do
|
113
|
+
mod = Module.new
|
114
|
+
observer = described_class.new( :testing )
|
115
|
+
observer.event( mod )
|
116
|
+
|
117
|
+
event = observer.finish
|
118
|
+
|
119
|
+
expect( event.type ).to eq( "anonymous_module_#{mod.object_id}" )
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
it "can be derived from a named Class" do
|
124
|
+
observer = described_class.new( :testing )
|
125
|
+
observer.event( FooLibrary::Bar::BazAdapter )
|
126
|
+
|
127
|
+
event = observer.finish
|
128
|
+
|
129
|
+
expect( event.type ).to eq( "foo_library.bar.baz_adapter" )
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
it "can be derived from an anonymous Class" do
|
134
|
+
mod = Class.new
|
135
|
+
observer = described_class.new( :testing )
|
136
|
+
observer.event( mod )
|
137
|
+
|
138
|
+
event = observer.finish
|
139
|
+
|
140
|
+
expect( event.type ).to eq( "anonymous_class_#{mod.object_id}" )
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
it "can be derived from an UnboundMethod" do
|
145
|
+
observer = described_class.new( :testing )
|
146
|
+
observer.event( FooLibrary::Bar::BazAdapter.instance_method(:start) )
|
147
|
+
|
148
|
+
event = observer.finish
|
149
|
+
|
150
|
+
expect( event.type ).to eq( "foo_library.bar.baz_adapter.start" )
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
it "can be derived from a bound Method" do
|
155
|
+
observer = described_class.new( :testing )
|
156
|
+
observer.event( FooLibrary::Bar::BazAdapter.new.method(:start) )
|
157
|
+
|
158
|
+
event = observer.finish
|
159
|
+
|
160
|
+
expect( event.type ).to eq( "foo_library.bar.baz_adapter.start" )
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
it "can be derived from a Method and a Symbol" do
|
165
|
+
obj = FooLibrary::Bar::BazAdapter.new
|
166
|
+
meth = obj.method( :start )
|
167
|
+
|
168
|
+
observer = described_class.new( :testing )
|
169
|
+
observer.event( [meth, :loop] )
|
170
|
+
|
171
|
+
event = observer.finish
|
172
|
+
|
173
|
+
expect( event.type ).to eq( "foo_library.bar.baz_adapter.start.loop" )
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
it "can be derived from a Proc and a Symbol" do
|
178
|
+
obj = FooLibrary::Bar::BazAdapter.new
|
179
|
+
meth = obj.method( :start )
|
180
|
+
|
181
|
+
observer = described_class.new( :testing )
|
182
|
+
observer.event( [meth, :loop] )
|
183
|
+
|
184
|
+
event = observer.finish
|
185
|
+
|
186
|
+
expect( event.type ).to eq( "foo_library.bar.baz_adapter.start.loop" )
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
describe "event options" do
|
193
|
+
|
194
|
+
it "allows additions to be made to new events directly" do
|
195
|
+
observer = described_class.new( :testing )
|
196
|
+
observer.event( 'acme.engine.throttle', add: { factor: 7 } )
|
197
|
+
event = observer.finish
|
198
|
+
|
199
|
+
expect( event.resolve ).to include( factor: 7 )
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
describe ":model" do
|
204
|
+
|
205
|
+
it "adds fields germane to processes for the process model"
|
206
|
+
it "adds fields germane to threads for the thread model"
|
207
|
+
it "adds fields germane to loops for the loop model"
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
describe ":timed" do
|
213
|
+
|
214
|
+
it "adds a calculated duration field" do
|
215
|
+
observer = described_class.new( :testing )
|
216
|
+
observer.event( 'acme.engine.run', timed: true )
|
217
|
+
sleep 0.1
|
218
|
+
event = observer.finish
|
219
|
+
|
220
|
+
expect( event.resolve ).to include( duration: a_value > 0.1 )
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
describe "derived fields" do
|
229
|
+
|
230
|
+
it "can be added via an Exception" do
|
231
|
+
observer = described_class.new( :testing )
|
232
|
+
observer.event( 'acme.engine.start' )
|
233
|
+
|
234
|
+
expect {
|
235
|
+
observer.finish_after_block do
|
236
|
+
raise "misfire!"
|
237
|
+
end
|
238
|
+
}.to raise_error( RuntimeError, 'misfire!' )
|
239
|
+
|
240
|
+
event = observer.sender.enqueued_events.last
|
241
|
+
expect( event.type ).to eq( 'acme.engine.start' )
|
242
|
+
|
243
|
+
expect( event.fields ).to include(
|
244
|
+
error: a_hash_including(
|
245
|
+
type: 'RuntimeError',
|
246
|
+
message: 'misfire!',
|
247
|
+
backtrace: an_instance_of( Array )
|
248
|
+
)
|
249
|
+
)
|
250
|
+
expect( event.resolve[:error][:backtrace] ).
|
251
|
+
to all( be_a(Hash).and( include(:label, :path, :lineno) ) )
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
it "can be added for any object that responds to #to_h" do
|
256
|
+
observer = described_class.new( :testing )
|
257
|
+
observer.event( 'acme.engine.start' )
|
258
|
+
|
259
|
+
to_h_class = Class.new do
|
260
|
+
def to_h
|
261
|
+
return { sku: '121212', rev: '8c' }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
observer.add( to_h_class.new )
|
266
|
+
event = observer.finish
|
267
|
+
|
268
|
+
expect( event.resolve ).to include( sku: '121212', rev: '8c' )
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
|
274
|
+
|
275
|
+
describe "context" do
|
276
|
+
|
277
|
+
it "can be added for all inner events" do
|
278
|
+
observer = described_class.new( :testing )
|
279
|
+
event = observer.event( 'acme.engine.start' ) do
|
280
|
+
observer.add_context( request_id: 'E30E90E0-585B-4015-9C96-AE6EC487970C' )
|
281
|
+
|
282
|
+
inner_event = observer.event( 'acme.engine.rev' ) {}
|
283
|
+
end
|
284
|
+
|
285
|
+
expect( observer.sender.enqueued_events.map(&:resolve) ).
|
286
|
+
to all( include( request_id: 'E30E90E0-585B-4015-9C96-AE6EC487970C' ) )
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|