observability 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'observability/sender/logger'
6
+ require 'observability/event'
7
+
8
+
9
+ describe Observability::Sender::Logger do
10
+
11
+ it "sends events to its own logger" do
12
+ sender = described_class.new
13
+ sender.start
14
+
15
+ event = Observability::Event.new( 'acme.engine.start' )
16
+
17
+ log = []
18
+ Loggability.outputting_to( log ).formatted_with( :default ).with_level( :debug ) do
19
+ sender.enqueue( event )
20
+ end
21
+ sender.stop
22
+
23
+ # Race to find the log
24
+ expect( log ).to include( a_string_matching(/acme\.engine\.start/) )
25
+ end
26
+
27
+ end
28
+
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+ require 'observability/sender/udp'
6
+
7
+
8
+ describe Observability::Sender::UDP do
9
+
10
+ before( :all ) do
11
+ described_class.configure( host: 'localhost', port: 8787 )
12
+ end
13
+
14
+ after( :all ) do
15
+ described_class.configure
16
+ end
17
+
18
+
19
+ let( :udp_socket ) { instance_double(UDPSocket) }
20
+ let( :executor ) do
21
+ instance_double( Concurrent::SingleThreadExecutor, :auto_terminate= => nil )
22
+ end
23
+
24
+ before( :each ) do
25
+ allow( UDPSocket ).to receive( :new ).and_return( udp_socket )
26
+ allow( Concurrent::SingleThreadExecutor ).to receive( :new ).and_return( executor )
27
+ end
28
+
29
+
30
+ it "sends events via a UDP socket" do
31
+ event = Observability::Event.new( 'acme.engine.startup' )
32
+
33
+ expect( executor ).to receive( :post ) do |*args, &block|
34
+ block.call( *args )
35
+ end
36
+ expect( udp_socket ).to receive( :connect ).with( 'localhost', 8787 )
37
+ expect( udp_socket ).to receive( :sendmsg_nonblock ) do |msg, flags, **opts|
38
+ expect( msg ).to match( /\{.*"@type":"acme\.engine\.startup".*\}/ )
39
+ expect( flags ).to eq( 0 )
40
+ expect( opts ).to include( exception: false )
41
+ msg.bytesize
42
+ end
43
+
44
+ sender = described_class.new
45
+ sender.start
46
+ sender.enqueue( event )
47
+ end
48
+
49
+
50
+ it "retries if writing would block" do
51
+ event = Observability::Event.new( 'acme.engine.startup' )
52
+ blocked_once = false
53
+
54
+ expect( executor ).to receive( :post ) do |*args, &block|
55
+ block.call( *args )
56
+ end
57
+ expect( udp_socket ).to receive( :connect ).with( 'localhost', 8787 )
58
+ expect( udp_socket ).to receive( :sendmsg_nonblock ) do |msg, flags, **opts|
59
+ if !blocked_once
60
+ blocked_once = true
61
+ :wait_writable
62
+ elsif msg.bytesize > 5
63
+ 5
64
+ else
65
+ msg.bytesize
66
+ end
67
+ end.at_least( 2 ).times
68
+ expect( IO ).to receive( :select ).with( nil, [udp_socket], nil ).
69
+ and_return( nil, [udp_socket], nil ).
70
+ once
71
+
72
+ sender = described_class.new
73
+ sender.start
74
+ sender.enqueue( event )
75
+ end
76
+
77
+
78
+ it "shuts down the socket when it stops" do
79
+ expect( udp_socket ).to receive( :shutdown ).with( :WR )
80
+
81
+ sender = described_class.new
82
+ sender.stop
83
+ end
84
+
85
+ end
86
+
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'observability/sender'
6
+ require 'observability/sender/testing'
7
+ require 'observability/event'
8
+
9
+
10
+ describe Observability::Sender do
11
+
12
+ after( :all ) do
13
+ described_class.configure
14
+ end
15
+
16
+ before( :each ) do
17
+ described_class.configure
18
+ end
19
+
20
+
21
+ it "is an abstract class" do
22
+ expect {
23
+ described_class.new
24
+ }.to raise_error( NoMethodError, /private method `new'/i )
25
+ end
26
+
27
+
28
+ it "can create an instance of the configured type of sender" do
29
+ described_class.configure( type: :testing )
30
+ expect( described_class.configured_type ).to be_a( Observability::Sender::Testing )
31
+ end
32
+
33
+
34
+ context "concrete subclasses" do
35
+
36
+ let( :subclass ) do
37
+ Class.new( described_class ) do
38
+ def initialize( * )
39
+ super
40
+ @sent = []
41
+ end
42
+ attr_reader :sent
43
+ def send_event( ev )
44
+ @sent << ev
45
+ end
46
+ end
47
+ end
48
+
49
+ let( :instance ) do
50
+ subclass.new
51
+ end
52
+
53
+
54
+ it "sends events after it's started" do
55
+ events = 12.times.map { Observability::Event.new('acme.windowwasher.refill') }
56
+
57
+ instance.start
58
+ instance.enqueue( *events ).wait( 0.5 )
59
+ instance.stop
60
+
61
+ expect( instance.sent ).to contain_exactly( *(events.map(&:resolve)) )
62
+ end
63
+
64
+
65
+ it "drops events if it hasn't been started yet" do
66
+ events = 8.times.map { Observability::Event.new('acme.windowwasher.refill') }
67
+
68
+ instance.enqueue( *events ).wait( 0.5 )
69
+
70
+ expect( instance.sent ).to be_empty
71
+ end
72
+
73
+
74
+ end
75
+
76
+ end
77
+
@@ -0,0 +1,132 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'spec_helper'
5
+
6
+ require 'rspec'
7
+ require 'observability'
8
+ require 'observability/observer'
9
+
10
+
11
+ describe Observability do
12
+
13
+ before( :all ) do
14
+ @real_hook_mods = Observability.observer_hooks
15
+ Observability.instance_variable_set( :@observer_hooks, Concurrent::Map.new )
16
+ end
17
+
18
+ before( :each ) do
19
+ Observability.reset
20
+ Observability.observer_hooks.keys.each {|key| Observability.observer_hooks.delete(key) }
21
+ end
22
+
23
+ after( :all ) do
24
+ Observability.instance_variable_set( :@observer_hooks, @real_hook_mods )
25
+ end
26
+
27
+
28
+ it "can create a singleton observer" do
29
+ result = described_class.observer
30
+ expect( result ).to be_a( Observability::Observer )
31
+ end
32
+
33
+
34
+ it "doesn't race to create the singleton observer" do
35
+ val1 = Thread.new { described_class.observer }.value
36
+ val2 = Thread.new { described_class.observer }.value
37
+
38
+ expect( val1 ).to be( val2 )
39
+ end
40
+
41
+
42
+ it "tracks the hook modules of all extended modules" do
43
+ new_mod = Module.new
44
+ new_mod.extend( described_class )
45
+ expect( described_class.observer_hooks.keys ).to include( new_mod )
46
+ expect( described_class.observer_hooks[new_mod] ).to be_a( Module )
47
+ end
48
+
49
+
50
+ it "tracks the hook modules of the singleton classes of all extended modules" do
51
+ new_mod = Module.new
52
+ new_mod.extend( described_class )
53
+ s_class = new_mod.singleton_class
54
+ expect( described_class.observer_hooks.keys ).to include( s_class )
55
+ expect( described_class.observer_hooks[s_class] ).to be_a( Module )
56
+ end
57
+
58
+
59
+ it "provides a way to look up the hooks module for an extended module" do
60
+ new_mod = Module.new
61
+ new_mod.extend( described_class )
62
+
63
+ expect( described_class[new_mod] ).to be( described_class.observer_hooks[new_mod] )
64
+ end
65
+
66
+
67
+ it "provides a way to look up the hooks module via an instance of an extended class" do
68
+ new_class = Class.new
69
+ new_class.extend( described_class )
70
+
71
+ instance = new_class.new
72
+
73
+ expect( described_class[instance] ).to be( described_class.observer_hooks[new_class] )
74
+ end
75
+
76
+
77
+ describe "an including class" do
78
+
79
+ let( :observed_class ) do
80
+ the_class = Class.new do
81
+
82
+ def self::name
83
+ return "TestClass"
84
+ end
85
+
86
+ @class_things_done = 0
87
+ def self::do_a_class_thing
88
+ @class_things_done += 1
89
+ end
90
+
91
+ def initialize
92
+ @things_done = 0
93
+ end
94
+
95
+ attr_accessor :things_done
96
+
97
+ def do_a_thing
98
+ self.things_done += 1
99
+ end
100
+ end
101
+
102
+ the_class.extend( described_class )
103
+ return the_class
104
+ end
105
+
106
+
107
+ it "can decorate instance methods with observation" do
108
+ observed_class.observe_method( :do_a_thing )
109
+ object = observed_class.new
110
+
111
+ expect {
112
+ object.do_a_thing
113
+ }.to emit_event( "test_class.do_a_thing" )
114
+ end
115
+
116
+
117
+ it "can decorate class methods with observation" do
118
+ observed_class.observe_class_method( :do_a_class_thing )
119
+
120
+ # :FIXME: I want this to be the ID of the observed class instead, but
121
+ # I haven't figured out how to do this yet.
122
+ id = observed_class.singleton_class.object_id
123
+
124
+ expect {
125
+ observed_class.do_a_class_thing
126
+ }.to emit_event( "anonymous_class_#{id}.do_a_class_thing" )
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+
@@ -0,0 +1,155 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'simplecov' if ENV['COVERAGE']
5
+
6
+ require 'rspec'
7
+
8
+ require 'loggability/spechelpers'
9
+ require 'observability'
10
+
11
+
12
+ module Observability::SpecHelpers
13
+
14
+ ### Replace the real observer with a new one with a sender of the given
15
+ ### +sender_type+, yield to the block, then restore the real observer.
16
+ def self::replace_observer( sender_type )
17
+ real_ivar = Observability.instance_variable_get( :@observer )
18
+
19
+ new_sender = Observability::Sender.get_subclass( sender_type )
20
+ new_observer = Observability::Observer.new( new_sender )
21
+ new_ivar = Concurrent::IVar.new( new_observer )
22
+
23
+ begin
24
+ Observability.instance_variable_set( :@observer, new_ivar )
25
+ yield( new_observer )
26
+ ensure
27
+ Observability.instance_variable_set( :@observer, real_ivar )
28
+ end
29
+ end
30
+
31
+
32
+ # Expectation class to match emitted events.
33
+ class EventEmittedExpectation
34
+ include RSpec::Matchers::Composable
35
+
36
+ extend Loggability
37
+ log_to :observability
38
+
39
+
40
+ ### Create a new expectation that an event will be emitted that matches the
41
+ ### given +criteria+.
42
+ def initialize( expected_type )
43
+ @expected_type = expected_type
44
+ @prelude_error = nil
45
+ @sender = nil
46
+ end
47
+
48
+
49
+ ### RSpec matcher API -- specify that this matcher supports expect with a block.
50
+ def supports_block_expectations?
51
+ return true
52
+ end
53
+
54
+
55
+ ### Return +true+ if the +given_proc+ is a valid callable.
56
+ def valid_proc?( given_proc )
57
+ return true if given_proc.respond_to?( :call )
58
+
59
+ warn "`emit_event` was called with non-proc object #{given_proc.inspect}"
60
+ return false
61
+ end
62
+
63
+
64
+ ### Returns +true+ if the +given_proc+ results in an observation that matches
65
+ ### the built-up criteria to be sent.
66
+ def matches?( given_proc )
67
+ return false unless self.valid_proc?( given_proc )
68
+ return self.run_with_test_sender( given_proc )
69
+ end
70
+
71
+
72
+ ### RSpec negated matcher API -- return +true+ if an observation is not built when
73
+ ### the +given_proc+ is called.
74
+ def does_not_match?( given_proc )
75
+ return false unless self.valid_proc?( given_proc )
76
+ self.run_with_test_sender( given_proc )
77
+
78
+ return !( @prelude_error || self.job_ran_normally? )
79
+ end
80
+
81
+
82
+ ### Run the +given_proc+ with async publication emulation set up.
83
+ def run_with_test_sender( given_proc )
84
+ Observability::SpecHelpers.replace_observer( :testing ) do |observer|
85
+ @sender = observer.sender
86
+ begin
87
+ given_proc.call
88
+ rescue => err
89
+ @prelude_error = err
90
+ end
91
+ end
92
+
93
+ return self.job_ran_normally?
94
+ end
95
+
96
+
97
+ ### Returns +true+ if the job ran and succeeded.
98
+ def job_ran_normally?
99
+ return @sender.event_was_sent?( @expected_type )
100
+ end
101
+
102
+
103
+ ### Return a failure message based on the current state of the matcher.
104
+ def failure_message
105
+ return self.describe_prelude_error if @prelude_error
106
+ return "no events were emitted" if @sender.enqueued_events.empty?
107
+ return "no %s events were emitted; emitted events were:\n %s" %
108
+ [ @expected_type, @sender.enqueued_events.map(&:type).join("\n ") ]
109
+ end
110
+ alias_method :failure_message_for_should, :failure_message
111
+
112
+
113
+ ### Return a failure message based on the current state of the matcher when it was
114
+ ### passed to a `to_not`.
115
+ def failure_message_when_negated
116
+ return self.describe_prelude_error if @prelude_error
117
+ return "expected not to emit a %s event, but one was sent" % [ @expected_type ]
118
+ end
119
+ alias_method :failure_message_for_should_not, :failure_message_when_negated
120
+
121
+
122
+ ### Return a String describing an error which happened in the spec's
123
+ ### block before the event started.
124
+ def describe_prelude_error
125
+ return "%p before the event was emitted: %s" % [
126
+ @prelude_error.class,
127
+ @prelude_error.full_message( highlight: $stdout.tty?, order: :bottom )
128
+ ]
129
+ end
130
+
131
+ end # class EventEmittedExpectation
132
+
133
+
134
+ ### Expect an event matching the given +criteria+ to be emitted.
135
+ def emit_event( *criteria )
136
+ return Observability::SpecHelpers::EventEmittedExpectation.new( *criteria )
137
+ end
138
+
139
+ end # module Observability::SpecHelpers
140
+
141
+
142
+ ### Mock with RSpec
143
+ RSpec.configure do |config|
144
+ config.run_all_when_everything_filtered = true
145
+ config.filter_run :focus
146
+ config.order = 'random'
147
+ config.mock_with( :rspec ) do |mock|
148
+ mock.syntax = :expect
149
+ end
150
+
151
+ config.include( Loggability::SpecHelpers )
152
+ config.include( Observability::SpecHelpers )
153
+ end
154
+
155
+