mnemosyne-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +50 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE +21 -0
  8. data/README.md +36 -0
  9. data/Rakefile +7 -0
  10. data/lib/mnemosyne/client.rb +48 -0
  11. data/lib/mnemosyne/clock.rb +15 -0
  12. data/lib/mnemosyne/config.rb +53 -0
  13. data/lib/mnemosyne/global.rb +13 -0
  14. data/lib/mnemosyne/instrumenter.rb +111 -0
  15. data/lib/mnemosyne/middleware/acfs.rb +46 -0
  16. data/lib/mnemosyne/middleware/rack.rb +93 -0
  17. data/lib/mnemosyne/middleware/restify.rb +50 -0
  18. data/lib/mnemosyne/probe.rb +37 -0
  19. data/lib/mnemosyne/probes/acfs/request.rb +40 -0
  20. data/lib/mnemosyne/probes/action_controller/process_action.rb +34 -0
  21. data/lib/mnemosyne/probes/action_controller/renderers.rb +42 -0
  22. data/lib/mnemosyne/probes/action_view/render_partial.rb +26 -0
  23. data/lib/mnemosyne/probes/action_view/render_template.rb +26 -0
  24. data/lib/mnemosyne/probes/active_record/query.rb +34 -0
  25. data/lib/mnemosyne/probes/grape/endpoint_render.rb +31 -0
  26. data/lib/mnemosyne/probes/grape/endpoint_run.rb +31 -0
  27. data/lib/mnemosyne/probes/grape/endpoint_run_filters.rb +31 -0
  28. data/lib/mnemosyne/probes/mnemosyne/tracer.rb +26 -0
  29. data/lib/mnemosyne/probes/responder/respond.rb +43 -0
  30. data/lib/mnemosyne/probes/restify/em.rb +29 -0
  31. data/lib/mnemosyne/probes/restify/typhoeus.rb +29 -0
  32. data/lib/mnemosyne/probes.rb +90 -0
  33. data/lib/mnemosyne/railtie.rb +25 -0
  34. data/lib/mnemosyne/span.rb +36 -0
  35. data/lib/mnemosyne/trace.rb +46 -0
  36. data/lib/mnemosyne/version.rb +16 -0
  37. data/lib/mnemosyne.rb +44 -0
  38. data/mnemosyne-ruby.gemspec +29 -0
  39. data/scripts/console +15 -0
  40. data/scripts/setup +8 -0
  41. metadata +168 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5d1ae774cf6ab9a36c5d387675d49fc1f646dc03
4
+ data.tar.gz: 9a2e7ad49b27c1866dfd0517820849bb728744d9
5
+ SHA512:
6
+ metadata.gz: 2e0a25632d5e1447fdc7af933b02e5b459ebad667b442eb7ae8799ad4556bb42317a525d215ac383f2e24fd7bd028b69651c4f4ee1b2644e54bc7c83d9654159
7
+ data.tar.gz: 3bd51d5807b2b29dafe064976b0c3faa10649881056535ebe5720f7e4db9b16091006bea2984291fe32c0c15805dc55d3a4039f47e93f1b357d4f90eb23d1304
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,50 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.2
3
+
4
+ Rails:
5
+ Enabled: false
6
+
7
+ Style/AlignParameters:
8
+ EnforcedStyle: with_fixed_indentation
9
+
10
+ Style/BracesAroundHashParameters:
11
+ EnforcedStyle: context_dependent
12
+
13
+ Style/SpaceInsideHashLiteralBraces:
14
+ EnforcedStyle: no_space
15
+
16
+ Style/RaiseArgs:
17
+ EnforcedStyle: compact
18
+
19
+ Style/SpaceInsideBlockBraces:
20
+ EnforcedStyle: space
21
+ EnforcedStyleForEmptyBraces: no_space
22
+ SpaceBeforeBlockParameters: false
23
+
24
+ Style/SignalException:
25
+ EnforcedStyle: only_raise
26
+
27
+ Style/CaseIndentation:
28
+ IndentWhenRelativeTo: end
29
+ SupportedStyles:
30
+ - case
31
+ - end
32
+ IndentOneStep: true
33
+
34
+ Style/MultilineMethodCallIndentation:
35
+ EnforcedStyle: indented
36
+
37
+ Style/Semicolon:
38
+ Exclude:
39
+ - '**/*_spec.rb'
40
+
41
+ Style/RescueModifier:
42
+ Exclude:
43
+ - '**/*_spec.rb'
44
+
45
+ Metrics/LineLength:
46
+ Exclude:
47
+ - 'mnemosyne.gemspec'
48
+
49
+ Style/Documentation:
50
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - '2.3.3'
4
+ - '2.2.6'
5
+ before_install:
6
+ - gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in mnemosyne.gemspec
5
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Jan Graichen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # Mnemosyne
2
+
3
+ The ruby client plugin for the Mnemosyne monitoring system. It extracts full application traces including cross-application requests for distributed applications (services etc.).
4
+
5
+ Currently supported probes:
6
+
7
+ * ActionController: Process Action, Renderers
8
+ * ActiveRecord: SQL query
9
+ * Responders: render time
10
+ * Custom instrumentation
11
+
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'mnemosyne'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install mnemosyne
28
+
29
+ ## Usage
30
+
31
+ TODO
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
36
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ require 'bunny'
3
+
4
+ module Mnemosyne
5
+ class Client
6
+ attr_reader :connection
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def connection
13
+ @connection ||= begin
14
+ @config.logger.info "[Mnemosyne] Connect to #{@config.server}..."
15
+
16
+ connection = ::Bunny.new @config.amqp,
17
+ logger: @config.logger,
18
+ threaded: false
19
+
20
+ connection.start
21
+ connection
22
+ end
23
+ end
24
+
25
+ def channel
26
+ @channel ||= connection.create_channel
27
+ end
28
+
29
+ def exchange
30
+ @exchange ||= channel.topic @config.exchange, durable: true
31
+ end
32
+
33
+ def send(key, data)
34
+ blob = JSON.dump data
35
+
36
+ exchange.publish blob,
37
+ routing_key: key,
38
+ persistent: true,
39
+ content_type: 'application/json'
40
+ end
41
+
42
+ class << self
43
+ def instance
44
+ @instance ||= new
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ module Clock
5
+ class << self
6
+ def tick
7
+ to_tick Time.now
8
+ end
9
+
10
+ def to_tick(time)
11
+ time.to_i * 1_000_000_000 + time.nsec
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require 'socket'
3
+ require 'uri'
4
+ require 'cgi'
5
+
6
+ module Mnemosyne
7
+ class Config
8
+ attr_reader :application
9
+ attr_reader :hostname
10
+ attr_reader :amqp
11
+ attr_reader :exchange
12
+ attr_reader :logger
13
+ attr_reader :server
14
+
15
+ # rubocop:disable Metrics/AbcSize
16
+ def initialize(config)
17
+ @application = config.fetch('application').freeze
18
+ @enabled = config.fetch('enabled', true)
19
+ @hostname = config.fetch('hostname') { default_hostname }.freeze
20
+ @exchange = config.fetch('exchange', 'mnemosyne').freeze
21
+ @logger = config.fetch('logger') { Logger.new($stdout) }
22
+
23
+ server = config.fetch('server', 'amqp://localhost')
24
+ @amqp = AMQ::Settings.configure(server).freeze
25
+ @server = make_amqp_uri(@amqp).freeze
26
+
27
+ raise 'Application must be configured' unless application.present?
28
+ raise 'Hostname must be configured' unless hostname.present?
29
+ end
30
+
31
+ def enabled?
32
+ @enabled
33
+ end
34
+
35
+ private
36
+
37
+ def default_hostname
38
+ Socket.gethostbyname(Socket.gethostname).first
39
+ end
40
+
41
+ def make_amqp_uri(amqp)
42
+ uri = URI('')
43
+
44
+ uri.scheme = amqp[:scheme]
45
+ uri.user = amqp[:user]
46
+ uri.host = amqp[:host]
47
+ uri.port = amqp[:port] if amqp[:port] != AMQ::URI::AMQP_PORTS[uri.scheme]
48
+ uri.path = '/' + ::CGI.escape(amqp[:vhost]) if amqp[:vhost] != '/'
49
+
50
+ uri
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ module Global
5
+ def trace(name, meta: {})
6
+ ::ActiveSupport::Notifications.instrument 'trace.mnemosyne',
7
+ name: name, meta: meta do
8
+
9
+ yield
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+ require 'thread'
3
+
4
+ module Mnemosyne
5
+ class Instrumenter
6
+ IDENT = :__mnemosyne_current_trace
7
+ MUTEX = Mutex.new
8
+
9
+ attr_reader :logger
10
+
11
+ def initialize(config)
12
+ @config = config
13
+
14
+ raise 'Config required!' unless @config
15
+
16
+ @logger = config.logger
17
+ @client = Client.new(config)
18
+
19
+ logger.info 'Mnemosyne instrumenter started.'
20
+ end
21
+
22
+ def current_trace
23
+ Thread.current[IDENT]
24
+ end
25
+
26
+ def current_trace=(trace)
27
+ Thread.current[IDENT] = trace
28
+ end
29
+
30
+ # rubocop:disable Metrics/MethodLength
31
+ def trace(name, **kwargs)
32
+ if (trace = current_trace)
33
+ return yield trace if block_given?
34
+ return trace
35
+ end
36
+
37
+ trace = self.current_trace = Trace.new(self, name, **kwargs)
38
+
39
+ return trace unless block_given?
40
+
41
+ begin
42
+ yield trace
43
+ ensure
44
+ self.current_trace = nil
45
+ trace.submit
46
+ end
47
+ end
48
+
49
+ def submit(trace)
50
+ blob = {
51
+ hostname: @config.hostname,
52
+ application: @config.application
53
+ }
54
+
55
+ # TODO: nest
56
+ blob.merge! trace.serialize
57
+
58
+ logger.debug { "Submit trace #{trace.uuid}" }
59
+
60
+ @client.send 'mnemosyne.trace', blob
61
+ end
62
+
63
+ def release(trace)
64
+ return unless current_trace.equal?(trace)
65
+
66
+ self.current_trace = nil
67
+ end
68
+
69
+ class << self
70
+ attr_reader :instance
71
+
72
+ def start!(config = nil)
73
+ return @instance if @instance
74
+
75
+ MUTEX.synchronize do
76
+ return @instance if @instance
77
+
78
+ @instance = new(config)
79
+ end
80
+ rescue => err
81
+ message = "Unable to start instrumenter: #{err}"
82
+
83
+ if config && config.respond_to?(:logger)
84
+ config.logger.warn message
85
+ else
86
+ ::Kernel.warn message
87
+ end
88
+
89
+ raise
90
+ end
91
+
92
+ def trace(*args)
93
+ return unless (instrumenter = instance)
94
+ instrumenter.trace(*args)
95
+ end
96
+
97
+ def logger
98
+ if (instrumenter = instance)
99
+ instrumenter.logger
100
+ else
101
+ @logger ||= Logger.new($stdout)
102
+ end
103
+ end
104
+
105
+ def current_trace
106
+ return unless (instrumenter = instance)
107
+ instrumenter.current_trace
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ module Middleware
5
+ class Acfs
6
+ def initialize(app, *_)
7
+ @app = app
8
+ end
9
+
10
+ # rubocop:disable Metrics/MethodLength
11
+ def call(request)
12
+ trace = ::Mnemosyne::Instrumenter.current_trace
13
+
14
+ if trace
15
+ span = ::Mnemosyne::Span.new('external.http.acfs',
16
+ meta: extract_meta(request))
17
+
18
+ span.start!
19
+
20
+ request.headers['X-Mnemosyne-Transaction'] = trace.transaction
21
+ request.headers['X-Mnemosyne-Origin'] = span.uuid
22
+ end
23
+
24
+ request.on_complete do |response, nxt|
25
+ begin
26
+ span.finish!
27
+ trace << span
28
+ ensure
29
+ nxt.call(response)
30
+ end
31
+ end
32
+
33
+ @app.call(request)
34
+ end
35
+
36
+ private
37
+
38
+ def extract_meta(request)
39
+ {
40
+ method: request.method,
41
+ url: request.url
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ module Middleware
5
+ class Rack
6
+ class Proxy
7
+ def initialize(body, &block)
8
+ @body = body
9
+ @block = block
10
+ @closed = false
11
+ end
12
+
13
+ def respond_to_missing?(*args)
14
+ return false if args.first && args.first.to_s == 'to_ary'
15
+
16
+ @body.respond_to?(*args)
17
+ end
18
+
19
+ def method_missing(*args)
20
+ super if args.first && args.first.to_s == 'to_ary'
21
+
22
+ if block_given?
23
+ @body.__send__(*args, &Proc.new)
24
+ else
25
+ @body.__send__(*args)
26
+ end
27
+ end
28
+
29
+ def close
30
+ return if @closed
31
+ @closed = true
32
+
33
+ begin
34
+ @body.close if @body.respond_to? :close
35
+ ensure
36
+ @block.call
37
+ end
38
+ end
39
+
40
+ def closed?
41
+ @closed
42
+ end
43
+
44
+ def each(*args)
45
+ if block_given?
46
+ @body.each(*args, &Proc.new)
47
+ else
48
+ @body.each(*args)
49
+ end
50
+ end
51
+ end
52
+
53
+ def initialize(app)
54
+ @app = app
55
+ end
56
+
57
+ # rubocop:disable Metrics/MethodLength
58
+ def call(env)
59
+ origin = env.fetch('HTTP_X_MNEMOSYNE_ORIGIN', false)
60
+ transaction = env.fetch('HTTP_X_MNEMOSYNE_TRANSACTION') do
61
+ ::SecureRandom.uuid
62
+ end
63
+
64
+ trace = ::Mnemosyne::Instrumenter.trace 'app.web.request.rack',
65
+ transaction: transaction,
66
+ origin: origin
67
+
68
+ if trace
69
+ trace.start!
70
+
71
+ response = @app.call env
72
+ response[2] = Proxy.new(response[2]) { trace.submit }
73
+ response
74
+ else
75
+ @app.call env
76
+ end
77
+
78
+ # rubocop:disable Lint/RescueException
79
+ rescue Exception
80
+ trace.submit if trace
81
+ raise
82
+ ensure
83
+ trace.release if trace
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def _uuid
90
+ ::SecureRandom.uuid
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ module Middleware
5
+ module Restify
6
+ class Writer
7
+ def initialize(writer, trace, span)
8
+ @writer = writer
9
+ @trace = trace
10
+ @span = span
11
+ end
12
+
13
+ def fulfill(*args)
14
+ @span.finish!
15
+ @trace << @span
16
+
17
+ @writer.fulfill(*args)
18
+ end
19
+
20
+ def reject(*args)
21
+ @span.finish!
22
+ @trace << @span
23
+
24
+ @writer.reject(*args)
25
+ end
26
+ end
27
+
28
+ class << self
29
+ # rubocop:disable Metrics/MethodLength
30
+ def call(request, writer)
31
+ if (trace = ::Mnemosyne::Instrumenter.current_trace)
32
+ meta = {url: request.uri, method: request.method}
33
+
34
+ span = ::Mnemosyne::Span.new('external.http.restify', meta: meta)
35
+ span.start!
36
+
37
+ request.headers['X-Mnemosyne-Transaction'] = trace.transaction
38
+ request.headers['X-Mnemosyne-Origin'] = span.uuid
39
+
40
+ hook = Writer.new(writer, trace, span)
41
+
42
+ yield request, hook
43
+ else
44
+ yield request, writer
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ class Probe
5
+ # rubocop:disable Metrics/MethodLength
6
+ def install
7
+ setup
8
+
9
+ self.class.subscriptions.each do |subscribe|
10
+ ::ActiveSupport::Notifications.subscribe(subscribe) do |*args|
11
+ trace = ::Mnemosyne::Instrumenter.current_trace
12
+ next unless trace
13
+
14
+ call(trace, *args)
15
+ end
16
+ end
17
+
18
+ ::Mnemosyne::Instrumenter.logger.debug do
19
+ "Installed probe #{self.class.name}"
20
+ end
21
+ end
22
+
23
+ def setup
24
+ # noop
25
+ end
26
+
27
+ class << self
28
+ def subscriptions
29
+ @subscriptions ||= Set.new
30
+ end
31
+
32
+ def subscribe(name)
33
+ subscriptions << name
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mnemosyne
4
+ module Probes
5
+ module Acfs
6
+ module Request
7
+ class Probe < ::Mnemosyne::Probe
8
+ subscribe 'acfs.run'
9
+
10
+ def setup
11
+ require 'mnemosyne/middleware/acfs'
12
+
13
+ ::Acfs::Runner.use ::Mnemosyne::Middleware::Acfs
14
+ end
15
+
16
+ # rubocop:disable Metrics/ParameterLists
17
+ def call(trace, _name, start, finish, _id, _payload)
18
+ start = ::Mnemosyne::Clock.to_tick(start)
19
+ finish = ::Mnemosyne::Clock.to_tick(finish)
20
+
21
+ callers = caller
22
+
23
+ callers.shift until callers[0].include? 'lib/acfs/global.rb:'
24
+
25
+ meta = {
26
+ backtrace: callers[1..-1]
27
+ }
28
+
29
+ span = ::Mnemosyne::Span.new 'external.run.acfs',
30
+ start: start, finish: finish, meta: meta
31
+
32
+ trace << span
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ register 'Acfs::Runner', 'acfs/runner', Acfs::Request::Probe.new
39
+ end
40
+ end