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,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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|