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