atatus 1.0.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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile +57 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-THIRD-PARTY +205 -0
  7. data/README.md +13 -0
  8. data/Rakefile +19 -0
  9. data/atatus.gemspec +36 -0
  10. data/atatus.yml +2 -0
  11. data/bench/.gitignore +2 -0
  12. data/bench/app.rb +53 -0
  13. data/bench/benchmark.rb +36 -0
  14. data/bench/report.rb +55 -0
  15. data/bench/rubyprof.rb +39 -0
  16. data/bench/stackprof.rb +23 -0
  17. data/bin/build_docs +5 -0
  18. data/bin/console +15 -0
  19. data/bin/setup +8 -0
  20. data/bin/with_framework +7 -0
  21. data/lib/atatus.rb +325 -0
  22. data/lib/atatus/agent.rb +260 -0
  23. data/lib/atatus/central_config.rb +141 -0
  24. data/lib/atatus/central_config/cache_control.rb +34 -0
  25. data/lib/atatus/collector/base.rb +329 -0
  26. data/lib/atatus/collector/builder.rb +317 -0
  27. data/lib/atatus/collector/transport.rb +72 -0
  28. data/lib/atatus/config.rb +248 -0
  29. data/lib/atatus/config/bytes.rb +25 -0
  30. data/lib/atatus/config/duration.rb +23 -0
  31. data/lib/atatus/config/options.rb +134 -0
  32. data/lib/atatus/config/regexp_list.rb +13 -0
  33. data/lib/atatus/context.rb +33 -0
  34. data/lib/atatus/context/request.rb +11 -0
  35. data/lib/atatus/context/request/socket.rb +19 -0
  36. data/lib/atatus/context/request/url.rb +42 -0
  37. data/lib/atatus/context/response.rb +22 -0
  38. data/lib/atatus/context/user.rb +42 -0
  39. data/lib/atatus/context_builder.rb +97 -0
  40. data/lib/atatus/deprecations.rb +22 -0
  41. data/lib/atatus/error.rb +22 -0
  42. data/lib/atatus/error/exception.rb +46 -0
  43. data/lib/atatus/error/log.rb +24 -0
  44. data/lib/atatus/error_builder.rb +76 -0
  45. data/lib/atatus/instrumenter.rb +224 -0
  46. data/lib/atatus/internal_error.rb +6 -0
  47. data/lib/atatus/logging.rb +55 -0
  48. data/lib/atatus/metadata.rb +19 -0
  49. data/lib/atatus/metadata/process_info.rb +18 -0
  50. data/lib/atatus/metadata/service_info.rb +61 -0
  51. data/lib/atatus/metadata/system_info.rb +35 -0
  52. data/lib/atatus/metadata/system_info/container_info.rb +121 -0
  53. data/lib/atatus/metadata/system_info/hw_info.rb +118 -0
  54. data/lib/atatus/metadata/system_info/os_info.rb +31 -0
  55. data/lib/atatus/metrics.rb +98 -0
  56. data/lib/atatus/metrics/cpu_mem.rb +240 -0
  57. data/lib/atatus/metrics/vm.rb +60 -0
  58. data/lib/atatus/metricset.rb +19 -0
  59. data/lib/atatus/middleware.rb +76 -0
  60. data/lib/atatus/naively_hashable.rb +21 -0
  61. data/lib/atatus/normalizers.rb +68 -0
  62. data/lib/atatus/normalizers/action_controller.rb +27 -0
  63. data/lib/atatus/normalizers/action_mailer.rb +26 -0
  64. data/lib/atatus/normalizers/action_view.rb +77 -0
  65. data/lib/atatus/normalizers/active_record.rb +45 -0
  66. data/lib/atatus/opentracing.rb +346 -0
  67. data/lib/atatus/rails.rb +61 -0
  68. data/lib/atatus/railtie.rb +30 -0
  69. data/lib/atatus/span.rb +125 -0
  70. data/lib/atatus/span/context.rb +40 -0
  71. data/lib/atatus/span_helpers.rb +44 -0
  72. data/lib/atatus/spies.rb +86 -0
  73. data/lib/atatus/spies/action_dispatch.rb +28 -0
  74. data/lib/atatus/spies/delayed_job.rb +68 -0
  75. data/lib/atatus/spies/elasticsearch.rb +36 -0
  76. data/lib/atatus/spies/faraday.rb +70 -0
  77. data/lib/atatus/spies/http.rb +44 -0
  78. data/lib/atatus/spies/json.rb +22 -0
  79. data/lib/atatus/spies/mongo.rb +87 -0
  80. data/lib/atatus/spies/net_http.rb +70 -0
  81. data/lib/atatus/spies/rake.rb +45 -0
  82. data/lib/atatus/spies/redis.rb +27 -0
  83. data/lib/atatus/spies/sequel.rb +47 -0
  84. data/lib/atatus/spies/sidekiq.rb +89 -0
  85. data/lib/atatus/spies/sinatra.rb +41 -0
  86. data/lib/atatus/spies/tilt.rb +27 -0
  87. data/lib/atatus/sql_summarizer.rb +35 -0
  88. data/lib/atatus/stacktrace.rb +16 -0
  89. data/lib/atatus/stacktrace/frame.rb +52 -0
  90. data/lib/atatus/stacktrace_builder.rb +104 -0
  91. data/lib/atatus/subscriber.rb +77 -0
  92. data/lib/atatus/trace_context.rb +85 -0
  93. data/lib/atatus/transaction.rb +100 -0
  94. data/lib/atatus/transport/base.rb +174 -0
  95. data/lib/atatus/transport/connection.rb +156 -0
  96. data/lib/atatus/transport/connection/http.rb +116 -0
  97. data/lib/atatus/transport/connection/proxy_pipe.rb +75 -0
  98. data/lib/atatus/transport/filters.rb +43 -0
  99. data/lib/atatus/transport/filters/secrets_filter.rb +74 -0
  100. data/lib/atatus/transport/serializers.rb +93 -0
  101. data/lib/atatus/transport/serializers/context_serializer.rb +85 -0
  102. data/lib/atatus/transport/serializers/error_serializer.rb +77 -0
  103. data/lib/atatus/transport/serializers/metadata_serializer.rb +70 -0
  104. data/lib/atatus/transport/serializers/metricset_serializer.rb +28 -0
  105. data/lib/atatus/transport/serializers/span_serializer.rb +80 -0
  106. data/lib/atatus/transport/serializers/transaction_serializer.rb +37 -0
  107. data/lib/atatus/transport/worker.rb +73 -0
  108. data/lib/atatus/util.rb +42 -0
  109. data/lib/atatus/util/inflector.rb +93 -0
  110. data/lib/atatus/util/lru_cache.rb +48 -0
  111. data/lib/atatus/util/prefixed_logger.rb +18 -0
  112. data/lib/atatus/util/throttle.rb +35 -0
  113. data/lib/atatus/version.rb +5 -0
  114. data/vendor/.gitkeep +0 -0
  115. metadata +190 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Config
5
+ # @api private
6
+ class Bytes
7
+ MULTIPLIERS = {
8
+ 'kb' => 1024,
9
+ 'mb' => 1024 * 1_000,
10
+ 'gb' => 1024 * 100_000
11
+ }.freeze
12
+ REGEX = /^(\d+)(b|kb|mb|gb)?$/i.freeze
13
+
14
+ def initialize(default_unit: 'kb')
15
+ @default_unit = default_unit
16
+ end
17
+
18
+ def call(value)
19
+ _, amount, unit = REGEX.match(String(value)).to_a
20
+ unit ||= @default_unit
21
+ MULTIPLIERS.fetch(unit.downcase, 1) * amount.to_i
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Config
5
+ # @api private
6
+ class Duration
7
+ MULTIPLIERS = { 'ms' => 0.001, 'm' => 60 }.freeze
8
+ REGEX = /^(-)?(\d+)(m|ms|s)?$/i.freeze
9
+
10
+ def initialize(default_unit: 's')
11
+ @default_unit = default_unit
12
+ end
13
+
14
+ def call(str)
15
+ _, negative, amount, unit = REGEX.match(String(str)).to_a
16
+ unit ||= @default_unit
17
+ seconds = MULTIPLIERS.fetch(unit.downcase, 1) * amount.to_i
18
+ seconds = 0 - seconds if negative
19
+ seconds
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Config
5
+ # @api private
6
+ module Options
7
+ # @api private
8
+ class Option
9
+ def initialize(
10
+ key,
11
+ value: nil,
12
+ type: :string,
13
+ default: nil,
14
+ converter: nil
15
+ )
16
+ @key = key
17
+ @type = type
18
+ @default = default
19
+ @converter = converter
20
+
21
+ set(value || default)
22
+ end
23
+
24
+ attr_reader :key, :value, :default, :type
25
+
26
+ def set(value)
27
+ @value = normalize(value)
28
+ end
29
+
30
+ def env_key
31
+ "ATATUS_#{key.upcase}"
32
+ end
33
+
34
+ private
35
+
36
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
37
+ def normalize(val)
38
+ return unless val
39
+
40
+ if @converter
41
+ return @converter.call(val)
42
+ end
43
+
44
+ case type
45
+ when :string then val.to_s
46
+ when :int then val.to_i
47
+ when :float then val.to_f
48
+ when :bool then normalize_bool(val)
49
+ when :list then normalize_list(val)
50
+ when :dict then normalize_dict(val)
51
+ else
52
+ # raise "Unknown options type '#{type.inspect}'"
53
+ val
54
+ end
55
+ end
56
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
57
+
58
+ def normalize_bool(val)
59
+ return val unless val.is_a?(String)
60
+ !%w[0 false].include?(val.strip.downcase)
61
+ end
62
+
63
+ def normalize_list(val)
64
+ return Array(val) unless val.is_a?(String)
65
+ val.split(/[ ,]/)
66
+ end
67
+
68
+ def normalize_dict(val)
69
+ return val unless val.is_a?(String)
70
+ Hash[val.split(/[&,]/).map { |kv| kv.split('=') }]
71
+ end
72
+ end
73
+
74
+ # @api private
75
+ module ClassMethods
76
+ def schema
77
+ @schema ||= {}
78
+ end
79
+
80
+ def option(*args)
81
+ key = args.shift
82
+ schema[key] = *args
83
+ end
84
+ end
85
+
86
+ # @api private
87
+ module InstanceMethods
88
+ def load_schema
89
+ Hash[self.class.schema.map do |key, args|
90
+ [key, Option.new(key, *args)]
91
+ end]
92
+ end
93
+
94
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
95
+ def method_missing(name, *value)
96
+ name_str = name.to_s
97
+
98
+ if name_str.end_with?('=')
99
+ key = name_str[0...-1].to_sym
100
+ set(key, value.first)
101
+
102
+ elsif name_str.end_with?('?')
103
+ key = name_str[0...-1].to_sym
104
+ options.key?(key) ? options[key].value : super
105
+
106
+ elsif options.key?(name)
107
+ options.fetch(name).value
108
+
109
+ else
110
+ super
111
+ end
112
+ end
113
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
114
+
115
+ def [](key)
116
+ options[key]
117
+ end
118
+ alias :get :[]
119
+
120
+ def set(key, value)
121
+ options.fetch(key.to_sym).set(value)
122
+ rescue KeyError
123
+ warn format("Unknown option '%s'", key)
124
+ nil
125
+ end
126
+ end
127
+
128
+ def self.extended(kls)
129
+ kls.instance_eval { extend ClassMethods }
130
+ kls.class_eval { include InstanceMethods }
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Config
5
+ # @api private
6
+ class RegexpList
7
+ def call(value)
8
+ value = value.is_a?(String) ? value.split(',') : Array(value)
9
+ value.map(&Regexp.method(:new))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atatus/context/request'
4
+ require 'atatus/context/request/socket'
5
+ require 'atatus/context/request/url'
6
+ require 'atatus/context/response'
7
+ require 'atatus/context/user'
8
+
9
+ module Atatus
10
+ # @api private
11
+ class Context
12
+ def initialize(custom: {}, labels: {}, user: nil)
13
+ @custom = custom
14
+ @labels = labels
15
+ @user = user || User.new
16
+ end
17
+
18
+ attr_accessor :request
19
+ attr_accessor :response
20
+ attr_accessor :user
21
+ attr_reader :custom
22
+ attr_reader :labels
23
+
24
+ def empty?
25
+ return false if labels.any?
26
+ return false if custom.any?
27
+ return false if user.any?
28
+ return false if request || response
29
+
30
+ true
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Context
5
+ # @api private
6
+ class Request
7
+ attr_accessor :body, :cookies, :env, :headers, :http_version, :method,
8
+ :socket, :url
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Socket
10
+ def initialize(req)
11
+ @remote_addr = req.ip
12
+ @encrypted = req.scheme == 'https'
13
+ end
14
+
15
+ attr_reader :remote_addr, :encrypted
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ # @api private
5
+ class Context
6
+ # @api private
7
+ class Request
8
+ # @api private
9
+ class Url
10
+ SKIPPED_PORTS = {
11
+ 'http' => 80,
12
+ 'https' => 443
13
+ }.freeze
14
+
15
+ def initialize(req)
16
+ @protocol = req.scheme
17
+ @hostname = req.host
18
+ @port = req.port.to_s
19
+ @pathname = req.path
20
+ @search = req.query_string
21
+ @hash = nil
22
+ @full = build_full_url req
23
+ end
24
+
25
+ attr_reader :protocol, :hostname, :port, :pathname, :search, :hash,
26
+ :full
27
+
28
+ private
29
+
30
+ def build_full_url(req)
31
+ url = "#{req.scheme}://#{req.host}"
32
+
33
+ if req.port != SKIPPED_PORTS[req.scheme]
34
+ url += ":#{req.port}"
35
+ end
36
+
37
+ url + req.fullpath
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Context
5
+ # @api private
6
+ class Response
7
+ def initialize(
8
+ status_code,
9
+ headers: {},
10
+ headers_sent: true,
11
+ finished: true
12
+ )
13
+ @status_code = status_code
14
+ @headers = headers
15
+ @headers_sent = headers_sent
16
+ @finished = finished
17
+ end
18
+
19
+ attr_accessor :status_code, :headers, :headers_sent, :finished
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ class Context
5
+ # @api private
6
+ class User
7
+ def initialize(id: nil, email: nil, username: nil)
8
+ @id = id
9
+ @email = email
10
+ @username = username
11
+ end
12
+
13
+ def self.infer(config, record)
14
+ return unless record
15
+
16
+ new(
17
+ id: safe_get(record, config.current_user_id_method)&.to_s,
18
+ email: safe_get(record, config.current_user_email_method),
19
+ username: safe_get(record, config.current_user_username_method)
20
+ )
21
+ end
22
+
23
+ attr_accessor :id, :email, :username
24
+
25
+ def empty?
26
+ !id && !email && !username
27
+ end
28
+
29
+ def any?
30
+ !empty?
31
+ end
32
+
33
+ class << self
34
+ private
35
+
36
+ def safe_get(record, method_name)
37
+ record.respond_to?(method_name) ? record.send(method_name) : nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ # @api private
5
+ class ContextBuilder
6
+ MAX_BODY_LENGTH = 2048
7
+ SKIPPED = '[SKIPPED]'
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ attr_reader :config
14
+
15
+ def build(rack_env:, for_type:)
16
+ Context.new.tap do |context|
17
+ apply_to_request(context, rack_env: rack_env, for_type: for_type)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
24
+ def apply_to_request(context, rack_env:, for_type:)
25
+ req = rails_req?(rack_env) ? rack_env : Rack::Request.new(rack_env)
26
+
27
+ context.request = Context::Request.new unless context.request
28
+ request = context.request
29
+
30
+ request.socket = Context::Request::Socket.new(req)
31
+ request.http_version = build_http_version rack_env
32
+ request.method = req.request_method
33
+ request.url = Context::Request::Url.new(req)
34
+
35
+ request.body = should_capture_body?(for_type) ? get_body(req) : SKIPPED
36
+
37
+ headers, env = get_headers_and_env(rack_env)
38
+ request.headers = headers if config.capture_headers?
39
+ request.env = env if config.capture_env?
40
+
41
+ request.cookies = req.cookies
42
+
43
+ context
44
+ end
45
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
46
+
47
+ def should_capture_body?(for_type)
48
+ option = config.capture_body
49
+
50
+ return true if option == 'all'
51
+ return true if option == 'transactions' && for_type == :transaction
52
+ return true if option == 'errors' && for_type == :error
53
+
54
+ false
55
+ end
56
+
57
+ def get_body(req)
58
+ case req.media_type
59
+ when 'application/x-www-form-urlencoded', 'multipart/form-data'
60
+ req.POST.dup
61
+ else
62
+ body = req.body.read
63
+ req.body.rewind
64
+ body.byteslice(0, MAX_BODY_LENGTH).force_encoding('utf-8')
65
+ end
66
+ end
67
+
68
+ def rails_req?(env)
69
+ defined?(ActionDispatch::Request) && env.is_a?(ActionDispatch::Request)
70
+ end
71
+
72
+ def get_headers_and_env(rack_env)
73
+ # In Rails < 5 ActionDispatch::Request inherits from Hash
74
+ headers =
75
+ rack_env.respond_to?(:headers) ? rack_env.headers : rack_env
76
+
77
+ headers.each_with_object([{}, {}]) do |(key, value), (http, env)|
78
+ next unless key == key.upcase
79
+
80
+ if key.start_with?('HTTP_')
81
+ http[camel_key(key)] = value
82
+ else
83
+ env[key] = value
84
+ end
85
+ end
86
+ end
87
+
88
+ def camel_key(key)
89
+ key.gsub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
90
+ end
91
+
92
+ def build_http_version(rack_env)
93
+ return unless (http_version = rack_env['HTTP_VERSION'])
94
+ http_version.gsub(%r{HTTP/}, '')
95
+ end
96
+ end
97
+ end