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