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,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