atatus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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