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,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'http'
4
+ require 'concurrent'
5
+ require 'zlib'
6
+
7
+ require 'atatus/transport/connection/proxy_pipe'
8
+
9
+ module Atatus
10
+ module Transport
11
+ class Connection
12
+ # @api private
13
+ class Http
14
+ include Logging
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ @closed = Concurrent::AtomicBoolean.new
19
+
20
+ @rd, @wr = ProxyPipe.pipe(compress: @config.http_compression?)
21
+ end
22
+
23
+ def open(url, headers: {}, ssl_context: nil)
24
+ @request = open_request_in_thread(url, headers, ssl_context)
25
+ end
26
+
27
+ def self.open(config, url, headers: {}, ssl_context: nil)
28
+ new(config).tap do |http|
29
+ http.open(url, headers: headers, ssl_context: ssl_context)
30
+ end
31
+ end
32
+
33
+ def write(str)
34
+ @wr.write(str)
35
+ @wr.bytes_sent
36
+ end
37
+
38
+ def close(reason)
39
+ return if closed?
40
+
41
+ debug '%s: Closing request with reason %s', thread_str, reason
42
+ @closed.make_true
43
+
44
+ @wr&.close(reason)
45
+ return if @request.nil? || @request&.join(5)
46
+
47
+ error(
48
+ '%s: APM Server not responding in time, terminating request',
49
+ thread_str
50
+ )
51
+ @request.kill
52
+ end
53
+
54
+ def closed?
55
+ @closed.true?
56
+ end
57
+
58
+ def inspect
59
+ format(
60
+ '%s closed: %s>',
61
+ super.split.first,
62
+ closed?
63
+ )
64
+ end
65
+
66
+ private
67
+
68
+ def thread_str
69
+ format('[THREAD:%s]', Thread.current.object_id)
70
+ end
71
+
72
+ # rubocop:disable Metrics/LineLength
73
+ def open_request_in_thread(url, headers, ssl_context)
74
+ client = build_client(headers)
75
+
76
+ debug '%s: Opening new request', thread_str
77
+ Thread.new do
78
+ begin
79
+ post(client, url, ssl_context)
80
+ rescue Exception => e
81
+ error "Couldn't establish connection to APM Server:\n%p", e.inspect
82
+ end
83
+ end
84
+ end
85
+ # rubocop:enable Metrics/LineLength
86
+
87
+ def build_client(headers)
88
+ client = HTTP.headers(headers)
89
+ return client unless @config.proxy_address && @config.proxy_port
90
+
91
+ client.via(
92
+ @config.proxy_address,
93
+ @config.proxy_port,
94
+ @config.proxy_username,
95
+ @config.proxy_password,
96
+ @config.proxy_headers
97
+ )
98
+ end
99
+
100
+ def post(client, url, ssl_context)
101
+ resp = client.post(
102
+ url,
103
+ body: @rd,
104
+ ssl_context: ssl_context
105
+ ).flush
106
+
107
+ if resp&.status == 202
108
+ debug 'APM Server responded with status 202'
109
+ elsif resp
110
+ error "APM Server responded with an error:\n%p", resp.body.to_s
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+ require 'zlib'
5
+
6
+ module Atatus
7
+ module Transport
8
+ class Connection
9
+ # @api private
10
+ class ProxyPipe
11
+ def initialize(enc = nil, compress: true)
12
+ rd, wr = IO.pipe(enc)
13
+
14
+ @read = rd
15
+ @write = Write.new(wr, compress: compress)
16
+
17
+ # Http.rb<4 calls rewind on the request bodies, but IO::Pipe raises
18
+ # ~mikker
19
+ return if HTTP::VERSION.to_i >= 4
20
+ def rd.rewind; end
21
+ end
22
+
23
+ attr_reader :read, :write
24
+
25
+ # @api private
26
+ class Write
27
+ include Logging
28
+
29
+ def initialize(io, compress: true)
30
+ @io = io
31
+ @compress = compress
32
+ @bytes_sent = Concurrent::AtomicFixnum.new(0)
33
+ @config = Atatus.agent&.config # this is silly, fix Logging
34
+
35
+ return unless compress
36
+ enable_compression!
37
+ end
38
+
39
+ attr_reader :io
40
+
41
+ def enable_compression!
42
+ io.binmode
43
+ @io = Zlib::GzipWriter.new(io)
44
+ end
45
+
46
+ def close(reason = nil)
47
+ debug("Closing writer with reason #{reason}")
48
+ io.close
49
+ end
50
+
51
+ def closed?
52
+ io.closed?
53
+ end
54
+
55
+ def write(str)
56
+ io.puts(str).tap do
57
+ @bytes_sent.update do |curr|
58
+ @compress ? io.tell : curr + str.bytesize
59
+ end
60
+ end
61
+ end
62
+
63
+ def bytes_sent
64
+ @bytes_sent.value
65
+ end
66
+ end
67
+
68
+ def self.pipe(*args)
69
+ pipe = new(*args)
70
+ [pipe.read, pipe.write]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atatus/transport/filters/secrets_filter'
4
+
5
+ module Atatus
6
+ module Transport
7
+ # @api private
8
+ module Filters
9
+ SKIP = :skip
10
+
11
+ def self.new(config)
12
+ Container.new(config)
13
+ end
14
+
15
+ # @api private
16
+ class Container
17
+ def initialize(config)
18
+ @filters = { secrets: SecretsFilter.new(config) }
19
+ end
20
+
21
+ def add(key, filter)
22
+ @filters[key] = filter
23
+ end
24
+
25
+ def remove(key)
26
+ @filters.delete(key)
27
+ end
28
+
29
+ def apply!(payload)
30
+ @filters.reduce(payload) do |result, (_key, filter)|
31
+ result = filter.call(result)
32
+ break SKIP if result.nil?
33
+ result
34
+ end
35
+ end
36
+
37
+ def length
38
+ @filters.length
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Filters
6
+ # @api private
7
+ class SecretsFilter
8
+ FILTERED = '[FILTERED]'
9
+
10
+ KEY_FILTERS = [
11
+ /passw(or)?d/i,
12
+ /auth/i,
13
+ /^pw$/,
14
+ /secret/i,
15
+ /token/i,
16
+ /api[-._]?key/i,
17
+ /session[-._]?id/i,
18
+ /(set[-_])?cookie/i
19
+ ].freeze
20
+
21
+ VALUE_FILTERS = [
22
+ # (probably) credit card number
23
+ /^\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}$/
24
+ ].freeze
25
+
26
+ def initialize(config)
27
+ @config = config
28
+ @key_filters = KEY_FILTERS + config.custom_key_filters
29
+ end
30
+
31
+ def call(payload)
32
+ strip_from! payload.dig(:transaction, :context, :request, :headers)
33
+ strip_from! payload.dig(:transaction, :context, :request, :env)
34
+ strip_from! payload.dig(:transaction, :context, :request, :cookies)
35
+ strip_from! payload.dig(:transaction, :context, :response, :headers)
36
+ strip_from! payload.dig(:error, :context, :request, :headers)
37
+ strip_from! payload.dig(:error, :context, :response, :headers)
38
+ strip_from! payload.dig(:transaction, :context, :request, :body)
39
+
40
+ payload
41
+ end
42
+
43
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
44
+ def strip_from!(obj)
45
+ return unless obj && obj.is_a?(Hash)
46
+
47
+ obj.each do |k, v|
48
+ if filter_key?(k)
49
+ next obj[k] = FILTERED
50
+ end
51
+
52
+ case v
53
+ when Hash
54
+ strip_from!(v)
55
+ when String
56
+ if filter_value?(v)
57
+ obj[k] = FILTERED
58
+ end
59
+ end
60
+ end
61
+ end
62
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
63
+
64
+ def filter_key?(key)
65
+ @key_filters.any? { |regex| key.match regex }
66
+ end
67
+
68
+ def filter_value?(value)
69
+ VALUE_FILTERS.any? { |regex| value.match regex }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Atatus
6
+ module Transport
7
+ # @api private
8
+ module Serializers
9
+ # @api private
10
+ class UnrecognizedResource < InternalError; end
11
+
12
+ # @api private
13
+ class Serializer
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ attr_reader :config
19
+
20
+ private
21
+
22
+ def ms(micros)
23
+ micros.to_f / 1_000
24
+ end
25
+
26
+ def keyword_field(value)
27
+ Util.truncate(value)
28
+ end
29
+
30
+ def keyword_object(hash)
31
+ return unless hash
32
+
33
+ hash.tap do |h|
34
+ h.each { |k, v| hash[k] = keyword_field(v) }
35
+ end
36
+ end
37
+
38
+ def mixed_object(hash)
39
+ return unless hash
40
+
41
+ hash.tap do |h|
42
+ h.each do |k, v|
43
+ hash[k] = v.is_a?(String) ? keyword_field(v) : v
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ class Container
51
+ def initialize(config)
52
+ @transaction = Serializers::TransactionSerializer.new(config)
53
+ @span = Serializers::SpanSerializer.new(config)
54
+ @error = Serializers::ErrorSerializer.new(config)
55
+ @metadata = Serializers::MetadataSerializer.new(config)
56
+ @metricset = Serializers::MetricsetSerializer.new(config)
57
+ end
58
+
59
+ attr_reader :transaction, :span, :error, :metadata, :metricset
60
+
61
+ # rubocop:disable Metrics/MethodLength
62
+ def serialize(resource)
63
+ case resource
64
+ when Transaction
65
+ transaction.build(resource)
66
+ when Span
67
+ span.build(resource)
68
+ when Error
69
+ error.build(resource)
70
+ when Metricset
71
+ metricset.build(resource)
72
+ when Metadata
73
+ metadata.build(resource)
74
+ else
75
+ raise UnrecognizedResource, resource.inspect
76
+ end
77
+ end
78
+ # rubocop:enable Metrics/MethodLength
79
+ end
80
+
81
+ def self.new(config)
82
+ Container.new(config)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ require 'atatus/transport/serializers/context_serializer'
89
+ require 'atatus/transport/serializers/transaction_serializer'
90
+ require 'atatus/transport/serializers/span_serializer'
91
+ require 'atatus/transport/serializers/error_serializer'
92
+ require 'atatus/transport/serializers/metricset_serializer'
93
+ require 'atatus/transport/serializers/metadata_serializer'
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Serializers
6
+ # @api private
7
+ class ContextSerializer < Serializer
8
+ def build(context)
9
+ return nil if context.nil? || context.empty?
10
+
11
+ {
12
+ custom: context.custom,
13
+ tags: mixed_object(context.labels),
14
+ request: build_request(context.request),
15
+ response: build_response(context.response),
16
+ user: build_user(context.user)
17
+ }
18
+ end
19
+
20
+ private
21
+
22
+ # rubocop:disable Metrics/MethodLength
23
+ def build_request(request)
24
+ return unless request
25
+
26
+ {
27
+ body: request.body,
28
+ cookies: request.cookies,
29
+ env: request.env,
30
+ headers: request.headers,
31
+ http_version: keyword_field(request.http_version),
32
+ method: keyword_field(request.method),
33
+ socket: build_socket(request.socket),
34
+ url: build_url(request.url)
35
+ }
36
+ end
37
+ # rubocop:enable Metrics/MethodLength
38
+
39
+ def build_response(response)
40
+ return unless response
41
+
42
+ {
43
+ status_code: response.status_code.to_i,
44
+ headers: response.headers,
45
+ headers_sent: response.headers_sent,
46
+ finished: response.finished
47
+ }
48
+ end
49
+
50
+ def build_user(user)
51
+ return if !user || user.empty?
52
+
53
+ {
54
+ id: keyword_field(user.id),
55
+ email: keyword_field(user.email),
56
+ username: keyword_field(user.username)
57
+ }
58
+ end
59
+
60
+ def build_socket(socket)
61
+ return unless socket
62
+
63
+ {
64
+ remote_addr: socket.remote_addr,
65
+ encrypted: socket.encrypted
66
+ }
67
+ end
68
+
69
+ def build_url(url)
70
+ return unless url
71
+
72
+ {
73
+ protocol: keyword_field(url.protocol),
74
+ full: keyword_field(url.full),
75
+ hostname: keyword_field(url.hostname),
76
+ port: keyword_field(url.port),
77
+ pathname: keyword_field(url.pathname),
78
+ search: keyword_field(url.search),
79
+ hash: keyword_field(url.hash)
80
+ }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end