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