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,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
|
+
|