observability 0.1.0 → 0.2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/History.md +15 -0
- data/README.md +13 -5
- data/lib/observability.rb +9 -1
- data/lib/observability/collector/rabbitmq.rb +196 -0
- data/lib/observability/collector/timescale.rb +1 -1
- data/lib/observability/instrumentation.rb +118 -0
- data/lib/observability/instrumentation/bunny.rb +24 -0
- data/lib/observability/instrumentation/grape.rb +22 -0
- data/lib/observability/instrumentation/rack.rb +23 -0
- data/lib/observability/instrumentation/sequel.rb +37 -0
- data/lib/observability/observer.rb +15 -9
- data/lib/observability/sender.rb +3 -1
- data/lib/observability/sender/udp_multicast.rb +114 -0
- data/spec/observability/instrumentation_spec.rb +48 -0
- data/spec/observability/observer_spec.rb +31 -1
- metadata +42 -123
- metadata.gz.sig +0 -0
- data/.document +0 -5
- data/.rdoc_options +0 -16
- data/.simplecov +0 -9
- data/ChangeLog +0 -139
- data/Manifest.txt +0 -31
- data/Rakefile +0 -102
- data/examples/basic-usage.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb2bcfb9c815937bbc46798e8645ccc77e0a7e87dfa5024d327c9e9d4db5a1ff
|
4
|
+
data.tar.gz: c561389042c72073f21a5e18c06252a36177cd19022e6330c2633f8368953a8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4295af2f27a6d6eed728608896ae92fd79736d159c9b2f4c0595b5918708e6b6f63cde95d9824450daa741db481b0ecc7331c842fd43006eb03e704a35b9e603
|
7
|
+
data.tar.gz: e98b10c101567afa169e55d4b75dffa400d06491ff0320e3c9f01c72d73349ca413655697bb4e69b6493bfa676b2682ff1a48a035f3474de9552e349f9734fbf
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/History.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
# Release History for observability
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
## v0.2.0 [2019-10-16] Michael Granger <ged@faeriemud.org>
|
6
|
+
|
7
|
+
Improvements:
|
8
|
+
|
9
|
+
- Add an experimental rabbitmq collector
|
10
|
+
- Add a udp multicast sender
|
11
|
+
- Handle exceptions with a #cause
|
12
|
+
- Add instrumentation API
|
13
|
+
- Update the timescale store schema
|
14
|
+
|
15
|
+
|
1
16
|
## v0.1.0 [2019-07-23] Michael Granger <ged@FaerieMUD.org>
|
2
17
|
|
3
18
|
Initial release.
|
data/README.md
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# Observability
|
2
2
|
|
3
|
+
home
|
4
|
+
: https://hg.sr.ht/~ged/Observability
|
5
|
+
|
3
6
|
code
|
4
|
-
:
|
7
|
+
: https://hg.sr.ht/~ged/Observability/browse
|
5
8
|
|
6
9
|
github
|
7
10
|
: https://github.com/ged/observability
|
@@ -12,8 +15,8 @@ docs
|
|
12
15
|
|
13
16
|
## Description
|
14
17
|
|
15
|
-
Observability is a toolkit for instrumenting code to make it more observable
|
16
|
-
|
18
|
+
Observability is a toolkit for instrumenting code to make it more observable.
|
19
|
+
It follows the principle of Observability-Oriented Design as expressed by Charity
|
17
20
|
Majors (@mipsytipsy).
|
18
21
|
|
19
22
|
Its goals are [stolen from https://charity.wtf/2019/02/05/logs-vs-structured-events/]:
|
@@ -46,7 +49,7 @@ Its goals are [stolen from https://charity.wtf/2019/02/05/logs-vs-structured-eve
|
|
46
49
|
## Contributing
|
47
50
|
|
48
51
|
You can check out the current development source with Mercurial via its
|
49
|
-
[project page][
|
52
|
+
[project page][sourcehut]. Or if you prefer Git, via
|
50
53
|
[its Github mirror][github].
|
51
54
|
|
52
55
|
After checking out the source, run:
|
@@ -57,6 +60,11 @@ This task will install any missing dependencies, run the tests/specs,
|
|
57
60
|
and generate the API documentation.
|
58
61
|
|
59
62
|
|
63
|
+
## Author
|
64
|
+
|
65
|
+
- Michael Granger <ged@faeriemud.org>
|
66
|
+
|
67
|
+
|
60
68
|
## License
|
61
69
|
|
62
70
|
Copyright (c) 2019, Michael Granger
|
@@ -88,6 +96,6 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
88
96
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
89
97
|
|
90
98
|
|
91
|
-
[
|
99
|
+
[sourcehut]: https://hg.sr.ht/~ged/Observability
|
92
100
|
[github]: https://github.com/ged/observability
|
93
101
|
|
data/lib/observability.rb
CHANGED
@@ -13,7 +13,7 @@ module Observability
|
|
13
13
|
|
14
14
|
|
15
15
|
# Package version
|
16
|
-
VERSION = '0.
|
16
|
+
VERSION = '0.2.0'
|
17
17
|
|
18
18
|
# Version control revision
|
19
19
|
REVISION = %q$Revision$
|
@@ -27,6 +27,7 @@ module Observability
|
|
27
27
|
|
28
28
|
autoload :Collector, 'observability/collector'
|
29
29
|
autoload :Event, 'observability/event'
|
30
|
+
autoload :Instrumentation, 'observability/instrumentation'
|
30
31
|
autoload :ObserverHooks, 'observability/observer_hooks'
|
31
32
|
autoload :Observer, 'observability/observer'
|
32
33
|
autoload :Sender, 'observability/sender'
|
@@ -59,6 +60,13 @@ module Observability
|
|
59
60
|
end
|
60
61
|
|
61
62
|
|
63
|
+
### Install the default instrumentatin for one or more +libraries+.
|
64
|
+
def self::install_instrumentation( *libraries )
|
65
|
+
Observability::Instrumentation.load( *libraries )
|
66
|
+
Observability::Instrumentation.install
|
67
|
+
end
|
68
|
+
|
69
|
+
|
62
70
|
### Return the current Observer, creating it if necessary.
|
63
71
|
def self::observer
|
64
72
|
unless @observer.complete?
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'configurability'
|
6
|
+
require 'bunny'
|
7
|
+
|
8
|
+
require 'observability/collector' unless defined?( Observability::Collector )
|
9
|
+
|
10
|
+
|
11
|
+
# A collector that re-injects events over AMQP to a RabbitMQ cluster.
|
12
|
+
class Observability::Collector::RabbitMQ < Observability::Collector
|
13
|
+
extend Configurability,
|
14
|
+
Loggability
|
15
|
+
|
16
|
+
# The maximum size of event messages
|
17
|
+
MAX_EVENT_BYTES = 64 * 1024
|
18
|
+
|
19
|
+
# The number of seconds to wait between IO loops
|
20
|
+
LOOP_TIMER = 0.25
|
21
|
+
|
22
|
+
# Default options for publication
|
23
|
+
DEFAULT_PUBLISH_OPTIONS = {
|
24
|
+
mandatory: false,
|
25
|
+
persistent: true
|
26
|
+
}
|
27
|
+
|
28
|
+
|
29
|
+
log_to :observability
|
30
|
+
|
31
|
+
configurability( 'observability.collector.rabbitmq' ) do
|
32
|
+
|
33
|
+
##
|
34
|
+
# The host to bind to
|
35
|
+
setting :host, default: 'localhost'
|
36
|
+
|
37
|
+
##
|
38
|
+
# The port to bind to
|
39
|
+
setting :port, default: 15775
|
40
|
+
|
41
|
+
##
|
42
|
+
# The broker_uri to use when connecting to RabbitMQ
|
43
|
+
setting :broker_uri
|
44
|
+
|
45
|
+
##
|
46
|
+
# The exchange to use when connecting to RabbitMQ
|
47
|
+
setting :exchange, default: 'events'
|
48
|
+
|
49
|
+
##
|
50
|
+
# The vhost to use when connecting to RabbitMQ
|
51
|
+
setting :vhost, default: '/telemetry'
|
52
|
+
|
53
|
+
##
|
54
|
+
# The heartbeat to use when connecting to RabbitMQ
|
55
|
+
setting :heartbeat, default: 'server' do |value|
|
56
|
+
value.to_sym if value
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Use single-threaded connections when set to `false`
|
61
|
+
setting :threaded, default: false
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
### Fetch a Hash of AMQP options.
|
67
|
+
def self::amqp_session_options
|
68
|
+
return {
|
69
|
+
logger: Loggability[ Observability ],
|
70
|
+
heartbeat: self.heartbeat,
|
71
|
+
exchange: self.exchange,
|
72
|
+
vhost: self.vhost,
|
73
|
+
threaded: self.threaded,
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### Return a formatted list of the server's capabilities listed in +server_info+.
|
79
|
+
def self::capabilities_list( server_info )
|
80
|
+
server_info.
|
81
|
+
map {|name,enabled| enabled ? name : nil }.
|
82
|
+
compact.join(', ')
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
### Establish the connection to RabbitMQ based on the loaded configuration.
|
87
|
+
def self::configured_amqp_session
|
88
|
+
uri = self.broker_uri or raise "No broker_uri configured."
|
89
|
+
options = self.amqp_session_options
|
90
|
+
|
91
|
+
session = Bunny.new( uri, options )
|
92
|
+
session.start
|
93
|
+
|
94
|
+
self.log.info "Connected to %s v%s server: %s" % [
|
95
|
+
session.server_properties['product'],
|
96
|
+
session.server_properties['version'],
|
97
|
+
self.capabilities_list( session.server_properties['capabilities'] ),
|
98
|
+
]
|
99
|
+
|
100
|
+
return session
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
### Create a new UDP collector
|
106
|
+
def initialize
|
107
|
+
super
|
108
|
+
|
109
|
+
@socket = UDPSocket.new
|
110
|
+
@amqp_session = nil
|
111
|
+
@amqp_channel = Concurrent::ThreadLocalVar.new { @amqp_session.create_channel }
|
112
|
+
@amqp_exchange = Concurrent::ThreadLocalVar.new do
|
113
|
+
@amqp_channel.value.headers( self.class.exchange, passive: true )
|
114
|
+
end
|
115
|
+
@processing = false
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
######
|
120
|
+
public
|
121
|
+
######
|
122
|
+
|
123
|
+
### Start receiving events.
|
124
|
+
def start
|
125
|
+
self.log.info "Starting up."
|
126
|
+
|
127
|
+
@amqp_session = self.class.configured_amqp_session
|
128
|
+
@socket.bind( self.class.host, self.class.port )
|
129
|
+
|
130
|
+
self.start_processing
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
### Stop receiving events.
|
135
|
+
def stop
|
136
|
+
self.stop_processing
|
137
|
+
|
138
|
+
@socket.shutdown( :SHUT_RDWR )
|
139
|
+
@amqp_session.close
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
### Start consuming incoming events and storing them.
|
144
|
+
def start_processing
|
145
|
+
@processing = true
|
146
|
+
while @processing
|
147
|
+
event = self.read_next_event or next
|
148
|
+
self.log.debug "Read event: %p" % [ event ]
|
149
|
+
self.store_event( event )
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
### Stop consuming events.
|
155
|
+
def stop_processing
|
156
|
+
@processing = false
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
### Read the next event from the socket
|
161
|
+
def read_next_event
|
162
|
+
self.log.debug "Reading next event."
|
163
|
+
data = @socket.recv_nonblock( MAX_EVENT_BYTES, exception: false )
|
164
|
+
|
165
|
+
if data == :wait_readable
|
166
|
+
IO.select( [@socket], nil, nil, LOOP_TIMER )
|
167
|
+
return nil
|
168
|
+
elsif data.empty?
|
169
|
+
return nil
|
170
|
+
else
|
171
|
+
self.log.info "Read %d bytes" % [ data.bytesize ]
|
172
|
+
return JSON.parse( data )
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
### Store the specified +event+.
|
178
|
+
def store_event( event )
|
179
|
+
time = event.delete( '@timestamp' )
|
180
|
+
type = event.delete( '@type' )
|
181
|
+
version = event.delete( '@version' )
|
182
|
+
|
183
|
+
data = JSON.generate( event )
|
184
|
+
headers = {
|
185
|
+
time: time,
|
186
|
+
type: type,
|
187
|
+
version: version,
|
188
|
+
content_type: 'application/json',
|
189
|
+
content_encoding: data.encoding.name,
|
190
|
+
timestamp: Time.now.to_f,
|
191
|
+
}
|
192
|
+
|
193
|
+
@amqp_exchange.value.publish( data, headers )
|
194
|
+
end
|
195
|
+
|
196
|
+
end # class Observability::Collector::RabbitMQ
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'set'
|
5
|
+
require 'loggability'
|
6
|
+
|
7
|
+
require 'observability' unless defined?( Observability )
|
8
|
+
|
9
|
+
|
10
|
+
# Utilities for loading and installing pre-packaged instrumentation for common
|
11
|
+
# libraries.
|
12
|
+
module Observability::Instrumentation
|
13
|
+
extend Loggability
|
14
|
+
|
15
|
+
|
16
|
+
# Loggability API -- use the Observability logger for instrumentation
|
17
|
+
log_to :observability
|
18
|
+
|
19
|
+
|
20
|
+
##
|
21
|
+
# :singleton-method:
|
22
|
+
# The Set of Instrumentation modules that are laoded
|
23
|
+
singleton_class.attr_reader :modules
|
24
|
+
@modules = Set.new
|
25
|
+
|
26
|
+
|
27
|
+
### Load instrumentation for the specified +libraries+.
|
28
|
+
def self::load( *libraries )
|
29
|
+
libraries.flatten.each do |library|
|
30
|
+
libfile = "observability/instrumentation/%s" % [ library ]
|
31
|
+
require( libfile )
|
32
|
+
end
|
33
|
+
|
34
|
+
return self
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
### Extension callback -- declare some instance variables in the extending
|
39
|
+
### +mod+.
|
40
|
+
def self::extended( mod )
|
41
|
+
super
|
42
|
+
|
43
|
+
if self.modules.add?( mod )
|
44
|
+
self.log.info "Loaded %p" % [ mod ]
|
45
|
+
mod.extend( Loggability )
|
46
|
+
mod.log_to( :observability )
|
47
|
+
mod.instance_variable_set( :@depends_on, Set.new )
|
48
|
+
mod.instance_variable_set( :@installation_callbacks, Set.new )
|
49
|
+
mod.singleton_class.attr_reader( :installation_callbacks )
|
50
|
+
else
|
51
|
+
self.log.warn "Already loaded %p" % [ mod ]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
### Install loaded instrumentation if the requisite modules are present.
|
57
|
+
def self::install
|
58
|
+
self.modules.each do |mod|
|
59
|
+
mod.install if mod.available?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
### Returns +true+ if each of the given +module_names+ are defined and are
|
65
|
+
### Module objects.
|
66
|
+
def self::dependencies_met?( *module_names )
|
67
|
+
return module_names.flatten.all? do |mod|
|
68
|
+
self.check_for_module( mod )
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
### Returns +true+ if +mod+ is defined and is a Module.
|
74
|
+
def self::check_for_module( mod )
|
75
|
+
self.log.debug "Checking for presence of `%s` module..." % [ mod ]
|
76
|
+
Object.const_defined?( mod ) && Object.const_get( mod ).is_a?( Module )
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
### Declare +modules+ that must be available for instrumentation to be loaded.
|
81
|
+
### If they are not present when the instrumentation loads, it will be skipped
|
82
|
+
### entirely.
|
83
|
+
def depends_on( *modules )
|
84
|
+
@depends_on.merge( modules.flatten(1) )
|
85
|
+
return @depends_on
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
### Register a +callback+ that will be called when instrumentation is installed,
|
90
|
+
### if and only if all of the given +modules+ are present (may be empty).
|
91
|
+
def when_installed( *modules, &callback )
|
92
|
+
self.installation_callbacks.add( [callback, modules] )
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
### Returns +true+ if all of the modules registered with #depends_on are defined.
|
97
|
+
def available?
|
98
|
+
return Observability::Instrumentation.dependencies_met?( self.depends_on.to_a )
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
### Call installation callbacks which meet their prerequistes.
|
103
|
+
def install
|
104
|
+
self.installation_callbacks.each do |callback, dependencies|
|
105
|
+
missing = dependencies.
|
106
|
+
reject {|mod| Observability::Instrumentation.check_for_module(mod) }
|
107
|
+
|
108
|
+
if missing.empty?
|
109
|
+
self.log.debug "Instrumenting %s: %p" % [ dependencies.join(', '), callback ]
|
110
|
+
callback.call
|
111
|
+
else
|
112
|
+
self.log.info "Skipping %p: missing %s" % [ callback, missing.join(', ') ]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end # module Observability::Instrumentation
|
118
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'observability/instrumentation' unless defined?( Observability::Instrumentation )
|
5
|
+
|
6
|
+
|
7
|
+
# Instrumentation for the Bunny RabbitMQ client library
|
8
|
+
# Refs:
|
9
|
+
# - http://rubybunny.info/
|
10
|
+
module Observability::Instrumentation::Bunny
|
11
|
+
extend Observability::Instrumentation
|
12
|
+
|
13
|
+
depends_on 'Bunny'
|
14
|
+
|
15
|
+
|
16
|
+
when_installed( 'Bunny::Session' ) do
|
17
|
+
Bunny::Session.extend( Observability )
|
18
|
+
Bunny::Session.observe_class_method( :new )
|
19
|
+
Bunny::Session.observe_method( :start )
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
end # module Observability::Instrumentation::Bunny
|
24
|
+
|