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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e8e5db4293106c0b48ed3ea428c4a786eec948d11f19e8a78950ee49383655b
4
- data.tar.gz: a9195f349e14fbc0721c22af67f8cb115bb15264a68dbf12ffd4243418f62167
3
+ metadata.gz: eb2bcfb9c815937bbc46798e8645ccc77e0a7e87dfa5024d327c9e9d4db5a1ff
4
+ data.tar.gz: c561389042c72073f21a5e18c06252a36177cd19022e6330c2633f8368953a8f
5
5
  SHA512:
6
- metadata.gz: a851f201c9ff6757fb53bd14455b062fec2e4923d1386ee0192bc7f3033b3bb74ce48549b2b21ce4888ed674cb6f48050c89039b9d678d32baf85721fde965db
7
- data.tar.gz: b7eee539d2e8a7ea9ae40262071b21f986945786f2409e3721342e182e5acb6bee27c25a2dc35ae5db1952aa434c32faec23203946b345cdfb88201fc7f4b4f9
6
+ metadata.gz: 4295af2f27a6d6eed728608896ae92fd79736d159c9b2f4c0595b5918708e6b6f63cde95d9824450daa741db481b0ecc7331c842fd43006eb03e704a35b9e603
7
+ data.tar.gz: e98b10c101567afa169e55d4b75dffa400d06491ff0320e3c9f01c72d73349ca413655697bb4e69b6493bfa676b2682ff1a48a035f3474de9552e349f9734fbf
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
- : http://bitbucket.org/ged/observability
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
- following the principle of Observability-Oriented Design as expressed by Charity
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][bitbucket]. Or if you prefer Git, via
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
- [bitbucket]: http://bitbucket.org/ged/observability
99
+ [sourcehut]: https://hg.sr.ht/~ged/Observability
92
100
  [github]: https://github.com/ged/observability
93
101
 
@@ -13,7 +13,7 @@ module Observability
13
13
 
14
14
 
15
15
  # Package version
16
- VERSION = '0.1.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
@@ -83,7 +83,7 @@ class Observability::Collector::Timescale < Observability::Collector
83
83
  self.stop_processing
84
84
 
85
85
  @cursor = nil
86
- @db.disconnct
86
+ @db.disconnect
87
87
  end
88
88
 
89
89
 
@@ -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
+