observability 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'time'
5
+ require 'forwardable'
6
+ require 'loggability'
7
+ require 'concurrent'
8
+
9
+ require 'observability' unless defined?( Observability )
10
+
11
+
12
+ class Observability::Event
13
+ extend Loggability,
14
+ Forwardable
15
+
16
+
17
+ # The event format version to send with all events
18
+ FORMAT_VERSION = 1
19
+
20
+
21
+ # Loggability API -- send logs to the top-level module's logger
22
+ log_to :observability
23
+
24
+
25
+ ### Create a new event
26
+ def initialize( type, **fields )
27
+ @type = type.freeze
28
+ @timestamp = Time.now
29
+ @start = Concurrent.monotonic_time
30
+ @fields = fields
31
+ end
32
+
33
+
34
+ ######
35
+ public
36
+ ######
37
+
38
+ ##
39
+ # The type of the event, which should be a string of the form: 'foo.bar.baz'
40
+ attr_reader :type
41
+
42
+ ##
43
+ # The Time the event was created
44
+ attr_reader :timestamp
45
+
46
+ ##
47
+ # The monotonic clock time of when the event was created
48
+ attr_reader :start
49
+
50
+ ##
51
+ # A Symbol-keyed Hash of values that make up the event data
52
+ attr_reader :fields
53
+
54
+
55
+ ##
56
+ # Delegate some read-only methods to the #fields Hash.
57
+ def_instance_delegators :fields, :[], :keys
58
+
59
+
60
+ ### Merge the specified +fields+ into the event's data.
61
+ ### :TODO: Handle conflicts?
62
+ def merge( fields )
63
+ self.fields.merge!( fields )
64
+ rescue FrozenError => err
65
+ raise "event is already resolved", cause: err
66
+ end
67
+
68
+
69
+ ### Finalize all of the event's data and return it as a Hash.
70
+ def resolve
71
+ unless @fields.frozen?
72
+ self.log.debug "Resolving event %#x" % [ self.object_id ]
73
+ data = self.fields.merge(
74
+ :@type => self.type,
75
+ :@timestamp => self.timestamp,
76
+ :@version => FORMAT_VERSION
77
+ )
78
+ data = data.transform_values( &self.method(:resolve_value) )
79
+ @fields = data.freeze
80
+ end
81
+
82
+ return @fields
83
+ end
84
+ alias_method :to_h, :resolve
85
+
86
+
87
+ ### Resolve the given +value+ into a serializable object.
88
+ def resolve_value( value )
89
+ case
90
+
91
+ when value.respond_to?( :call ) # Procs, Methods
92
+ return value.call( self )
93
+
94
+ when value.respond_to?( :iso8601 ) # Time, Date, DateTime, etc.
95
+ return value.iso8601( 6 )
96
+
97
+ else
98
+ return value
99
+ end
100
+ end
101
+
102
+ end # class Observability::Event
103
+
@@ -0,0 +1,296 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'concurrent'
5
+ require 'loggability'
6
+
7
+ require 'observability' unless defined?( Observability )
8
+
9
+
10
+ class Observability::Observer
11
+ extend Loggability
12
+
13
+
14
+ # Pattern for finding places for underscores when changing a camel-cased string
15
+ # to a snake-cased one.
16
+ SNAKE_CASE_SEPARATOR = /(\P{Upper}\p{Upper}|\p{Lower}\P{Lower})/
17
+
18
+
19
+ # Log to Observability's internal logger
20
+ log_to :observability
21
+
22
+
23
+ ### Create a new Observer that will send events via the specified +sender+.
24
+ def initialize( sender_type=nil )
25
+ @sender = self.configured_sender( sender_type )
26
+ @event_stack = Concurrent::ThreadLocalVar.new( &Array.method(:new) )
27
+ @context_stack = Concurrent::ThreadLocalVar.new( &Array.method(:new) )
28
+ end
29
+
30
+
31
+ ######
32
+ public
33
+ ######
34
+
35
+ ##
36
+ # The Observability::Sender used to deliver events
37
+ attr_reader :sender
38
+
39
+
40
+ ### Start recording events and sending them.
41
+ def start
42
+ self.sender.start
43
+ end
44
+
45
+
46
+ ### Stop recording and sending events.
47
+ def stop
48
+ self.sender.stop
49
+ end
50
+
51
+
52
+ ### Create a new event with the specified +type+ and make it the current one.
53
+ def event( type, **options, &block )
54
+ type = self.type_from_object( type )
55
+ fields = self.fields_from_options( options )
56
+
57
+ event = Observability::Event.new( type, **fields )
58
+ @event_stack.value.push( event )
59
+
60
+ new_context = @context_stack.value.last&.dup || {}
61
+ @context_stack.value.push( new_context )
62
+
63
+ return self.finish_after_block( event.object_id, &block ) if block
64
+ return event.object_id
65
+ end
66
+
67
+
68
+ ### Finish the and send the current event, comparing it against +event_marker+
69
+ ### and raising an exception if it's provided but doesn't match.
70
+ ### --
71
+ ### :TODO: Instead of raising, dequeue events up to the given marker if it
72
+ ### exists in the queue?
73
+ def finish( event_marker=nil )
74
+ raise "Event mismatch" if
75
+ event_marker && @event_stack.value.last.object_id != event_marker
76
+
77
+ event = @event_stack.value.pop
78
+ context = @context_stack.value.pop
79
+ self.log.debug "Adding context %p (%d left) to finishing event." %
80
+ [ context, @context_stack.value.length ]
81
+ event.merge( context )
82
+
83
+ self.log.debug "Finishing event: %p" % [ event ]
84
+ self.sender.enqueue( event )
85
+
86
+ return event
87
+ end
88
+
89
+
90
+ ### Call the given +block+, then when it returns, finish the event that
91
+ ### corresponds to the given +marker+.
92
+ def finish_after_block( event_marker=nil, &block )
93
+ block.call( self )
94
+ rescue Exception => err
95
+ self.add( err )
96
+ raise
97
+ ensure
98
+ self.finish( event_marker )
99
+ end
100
+
101
+
102
+ ### Add an +object+ and/or a Hash of +fields+ to the current event.
103
+ def add( object=nil, **fields )
104
+ self.log.debug "Adding %p" % [ object || fields ]
105
+ event = @event_stack.value.last or return
106
+
107
+ if object
108
+ object_fields = self.fields_from_object( object )
109
+ fields = fields.merge( object_fields )
110
+ end
111
+
112
+ event.merge( fields )
113
+ end
114
+
115
+
116
+ ### Add the specified +fields+ to the current event and any that are created
117
+ ### before the current event is finished.
118
+ def add_context( object=nil, **fields )
119
+ self.log.debug "Adding context from %p" % [ object || fields ]
120
+ current_context = @context_stack.value.last or return
121
+
122
+ if object
123
+ object_fields = self.fields_from_object( object )
124
+ fields = fields.merge( object_fields )
125
+ end
126
+
127
+ current_context.merge!( fields )
128
+ end
129
+
130
+
131
+ ### Return the depth of the stack of pending events.
132
+ def pending_event_count
133
+ return @event_stack.value.length
134
+ end
135
+
136
+
137
+ ### Returns +true+ if the Observer has events that are under construction.
138
+ def has_pending_events?
139
+ return self.pending_event_count.nonzero? ? true : false
140
+ end
141
+ alias_method :pending_events?, :has_pending_events?
142
+
143
+
144
+ #########
145
+ protected
146
+ #########
147
+
148
+ #
149
+ # Types
150
+ #
151
+
152
+ ### Derive an event type from the specified +object+ and an optional +block+
153
+ ### that provides additional context.
154
+ def type_from_object( object, block=nil )
155
+ case object
156
+ when Module
157
+ self.log.debug "Deriving a type from module %p" % [ object ]
158
+ return self.type_from_module( object )
159
+
160
+ when Method, UnboundMethod
161
+ self.log.debug "Deriving a type from method %p" % [ object ]
162
+ return self.type_from_method( object )
163
+
164
+ when Array
165
+ self.log.debug "Deriving a type from context %p" % [ object ]
166
+ return self.type_from_context( *object )
167
+
168
+ when Proc
169
+ self.log.debug "Deriving a type from context proc %p" % [ object ]
170
+ return self.type_from_context_proc( object )
171
+
172
+ when String
173
+ self.log.debug "Using string %p as type" % [ object ]
174
+ return object
175
+
176
+ else
177
+ raise "don't know how to derive an event type from a %p" % [ object.class ]
178
+ end
179
+ end
180
+
181
+
182
+ ### Derive an event type from the specified Class or Module +object+.
183
+ def type_from_module( object )
184
+ if ( name = object.name )
185
+ name = name.split( '::' ).collect do |part|
186
+ part.gsub( SNAKE_CASE_SEPARATOR ) do |m|
187
+ "%s_%s" % [ m[0], m[1] ]
188
+ end
189
+ end.join( '.' )
190
+
191
+ return name.downcase
192
+ else
193
+ return "anonymous_%s_%d" % [ object.class.name.downcase, object.object_id ]
194
+ end
195
+ end
196
+
197
+
198
+ ### Derive an event type from the specified Method or UnboundMethod +object+.
199
+ def type_from_method( object )
200
+ name = object.original_name
201
+ mod = object.owner
202
+
203
+ return [ self.type_from_module(mod), name.to_s ].join( '.' )
204
+ end
205
+
206
+
207
+ ### Derive an event type from the specified Method and a +detail+.
208
+ def type_from_context( context, *details )
209
+ prefix = self.type_from_object( context )
210
+ suffix = details.map {|detail| detail.to_s }
211
+
212
+ return ([prefix] + suffix).join( '.' )
213
+ end
214
+
215
+
216
+ ### Derive an event type from the specified Proc, which should be a block passed
217
+ ### to an observed method.
218
+ def type_from_context_proc( object )
219
+ bind = object.binding
220
+ recv = bind.receiver
221
+ methname = bind.eval( "__method__" )
222
+ meth = recv.method( methname )
223
+
224
+ return self.type_from_object( meth )
225
+ end
226
+
227
+
228
+ #
229
+ # Fields
230
+ #
231
+
232
+ ### Extract fields specified by the specified +options+ and return them all
233
+ ### merged into one Hash.
234
+ ### :TODO: Handle options like :model, :timed, etc.
235
+ def fields_from_options( options )
236
+ fields = {}
237
+
238
+ options.each do |key, value|
239
+ self.log.debug "Applying option %p: %p" % [ key, value ]
240
+ case key
241
+ when :add
242
+ fields.merge!( value )
243
+ when :timed
244
+ duration_callback = lambda {|ev| Concurrent.monotonic_time - ev.start }
245
+ fields.merge!( duration: duration_callback )
246
+ else
247
+ raise "unknown event option %p" % [ key ]
248
+ end
249
+ end
250
+
251
+ return fields
252
+ end
253
+
254
+
255
+ ### Return a Hash of fields to add to the current event derived from the given
256
+ ### +object+.
257
+ def fields_from_object( object )
258
+ case object
259
+ when ::Exception
260
+ return self.fields_from_exception( object )
261
+ else
262
+ if object.respond_to?( :to_h )
263
+ return object.to_h
264
+ else
265
+ raise "don't know how to derive fields from a %p" % [ object.class ]
266
+ end
267
+ end
268
+ end
269
+
270
+
271
+ ### Return a Hash of fields to add to the current event derived from the given
272
+ ### +exception+ object.
273
+ def fields_from_exception( exception )
274
+ trace_frames = exception.backtrace_locations.map do |loc|
275
+ { label: loc.label, path: loc.absolute_path, lineno: loc.lineno }
276
+ end
277
+
278
+ return {
279
+ error: {
280
+ type: exception.class.name,
281
+ message: exception.message,
282
+ backtrace: trace_frames
283
+ }
284
+ }
285
+ end
286
+
287
+
288
+ ### Create an instance of the given +sender_type+, or the type specified in the
289
+ ### configuration if +sender_type+ is nil.
290
+ def configured_sender( sender_type )
291
+ return Observability::Sender.create( sender_type ) if sender_type
292
+ return Observability::Sender.configured_type
293
+ end
294
+
295
+ end # class Observability::Observer
296
+
@@ -0,0 +1,28 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'observability' unless defined?( Observability )
5
+
6
+
7
+ # A mixin that allows events to be created for any current observers at runtime.
8
+ module Observability::ObserverHooks
9
+
10
+ ### Create an event at the current point of execution, make it the innermost
11
+ ### context, then yield to the method's block. Finish the event when the yield
12
+ ### returns, handling exceptions that are being raised automatically.
13
+ def observe( detail, **options, &block )
14
+ raise LocalJumpError, "no block given" unless block
15
+
16
+ marker = Observability.observer.event( [block, detail], **options )
17
+ Observability.observer.finish_after_block( marker, &block )
18
+ end
19
+
20
+
21
+ ### Return the current Observability observer agent.
22
+ def observability
23
+ return Observability.observer
24
+ end
25
+
26
+ end # module Observability::ObserverHooks
27
+
28
+
@@ -0,0 +1,127 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require 'concurrent'
6
+ require 'pluggability'
7
+
8
+ require 'observability' unless defined?( Observability )
9
+
10
+
11
+ class Observability::Sender
12
+ extend Pluggability,
13
+ Loggability,
14
+ Configurability
15
+
16
+
17
+ # Logs go to the main module
18
+ log_to :observability
19
+
20
+ # Set the prefix for derivative classes
21
+ plugin_prefixes 'observability/sender'
22
+
23
+ # Configuration settings
24
+ configurability( 'observability.sender' ) do
25
+
26
+ ##
27
+ # The sender type to use
28
+ setting :type, default: :null
29
+
30
+ end
31
+
32
+
33
+
34
+ # Prevent direct instantiation
35
+ private_class_method :new
36
+
37
+ ### Let subclasses be inherited
38
+ def self::inherited( subclass )
39
+ super
40
+ subclass.public_class_method( :new )
41
+ end
42
+
43
+
44
+ ### Return an instance of the configured type of Sender.
45
+ def self::configured_type
46
+ return self.create( self.type )
47
+ end
48
+
49
+
50
+ ### Set up some instance variables
51
+ def initialize # :notnew:
52
+ @executor = nil
53
+ end
54
+
55
+
56
+ ######
57
+ public
58
+ ######
59
+
60
+ ##
61
+ # The processing executor.
62
+ attr_reader :executor
63
+
64
+
65
+ ### Start sending queued events.
66
+ def start
67
+ self.log.debug "Starting a %p" % [ self.class ]
68
+ @executor = Concurrent::SingleThreadExecutor.new( fallback_policy: :abort )
69
+ @executor.auto_terminate = true
70
+ end
71
+
72
+
73
+ ### Stop the sender's executor.
74
+ def stop
75
+ self.log.debug "Stopping the %p" % [ self.class ]
76
+ return if !self.executor || self.executor.shuttingdown? || self.executor.shutdown?
77
+
78
+ self.log.debug " shutting down the executor"
79
+ self.executor.shutdown
80
+ unless self.executor.wait_for_termination( 3 )
81
+ self.log.debug " killing the executor"
82
+ self.executor.halt
83
+ self.executor.wait_for_termination( 3 )
84
+ end
85
+ end
86
+
87
+
88
+ ### Queue up the specified +events+ for sending.
89
+ def enqueue( *events )
90
+ posted_event = Concurrent::Event.new
91
+
92
+ unless self.executor
93
+ self.log.debug "No executor; dropping %d events" % [ events.length ]
94
+ posted_event.set
95
+ return posted_event
96
+ end
97
+
98
+ self.executor.post( *events ) do |*ev|
99
+ serialized = self.serialize_events( ev.flatten )
100
+ serialized.each do |ev|
101
+ self.send_event( ev )
102
+ end
103
+ posted_event.set
104
+ end
105
+
106
+ return posted_event
107
+ end
108
+
109
+
110
+ #########
111
+ protected
112
+ #########
113
+
114
+ ### Serialize each the given +events+ and return the results.
115
+ def serialize_events( events )
116
+ return events.map( &:resolve )
117
+ end
118
+
119
+
120
+ ### Send the specified +event+.
121
+ def send_event( event )
122
+ self.log.warn "%p does not implement required method %s" % [ self.class, __method__ ]
123
+ end
124
+
125
+ end # class Observability::Sender
126
+
127
+