observability 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+