observability 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|