heimdall_apm 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0f1d534cb7c6454c3d80e42c5a5d6c43eb399fe4
4
+ data.tar.gz: 50874cb30661dc750183cc6aa0694bbc78db3cd2
5
+ SHA512:
6
+ metadata.gz: 701c5cb8ef98c9c27c5d368cec5bddcc90b6627a7d676887424f4b6277e3ad13d337235815afa938660f5a4586a30ff9680cc4da1fe4dd7e2b5069a8e0eb8694
7
+ data.tar.gz: 4710a3e60d589f3d5582fbe989d0de66e50da4dbb5c92a78be225c7f90bee37567bee4425fde4a7655ce294089d3ff905443833991a318c0c19f13d68f219db2
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ # misc
2
+ .DS_Store
3
+
4
+ # Ruby
5
+ .bundle/
6
+ .yardoc
7
+ coverage/
8
+ doc/
9
+ tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.2
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ # git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in heimdall.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,23 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ heimdall_apm (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.11.3)
10
+ rake (10.5.0)
11
+
12
+ PLATFORMS
13
+ ruby
14
+ x86_64-darwin-16
15
+
16
+ DEPENDENCIES
17
+ bundler (~> 1.16)
18
+ heimdall_apm!
19
+ minitest (~> 5.0)
20
+ rake (~> 10.0)
21
+
22
+ BUNDLED WITH
23
+ 1.16.1
data/LICENSE ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # Heimdall APM
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,25 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "heimdall_apm/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "heimdall_apm"
7
+ spec.version = HeimdallApm::VERSION
8
+ spec.authors = ["Christopher Cocchi-Perrier"]
9
+ spec.email = ["cocchi.c@gmail.com"]
10
+ spec.license = 'LGPL-3.0'
11
+
12
+ spec.summary = "Open source monitoring for Rails applications"
13
+ spec.homepage = "https://github.com/ccocchi/heimdall"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.start_with?('test', 'bin')
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.16"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "minitest", "~> 5.0"
25
+ end
@@ -0,0 +1,96 @@
1
+ require 'heimdall_apm/agent_context'
2
+ require 'heimdall_apm/reporting'
3
+
4
+ module HeimdallApm
5
+ # Main entry point for HeimdallApm. Only one instance is created per ruby
6
+ # process, and it manages the lifecycle of the monitoring
7
+ #
8
+ class Agent
9
+ DEFAULT_PUSH_INTERVAL = 60
10
+
11
+ @@instance = nil
12
+
13
+ def self.instance(opts = {})
14
+ @@instance ||= self.new(opts)
15
+ end
16
+
17
+ attr_reader :options
18
+
19
+ attr_reader :context
20
+
21
+ def initialize(opts)
22
+ @options = opts
23
+ @context = ::HeimdallApm::AgentContext.new
24
+ @background_thread = nil
25
+ @stopped = false
26
+ end
27
+
28
+ def install(options = {})
29
+ context.config = ::HeimdallApm::Config.new
30
+
31
+ if context.interactive?
32
+ HeimdallApm.logger.info 'Preventing agent to start in interactive mode'
33
+ return
34
+ end
35
+
36
+ if defined?(Sidekiq) && Sidekiq.server?
37
+ # TODO: handle custom instrumentation disabling
38
+ HeimdallApm.logger.info 'Preventing agent to start in sidekiq server'
39
+ return
40
+ end
41
+
42
+ start(options)
43
+ end
44
+
45
+ def start(options = {})
46
+ return unless context.config.value('enabled')
47
+
48
+ @background_thread = Thread.new { background_run }
49
+
50
+ # TODO: use instruments manager
51
+ require 'heimdall_apm/instruments/active_record' if defined?(ActiveRecord)
52
+ require 'heimdall_apm/instruments/action_controller' if defined?(ActionController)
53
+ require 'heimdall_apm/instruments/elasticsearch' if defined?(Elasticsearch)
54
+
55
+ if (options[:app])
56
+ require 'heimdall_apm/instruments/middleware'
57
+ # TODO: make the position configurable
58
+ options[:app].config.middleware.insert_after Rack::Cors, HeimdallApm::Instruments::Middleware
59
+ end
60
+ end
61
+
62
+ def stop
63
+ @stopped = true
64
+ if @background_thread.alive?
65
+ @background_thread.wakeup
66
+ @background_thread.join
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def background_run
73
+ HeimdallApm.logger.info "Start background thread"
74
+ reporting = ::HeimdallApm::Reporting.new(@context)
75
+ next_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + DEFAULT_PUSH_INTERVAL
76
+
77
+ loop do
78
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
+
80
+ break if @stopped
81
+
82
+ if now < next_time
83
+ remaining = next_time - now
84
+ HeimdallApm.logger.debug "Sleeping for #{remaining}"
85
+ sleep(remaining)
86
+ next
87
+ end
88
+
89
+ reporting.call
90
+ next_time = now + DEFAULT_PUSH_INTERVAL
91
+ end
92
+ rescue => e
93
+ HeimdallApm.logger.error e.message
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,30 @@
1
+ require 'heimdall_apm/vault'
2
+ require 'heimdall_apm/recorder'
3
+ require 'heimdall_apm/config'
4
+
5
+ module HeimdallApm
6
+ # Global context in which the agent is run. One context is assigned per
7
+ # agent. It contains most of the part that are going to be accessed globally
8
+ # by the rest of the monitoring.
9
+ #
10
+ class AgentContext
11
+ # Global configuration object
12
+ attr_writer :config
13
+
14
+ def config
15
+ @config ||= ::HeimdallApm::Config.new
16
+ end
17
+
18
+ def vault
19
+ @vault ||= ::HeimdallApm::Vault.new(self)
20
+ end
21
+
22
+ def recorder
23
+ @recorder ||= ::HeimdallApm::Recorder.new
24
+ end
25
+
26
+ def interactive?
27
+ defined?(::Rails::Console) && $stdout.isatty && $stdin.isatty
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module HeimdallApm
2
+ class Config
3
+ def initialize
4
+ @loaded = nil
5
+ load_default_config
6
+ end
7
+
8
+ def value(key)
9
+ # TODO: handle empty strings keys or boolean passed as strings
10
+ @loaded && @settings[key]
11
+ end
12
+ alias_method :[], :value
13
+
14
+ def has_key?(key)
15
+ @settings.key?(key)
16
+ end
17
+ alias_method :key?, :has_key?
18
+
19
+ private
20
+
21
+ def load_default_config
22
+ @settings = Rails.application.config_for(:heimdall_apm)
23
+ @loaded = true
24
+ rescue
25
+ # TODO: handle no configuration file found
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module HeimdallApm
2
+ module ActionController
3
+ class Subscriber
4
+ def start(name, id, payload)
5
+ txn = ::HeimdallApm::TransactionManager.current
6
+ scope = -"#{payload[:controller]}##{payload[:action]}"
7
+ segment = ::HeimdallApm::Segment.new('Controller'.freeze, scope)
8
+
9
+ txn.scope = scope unless txn.scope
10
+ txn.start_segment(segment)
11
+ end
12
+
13
+ def finish(name, id, payload)
14
+ txn = ::HeimdallApm::TransactionManager.current
15
+ txn.stop_segment
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ ActiveSupport::Notifications.subscribe(
22
+ 'process_action.action_controller',
23
+ ::HeimdallApm::ActionController::Subscriber.new
24
+ )
@@ -0,0 +1,23 @@
1
+ module HeimdallApm
2
+ module ActiveRecord
3
+ class Subscriber
4
+ def start(name, id, payload)
5
+ txn = ::HeimdallApm::TransactionManager.current
6
+ segment = ::HeimdallApm::Segment.new('Sql'.freeze, name)
7
+ segment.data = payload[:sql]
8
+
9
+ txn.start_segment(segment)
10
+ end
11
+
12
+ def finish(name, id, payload)
13
+ txn = ::HeimdallApm::TransactionManager.current
14
+ txn.stop_segment
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ ActiveSupport::Notifications.subscribe(
21
+ 'sql.active_record',
22
+ ::HeimdallApm::ActiveRecord::Subscriber.new
23
+ )
@@ -0,0 +1,23 @@
1
+ module HeimdallApm
2
+ module Elasticsearch
3
+ class Subscriber
4
+ def start(name, id, payload)
5
+ txn = ::HeimdallApm::TransactionManager.current
6
+ segment = ::HeimdallApm::Segment.new('Elastic'.freeze, name)
7
+ segment.data = payload[:search]
8
+
9
+ txn.start_segment(segment)
10
+ end
11
+
12
+ def finish(name, id, payload)
13
+ txn = ::HeimdallApm::TransactionManager.current
14
+ txn.stop_segment
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ ActiveSupport::Notifications.subscribe(
21
+ 'search.elasticsearch',
22
+ ::HeimdallApm::Elasticsearch::Subscriber.new
23
+ )
@@ -0,0 +1,18 @@
1
+ module HeimdallApm
2
+ module Instruments
3
+ class Middleware
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ txn = ::HeimdallApm::TransactionManager.current
10
+ segment = ::HeimdallApm::Segment.new('Middleware'.freeze, 'all'.freeze)
11
+ txn.start_segment(segment)
12
+ @app.call(env)
13
+ ensure
14
+ txn.stop_segment
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module HeimdallApm
2
+ # Metric name used in visitor's metrics hash
3
+ #
4
+ class MetricName
5
+ attr_reader :type, :name, :scope
6
+
7
+ def initialize(type, name, scope = nil)
8
+ @type = type
9
+ @name = name
10
+ @scope = scope
11
+ end
12
+
13
+ def hash
14
+ h = type.hash ^ name.hash
15
+ h ^= scope.hash if scope
16
+ h
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module HeimdallApm
2
+ # Stats associated with a single metric (used in metrics Hashs as value where
3
+ # keys are the metrics names)
4
+ #
5
+ class MetricStats
6
+ attr_accessor :call_count
7
+ attr_accessor :total_call_time
8
+ attr_accessor :total_exclusive_time
9
+ attr_accessor :min_call_time
10
+ attr_accessor :max_call_time
11
+
12
+ # If this metric is scoped inside another, use exclusive time for min/max.
13
+ # Non-scoped metrics (like Controller) track the total call time.
14
+ def initialize(scoped: false)
15
+ @scoped = scoped
16
+ @call_count = 0
17
+ @total_call_time = 0.0
18
+ @total_exclusive_time = 0.0
19
+ @min_call_time = 0.0
20
+ @max_call_time = 0.0
21
+ end
22
+
23
+ def update(call_time, exclusive_time = nil)
24
+ self.call_count += 1
25
+ self.total_call_time += call_time
26
+ self.total_exclusive_time += exclusive_time
27
+
28
+ t = @scoped ? exclusive_time : call_time
29
+ self.min_call_time = t if call_count == 0 || t < min_call_time
30
+ self.max_call_time = t if t > max_call_time
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module HeimdallApm
6
+ # Convert metrics hash from requests into an collection of points we want to
7
+ # track, but without aggregations and percentiles/std_dev calculations of same
8
+ # endpoints across multiples requests. These operations are deferred to
9
+ # InfluxDB, in favor of more granular data.
10
+ # This may change in the future if it proves non scalable.
11
+ #
12
+ class PointsCollection
13
+ # Metrics we want to explicitely keep separated into measurements. Everything
14
+ # else will be label as Ruby.
15
+ ROOT_METRICS = Set.new(['Sql', 'Elastic', 'Redis'])
16
+
17
+ def initialize
18
+ @points = []
19
+ end
20
+
21
+ def empty?
22
+ @points.empty?
23
+ end
24
+
25
+ def to_a
26
+ @points
27
+ end
28
+
29
+ def append(txn, metrics)
30
+ timestamp = txn.root_segment.stop_time
31
+ series_name = txn.custom_series_name || (txn.web? ? 'app' : 'job')
32
+ values = Hash.new { |h, k| h[k] = 0 }
33
+
34
+ tags = txn.tags || {}
35
+ tags[:endpoint] = txn.scope
36
+
37
+ metrics.each do |meta, stat|
38
+ if ROOT_METRICS.include?(meta.type)
39
+ key = -"#{meta.type.downcase}_time"
40
+ values[key] += stat.total_exclusive_time
41
+ else
42
+ values['ruby_time'] += stat.total_exclusive_time
43
+ end
44
+
45
+ values['total_time'] += stat.total_exclusive_time
46
+ end
47
+
48
+ # Segment time are in seconds, store them in milliseconds
49
+ values.transform_values! { |v| v * 1000 }
50
+
51
+ @points << {
52
+ series: series_name,
53
+ timestamp: (timestamp * 1000).to_i,
54
+ tags: tags,
55
+ values: values
56
+ }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,25 @@
1
+ module HeimdallApm
2
+ # Provides helpers for custom instrumentation of code
3
+ #
4
+ # include Probe
5
+ # instrument('Elastic', 'profiles#search') do ... end
6
+ #
7
+ module Probe
8
+ # Insruments block passed to the method into the current transaction.
9
+ #
10
+ # @param type Segment type (i.e 'ActiveRecord' or similar)
11
+ # @param name Specific name for the segment
12
+ #
13
+ def instrument(type, name, opts = {})
14
+ txn = ::HeimdallApm::TransactionManager.current
15
+ segment = ::HeimdallApm::Segment.new(type, name)
16
+ txn.start_segment(segment)
17
+
18
+ # TODO: maybe yield the segment here to have the block pass additional
19
+ # informations
20
+ yield
21
+ ensure
22
+ txn.stop_segment
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails/railtie'
2
+
3
+ module HeimdallApm
4
+ class Railtie < Rails::Railtie
5
+ initializer 'heimdall_apm.install' do |app|
6
+ HeimdallApm::Agent.instance.install(app: app)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ module HeimdallApm
2
+ # TODO: Maybe not needed if we keep transaction logic within visitors
3
+ class Recorder
4
+ def record(request)
5
+ request.record
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ module HeimdallApm
2
+ class Reporting
3
+ def initialize(context)
4
+ @context = context
5
+ end
6
+
7
+ def influx
8
+ @client ||= InfluxDB::Client.new("#{Rails.env}_metrics", time_precision: 'ms', retry: 0)
9
+ end
10
+
11
+ def call
12
+ span = @context.vault.retrieve_and_delete_previous_span
13
+ if span && !span.points_collection.empty?
14
+ influx.write_points(span.points_collection.to_a)
15
+ else
16
+ HeimdallApm.logger.debug "Nothing to report"
17
+ end
18
+ rescue => e
19
+ HeimdallApm.logger.error "#{e.message} during reporting to InfluxDB"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,68 @@
1
+ module HeimdallApm
2
+ class Segment
3
+ # Generic type of the thing being tracked
4
+ # Examples: "ActiveRecord", "SQL", "Controller"
5
+ attr_reader :type
6
+
7
+ # More specific name of the item
8
+ # Examples: "User#find", "find_by_sql", "users#index"
9
+ attr_reader :name
10
+
11
+ # Start and stop of this segment
12
+ attr_reader :start_time, :stop_time
13
+
14
+ # Additional data linked to the segment (for example SQL or Elastic queries).
15
+ # Can be left nil.
16
+ attr_accessor :data
17
+
18
+ def initialize(type, name, start_time = nil)
19
+ @type = type
20
+ @name = name
21
+ @start_time = start_time
22
+ @children = nil
23
+ end
24
+
25
+ def start
26
+ @start_time = Process.clock_gettime(Process::CLOCK_REALTIME)
27
+ end
28
+
29
+ # Lazy initialization of children to avoid bloating leaf segments
30
+ def children
31
+ @children ||= []
32
+ end
33
+
34
+ def add_child(segment)
35
+ children << segment
36
+ end
37
+
38
+ # Entry point for visitors depth-first style: start by visiting `self` then
39
+ # visit all of its children
40
+ def accept(visitor)
41
+ visitor.visit(self)
42
+ if @children
43
+ visitor.before_children if visitor.respond_to?(:before_children)
44
+ @children.each { |c| c.accept(visitor) }
45
+ visitor.after_children if visitor.respond_to?(:after_children)
46
+ end
47
+ end
48
+
49
+ def record_stop_time
50
+ @stop_time = Process.clock_gettime(Process::CLOCK_REALTIME)
51
+ end
52
+
53
+ def total_call_time
54
+ @total_call_time ||= stop_time - start_time
55
+ end
56
+
57
+ def total_exclusive_time
58
+ return total_call_time unless @children
59
+ total_call_time - children_call_time
60
+ end
61
+
62
+ private
63
+
64
+ def children_call_time
65
+ children.map(&:total_call_time).sum
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,129 @@
1
+ require 'heimdall_apm/visitors/request_metrics_visitor'
2
+ require 'heimdall_apm/visitors/pretty_print_visitor'
3
+
4
+ module HeimdallApm
5
+ # A TrackedTransaction is a collection of segments.
6
+ #
7
+ class TrackedTransaction
8
+
9
+ WEB_MODE = 1
10
+ JOB_MODE = 2
11
+
12
+ # First segment added to the transaction
13
+ attr_reader :root_segment
14
+
15
+ # Recorder used to process transaction data
16
+ attr_reader :recorder
17
+
18
+ # Scope of this transaction (controller routes / job id)
19
+ attr_accessor :scope
20
+
21
+ # Miscellaneous annotations made to the transaction. Must be a Hash.
22
+ attr_reader :annotations
23
+
24
+ def initialize(context)
25
+ @context = context
26
+ @root_segment = nil
27
+ @segments = []
28
+ @scope = nil
29
+ @stopped = false
30
+ @mode = nil
31
+ @annotations = {}
32
+
33
+ @recorder = context.recorder
34
+ @vault = context.vault
35
+ end
36
+
37
+ def start_segment(segment)
38
+ @root_segment = segment unless @root_segment
39
+ @segments.push(segment)
40
+
41
+ # TODO: maybe use a visitor to check that at the end of the request intead
42
+ @mode ||=
43
+ case segment.type
44
+ when 'Controller' then WEB_MODE
45
+ when 'Job' then JOB_MODE
46
+ else
47
+ nil
48
+ end
49
+
50
+ segment.start
51
+ end
52
+
53
+ def stop_segment
54
+ segment = @segments.pop
55
+ segment.record_stop_time
56
+
57
+ if finalized?
58
+ stop_request
59
+ else
60
+ @segments[-1].add_child(segment)
61
+ end
62
+ end
63
+
64
+ # Grab the currently running segment. Will be `nil` for a finalized
65
+ # transaction
66
+ def current_segment
67
+ @segments[-1]
68
+ end
69
+
70
+ VISITORS = {
71
+ metrics: ::HeimdallApm::Visitors::RequestMetricsVisitor
72
+ }
73
+
74
+ def record
75
+ return unless root_segment
76
+
77
+ VISITORS.each do |_, klass|
78
+ visitor = klass.new(@vault, self)
79
+ root_segment.accept(visitor)
80
+ visitor.store_in_vault
81
+ end
82
+ end
83
+
84
+ def stopped?
85
+ @stopped
86
+ end
87
+
88
+ def annotate(hsh)
89
+ @annotations.merge!(hsh)
90
+ end
91
+
92
+ # Allows InfluxDB's series name to be customize via annotations
93
+ def custom_series_name
94
+ annotations[:series_name]
95
+ end
96
+
97
+ def tags
98
+ annotations[:tags]
99
+ end
100
+
101
+ def web?
102
+ @mode == WEB_MODE
103
+ end
104
+
105
+ def job?
106
+ @mode == JOB_MODE
107
+ end
108
+
109
+ private
110
+
111
+ def pretty_print
112
+ visitor = HeimdallApm::Visitors::PrettyPrintVisitor.new(@scope)
113
+ root_segment.accept(visitor)
114
+ visitor.store_in_vault
115
+ end
116
+
117
+ # Send the request off to be stored
118
+ def stop_request
119
+ @stopped = true
120
+ recorder.record(self) if recorder
121
+ end
122
+
123
+ # Are we finished with this transaction, i.e. no layers are left to be
124
+ # popped out
125
+ def finalized?
126
+ @segments.empty?
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,27 @@
1
+ require 'heimdall_apm/tracked_transaction'
2
+
3
+ module HeimdallApm
4
+ # Handles the thread-local variable holding the current tracked transaction,
5
+ # populating it the first time it is accessed.
6
+ #
7
+ class TransactionManager
8
+ def self.current
9
+ find || create
10
+ end
11
+
12
+ def self.find
13
+ req = Thread.current[:heimdall_request]
14
+
15
+ if !req || req.stopped?
16
+ nil
17
+ else
18
+ req
19
+ end
20
+ end
21
+
22
+ def self.create
23
+ context = Agent.instance.context
24
+ Thread.current[:heimdall_request] = ::HeimdallApm::TrackedTransaction.new(context)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ require 'thread'
2
+ require 'heimdall_apm/points_collection'
3
+
4
+ module HeimdallApm
5
+ # Keeps in RAM one or more minute's worth of metrics.
6
+ # When informed to by the background thread, it pushes the in-RAM metrics off
7
+ # to InfluxDB.
8
+ class Vault
9
+ def initialize(context)
10
+ @context = context
11
+ @lock = Mutex.new
12
+ @spans = Hash.new { |h, k| h[k] = Span.new(k, @context) }
13
+ end
14
+
15
+ def current_span
16
+ @spans[current_timestamp]
17
+ end
18
+
19
+ def retrieve_and_delete_previous_span
20
+ timestamp = current_timestamp - 60
21
+ @lock.synchronize { @spans.delete(timestamp) }
22
+ end
23
+
24
+ def store_transaction_metrics(txn, metrics)
25
+ @lock.synchronize { current_span.add_point(txn, metrics) }
26
+ end
27
+
28
+ def current_timestamp
29
+ time = Time.now.utc
30
+ time.to_i - time.sec
31
+ end
32
+ end
33
+
34
+ # One span of storage
35
+ class Span
36
+ attr_reader :points_collection
37
+
38
+ def initialize(timestamp, context)
39
+ @timestamp = timestamp
40
+ @context = context
41
+
42
+ @points_collection = ::HeimdallApm::PointsCollection.new
43
+ end
44
+
45
+ def add_point(txn, metrics)
46
+ @points_collection.append(txn, metrics)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ module HeimdallApm
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,42 @@
1
+ module HeimdallApm
2
+ module Visitors
3
+ class PrettyPrintVisitor
4
+ def initialize(scope)
5
+ @indent = 0
6
+ @scope = scope
7
+
8
+ @io = File.open('log/heimdall_apm.log', 'ab')
9
+ at_exit { @io.close }
10
+
11
+ pprint("Request #{@scope}:\n")
12
+ end
13
+
14
+ def before_children
15
+ @indent += 2
16
+ end
17
+
18
+ def after_children
19
+ @indent -= 2
20
+ end
21
+
22
+ def visit(segment)
23
+ pprint("#{segment.type}/#{segment.name}\n")
24
+ @indent += 2
25
+ pprint("duration=#{segment.total_exclusive_time}ms\n")
26
+ @indent -= 2
27
+ end
28
+
29
+ def store_in_vault
30
+ @io.flush
31
+ end
32
+
33
+ private
34
+
35
+ def pprint(str)
36
+ @io.write(' ' * @indent) if @indent > 0
37
+ @io.write(str)
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ require 'heimdall_apm/metric_name'
2
+ require 'heimdall_apm/metric_stats'
3
+
4
+ module HeimdallApm
5
+ module Visitors
6
+ # Extract metrics for a given transaction
7
+ #
8
+ class RequestMetricsVisitor
9
+ attr_reader :metrics
10
+
11
+ def initialize(vault, transaction)
12
+ @transaction = transaction
13
+ @vault = vault
14
+ @metrics = {}
15
+ end
16
+
17
+ def visit(segment)
18
+ name = ::HeimdallApm::MetricName.new(segment.type, segment.name)
19
+ @metrics[name] ||= ::HeimdallApm::MetricStats.new
20
+
21
+ stat = @metrics[name]
22
+ stat.update(segment.total_call_time, segment.total_exclusive_time)
23
+ end
24
+
25
+ def store_in_vault
26
+ timestamp = @transaction.root_segment.stop_time
27
+ @vault.store_transaction_metrics(@transaction, metrics)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ require 'heimdall_apm/version'
2
+ require 'heimdall_apm/segment'
3
+ require 'heimdall_apm/transaction_manager'
4
+ require 'heimdall_apm/probe'
5
+ require 'heimdall_apm/agent'
6
+ require 'heimdall_apm/railtie' if defined?(Rails)
7
+
8
+ require 'logger'
9
+
10
+ module HeimdallApm
11
+ def self.logger
12
+ @logger ||= Logger.new('log/heimdall_apm.log')
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heimdall_apm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher Cocchi-Perrier
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-07-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description:
56
+ email:
57
+ - cocchi.c@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - heimdall_apm.gemspec
70
+ - lib/heimdall_apm.rb
71
+ - lib/heimdall_apm/agent.rb
72
+ - lib/heimdall_apm/agent_context.rb
73
+ - lib/heimdall_apm/config.rb
74
+ - lib/heimdall_apm/instruments/action_controller.rb
75
+ - lib/heimdall_apm/instruments/active_record.rb
76
+ - lib/heimdall_apm/instruments/elasticsearch.rb
77
+ - lib/heimdall_apm/instruments/middleware.rb
78
+ - lib/heimdall_apm/metric_name.rb
79
+ - lib/heimdall_apm/metric_stats.rb
80
+ - lib/heimdall_apm/points_collection.rb
81
+ - lib/heimdall_apm/probe.rb
82
+ - lib/heimdall_apm/railtie.rb
83
+ - lib/heimdall_apm/recorder.rb
84
+ - lib/heimdall_apm/reporting.rb
85
+ - lib/heimdall_apm/segment.rb
86
+ - lib/heimdall_apm/tracked_transaction.rb
87
+ - lib/heimdall_apm/transaction_manager.rb
88
+ - lib/heimdall_apm/vault.rb
89
+ - lib/heimdall_apm/version.rb
90
+ - lib/heimdall_apm/visitors/pretty_print_visitor.rb
91
+ - lib/heimdall_apm/visitors/request_metrics_visitor.rb
92
+ homepage: https://github.com/ccocchi/heimdall
93
+ licenses:
94
+ - LGPL-3.0
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.6.13
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Open source monitoring for Rails applications
116
+ test_files: []