elastic-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.

Potentially problematic release.


This version of elastic-apm might be problematic. Click here for more details.

Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +47 -0
  5. data/.travis.yml +5 -0
  6. data/CODE_OF_CONDUCT.md +47 -0
  7. data/Gemfile +38 -0
  8. data/LICENSE +201 -0
  9. data/README.md +55 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/bin/with_framework +7 -0
  14. data/elastic-apm.gemspec +22 -0
  15. data/lib/elastic-apm.rb +4 -0
  16. data/lib/elastic_apm.rb +92 -0
  17. data/lib/elastic_apm/agent.rb +164 -0
  18. data/lib/elastic_apm/config.rb +124 -0
  19. data/lib/elastic_apm/error.rb +21 -0
  20. data/lib/elastic_apm/error/context.rb +119 -0
  21. data/lib/elastic_apm/error/exception.rb +37 -0
  22. data/lib/elastic_apm/error/log.rb +24 -0
  23. data/lib/elastic_apm/error_builder.rb +40 -0
  24. data/lib/elastic_apm/http.rb +103 -0
  25. data/lib/elastic_apm/injectors.rb +71 -0
  26. data/lib/elastic_apm/injectors/action_dispatch.rb +26 -0
  27. data/lib/elastic_apm/injectors/json.rb +22 -0
  28. data/lib/elastic_apm/injectors/net_http.rb +50 -0
  29. data/lib/elastic_apm/injectors/redis.rb +33 -0
  30. data/lib/elastic_apm/injectors/sequel.rb +45 -0
  31. data/lib/elastic_apm/injectors/sinatra.rb +41 -0
  32. data/lib/elastic_apm/injectors/tilt.rb +27 -0
  33. data/lib/elastic_apm/instrumenter.rb +112 -0
  34. data/lib/elastic_apm/internal_error.rb +5 -0
  35. data/lib/elastic_apm/log.rb +47 -0
  36. data/lib/elastic_apm/middleware.rb +30 -0
  37. data/lib/elastic_apm/normalizers.rb +63 -0
  38. data/lib/elastic_apm/normalizers/action_controller.rb +24 -0
  39. data/lib/elastic_apm/normalizers/action_view.rb +72 -0
  40. data/lib/elastic_apm/normalizers/active_record.rb +41 -0
  41. data/lib/elastic_apm/railtie.rb +43 -0
  42. data/lib/elastic_apm/serializers.rb +26 -0
  43. data/lib/elastic_apm/serializers/errors.rb +40 -0
  44. data/lib/elastic_apm/serializers/transactions.rb +36 -0
  45. data/lib/elastic_apm/service_info.rb +66 -0
  46. data/lib/elastic_apm/span.rb +51 -0
  47. data/lib/elastic_apm/span/context.rb +20 -0
  48. data/lib/elastic_apm/span_helpers.rb +37 -0
  49. data/lib/elastic_apm/sql_summarizer.rb +26 -0
  50. data/lib/elastic_apm/stacktrace.rb +84 -0
  51. data/lib/elastic_apm/stacktrace/frame.rb +62 -0
  52. data/lib/elastic_apm/subscriber.rb +72 -0
  53. data/lib/elastic_apm/system_info.rb +30 -0
  54. data/lib/elastic_apm/transaction.rb +92 -0
  55. data/lib/elastic_apm/util.rb +20 -0
  56. data/lib/elastic_apm/util/inspector.rb +61 -0
  57. data/lib/elastic_apm/version.rb +5 -0
  58. data/lib/elastic_apm/worker.rb +48 -0
  59. data/vendor/.gitkeep +0 -0
  60. metadata +116 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ class SqlSummarizer
6
+ REGEXES = {
7
+ /^SELECT .* FROM ([^ ]+)/i => 'SELECT FROM ',
8
+ /^INSERT INTO ([^ ]+)/i => 'INSERT INTO ',
9
+ /^UPDATE ([^ ]+)/i => 'UPDATE ',
10
+ /^DELETE FROM ([^ ]+)/i => 'DELETE FROM '
11
+ }.freeze
12
+
13
+ def self.cache
14
+ @cache ||= {}
15
+ end
16
+
17
+ def summarize(sql)
18
+ self.class.cache[sql] ||=
19
+ REGEXES.find do |regex, sig|
20
+ if (match = sql.match(regex))
21
+ break format("#{sig}#{match[1]}")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elastic_apm/stacktrace/frame'
4
+
5
+ module ElasticAPM
6
+ # @api private
7
+ class Stacktrace
8
+ def initialize(backtrace)
9
+ @backtrace = backtrace
10
+ end
11
+
12
+ attr_reader :frames
13
+
14
+ def self.build(builder, backtrace)
15
+ return nil unless backtrace
16
+
17
+ stack = new(backtrace)
18
+ stack.build_frames(builder)
19
+ stack
20
+ end
21
+
22
+ def build_frames(builder)
23
+ @frames = @backtrace.reverse.map do |line|
24
+ build_frame(builder, line)
25
+ end
26
+ end
27
+
28
+ def length
29
+ frames.length
30
+ end
31
+
32
+ def to_a
33
+ frames.map(&:to_h)
34
+ end
35
+
36
+ private
37
+
38
+ JAVA_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/
39
+ RUBY_FORMAT = /^(.+?):(\d+)(?::in `(.+?)')?$/
40
+
41
+ def parse_line(line)
42
+ ruby_match = line.match(RUBY_FORMAT)
43
+
44
+ if ruby_match
45
+ _, file, number, method = ruby_match.to_a
46
+ file.sub!(/\.class$/, '.rb')
47
+ module_name = nil
48
+ else
49
+ java_match = line.match(JAVA_FORMAT)
50
+ _, module_name, method, file, number = java_match.to_a
51
+ end
52
+
53
+ [file, number, method, module_name]
54
+ end
55
+
56
+ def build_frame(_builder, line)
57
+ abs_path, lineno, function, _module_name = parse_line(line)
58
+
59
+ frame = Frame.new
60
+ frame.abs_path = abs_path
61
+ frame.filename = strip_load_path(abs_path)
62
+ frame.function = function
63
+ frame.lineno = lineno.to_i
64
+ frame.build_context 3
65
+
66
+ frame
67
+ end
68
+
69
+ def strip_load_path(path)
70
+ return nil unless path
71
+
72
+ prefix =
73
+ $LOAD_PATH
74
+ .map(&:to_s)
75
+ .select { |s| path.start_with?(s) }
76
+ .sort_by(&:length)
77
+ .last
78
+
79
+ return path unless prefix
80
+
81
+ path[prefix.chomp(File::SEPARATOR).length + 1..-1]
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ class Stacktrace
5
+ # @api private
6
+ class Frame
7
+ attr_accessor(
8
+ :abs_path,
9
+ :filename,
10
+ :function,
11
+ :vars,
12
+ :pre_context,
13
+ :context_line,
14
+ :post_context,
15
+ :in_app,
16
+ :lineno,
17
+ :module,
18
+ :colno
19
+ )
20
+
21
+ # rubocop:disable Metrics/AbcSize
22
+ def build_context(context_line_count)
23
+ return unless abs_path
24
+
25
+ file_lines = [nil] + read_lines(abs_path)
26
+
27
+ self.context_line = file_lines[lineno]
28
+ self.pre_context =
29
+ file_lines[(lineno - context_line_count - 1)...lineno]
30
+ self.post_context =
31
+ file_lines[(lineno + 1)..(lineno + context_line_count)]
32
+ end
33
+ # rubocop:enable Metrics/AbcSize
34
+
35
+ # rubocop:disable Metrics/MethodLength
36
+ def to_h
37
+ {
38
+ abs_path: abs_path,
39
+ filename: filename,
40
+ function: function,
41
+ vars: vars,
42
+ pre_context: pre_context,
43
+ context_line: context_line,
44
+ post_context: post_context,
45
+ in_app: in_app,
46
+ lineno: lineno,
47
+ module: self.module,
48
+ coln: colno
49
+ }
50
+ end
51
+ # rubocop:enable Metrics/MethodLength
52
+
53
+ private
54
+
55
+ def read_lines(path)
56
+ File.readlines(path)
57
+ rescue Errno::ENOENT
58
+ []
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/notifications'
4
+ require 'elastic_apm/normalizers'
5
+
6
+ module ElasticAPM
7
+ # @api private
8
+ class Subscriber
9
+ include Log
10
+
11
+ def initialize(agent)
12
+ @agent = agent
13
+ @config = agent.config
14
+ @normalizers = Normalizers.build(config)
15
+ end
16
+
17
+ attr_reader :config
18
+
19
+ def register!
20
+ unregister! if @subscription
21
+
22
+ @subscription =
23
+ ActiveSupport::Notifications.subscribe(notifications_regex, self)
24
+ end
25
+
26
+ def unregister!
27
+ ActiveSupport::Notifications.unsubscribe @subscription
28
+ @subscription = nil
29
+ end
30
+
31
+ # AS::Notifications API
32
+
33
+ Notification = Struct.new(:id, :span)
34
+
35
+ def start(name, id, payload)
36
+ # debug "AS::Notification#start:#{name}:#{id}"
37
+ return unless (transaction = @agent.current_transaction)
38
+
39
+ normalized = @normalizers.normalize(transaction, name, payload)
40
+
41
+ span =
42
+ if normalized == :skip
43
+ nil
44
+ else
45
+ name, type, context = normalized
46
+ transaction.span(name, type, context: context)
47
+ end
48
+
49
+ transaction.notifications << Notification.new(id, span)
50
+ end
51
+
52
+ def finish(_name, id, _payload)
53
+ # debug "AS::Notification#finish:#{name}:#{id}"
54
+ return unless (transaction = @agent.current_transaction)
55
+
56
+ while (notification = transaction.notifications.pop)
57
+ next unless notification.id == id
58
+
59
+ if (span = notification.span)
60
+ span.done
61
+ end
62
+ return
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def notifications_regex
69
+ @notifications_regex ||= /(#{@normalizers.keys.join('|')})/
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ class SystemInfo
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ attr_reader :config
11
+
12
+ def build
13
+ {
14
+ hostname: `hostname`,
15
+ architecture: platform.cpu,
16
+ platform: platform.os
17
+ }
18
+ end
19
+
20
+ def self.build(config)
21
+ new(config).build
22
+ end
23
+
24
+ private
25
+
26
+ def platform
27
+ @platform ||= Gem::Platform.local
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ class Transaction
6
+ def initialize(instrumenter, name, type = 'custom')
7
+ @instrumenter = instrumenter
8
+ @name = name
9
+ @type = type
10
+
11
+ @timestamp = Util.micros
12
+
13
+ @spans = []
14
+ @notifications = []
15
+ @span_id_ticker = -1
16
+
17
+ @notifications = [] # for AS::Notifications
18
+
19
+ yield self if block_given?
20
+ end
21
+
22
+ attr_accessor :name, :result, :type
23
+ attr_reader :duration, :root_span, :timestamp, :spans, :notifications
24
+
25
+ def release
26
+ @instrumenter.current_transaction = nil
27
+ end
28
+
29
+ def done(result = nil)
30
+ @result = result
31
+
32
+ @duration = Util.micros - @timestamp
33
+
34
+ self
35
+ end
36
+
37
+ def done?
38
+ !!(@result && @duration)
39
+ end
40
+
41
+ def submit(result = nil)
42
+ done result
43
+
44
+ release
45
+
46
+ @instrumenter.submit_transaction self
47
+
48
+ self
49
+ end
50
+
51
+ def running_spans
52
+ spans.select(&:running?)
53
+ end
54
+
55
+ def span(name, type = nil, context: nil)
56
+ span = next_span(name, type, context)
57
+ spans << span
58
+ span.start
59
+
60
+ return span unless block_given?
61
+
62
+ begin
63
+ result = yield span
64
+ ensure
65
+ span.done
66
+ end
67
+
68
+ result
69
+ end
70
+
71
+ def current_span
72
+ spans.reverse.lazy.find(&:running?)
73
+ end
74
+
75
+ private
76
+
77
+ def next_span_id
78
+ @span_id_ticker += 1
79
+ end
80
+
81
+ def next_span(name, type, context)
82
+ Span.new(
83
+ self,
84
+ next_span_id,
85
+ name,
86
+ type,
87
+ parent: current_span,
88
+ context: context
89
+ )
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ # @api private
5
+ module Util
6
+ def self.nearest_minute(target = Time.now.utc)
7
+ target - target.to_i % 60
8
+ end
9
+
10
+ def self.micros(target = Time.now.utc)
11
+ target.to_i * 1_000_000 + target.usec
12
+ end
13
+
14
+ def self.inspect_transaction(transaction)
15
+ Inspector.new.transaction transaction
16
+ end
17
+ end
18
+ end
19
+
20
+ require 'elastic_apm/util/inspector'
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ElasticAPM
4
+ module Util
5
+ # @api private
6
+ class Inspector
7
+ def initialize(width = 80)
8
+ @width = width
9
+ end
10
+
11
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
12
+ def transaction(transaction)
13
+ unless transaction.done?
14
+ raise ArgumentError, 'Transaction still running'
15
+ end
16
+
17
+ width_factor = @width.to_f / ms(transaction.duration)
18
+
19
+ lines = ['=' * @width]
20
+ lines << "[T] #{transaction.name} " \
21
+ "- #{transaction.type} (#{ms transaction.duration} ms)"
22
+ lines << "+#{'-' * (@width - 2)}+"
23
+
24
+ transaction.spans.each do |span|
25
+ indent = (ms(span.relative_start) * width_factor).to_i
26
+
27
+ if span.duration
28
+ span_width = ms(span.duration) * width_factor
29
+ duration_desc = ms(span.duration)
30
+ else
31
+ span_width = width - indent
32
+ duration_desc = 'RUNNING'
33
+ end
34
+
35
+ description = "[#{span.id}] " \
36
+ "#{span.name} - #{span.type} (#{duration_desc} ms)"
37
+ description_indent = [
38
+ 0,
39
+ [indent, @width - description.length].min
40
+ ].max
41
+
42
+ lines << "#{' ' * description_indent}#{description}"
43
+ lines << "#{' ' * indent}+#{'-' * [(span_width - 2), 0].max}+"
44
+ end
45
+
46
+ lines.map { |s| s[0..@width] }.join("\n")
47
+ rescue StandardError => e
48
+ puts e
49
+ puts e.backspan.join("\n")
50
+ nil
51
+ end
52
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
53
+
54
+ private
55
+
56
+ def ms(micros)
57
+ micros.to_f / 1_000
58
+ end
59
+ end
60
+ end
61
+ end