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