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.
@@ -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
+