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,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Serializers
6
+ # @api private
7
+ class ErrorSerializer < Serializer
8
+ def context_serializer
9
+ @context_serializer ||= ContextSerializer.new(config)
10
+ end
11
+
12
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
13
+ def build(error)
14
+ base = {
15
+ id: error.id,
16
+ transaction_id: error.transaction_id,
17
+ transaction: build_transaction(error.transaction),
18
+ trace_id: error.trace_id,
19
+ parent_id: error.parent_id,
20
+
21
+ culprit: keyword_field(error.culprit),
22
+ timestamp: error.timestamp
23
+ }
24
+
25
+ if (context = context_serializer.build(error.context))
26
+ base[:context] = context
27
+ end
28
+
29
+ if (exception = error.exception)
30
+ base[:exception] = build_exception exception
31
+ end
32
+
33
+ if (log = error.log)
34
+ base[:log] = build_log log
35
+ end
36
+
37
+ { error: base }
38
+ end
39
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
40
+
41
+ private
42
+
43
+ def build_exception(exception)
44
+ {
45
+ message: exception.message,
46
+ type: keyword_field(exception.type),
47
+ module: keyword_field(exception.module),
48
+ code: keyword_field(exception.code),
49
+ attributes: exception.attributes,
50
+ stacktrace: exception.stacktrace.to_a,
51
+ handled: exception.handled,
52
+ cause: exception.cause && [build_exception(exception.cause)]
53
+ }
54
+ end
55
+
56
+ def build_log(log)
57
+ {
58
+ message: log.message,
59
+ level: keyword_field(log.level),
60
+ logger_name: keyword_field(log.logger_name),
61
+ param_message: keyword_field(log.param_message),
62
+ stacktrace: log.stacktrace.to_a
63
+ }
64
+ end
65
+
66
+ def build_transaction(transaction)
67
+ return unless transaction
68
+
69
+ {
70
+ sampled: transaction[:sampled],
71
+ type: keyword_field(transaction[:type])
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Serializers
6
+ # @api private
7
+ class MetadataSerializer < Serializer
8
+ def build(metadata)
9
+ {
10
+ metadata: {
11
+ service: build_service(metadata.service),
12
+ process: build_process(metadata.process),
13
+ system: build_system(metadata.system),
14
+ labels: build_labels(metadata.labels)
15
+ }
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
22
+ def build_service(service)
23
+ {
24
+ name: keyword_field(service.name),
25
+ environment: keyword_field(service.environment),
26
+ version: keyword_field(service.version),
27
+ agent: {
28
+ name: keyword_field(service.agent.name),
29
+ version: keyword_field(service.agent.version)
30
+ },
31
+ framework: {
32
+ name: keyword_field(service.framework.name),
33
+ version: keyword_field(service.framework.version)
34
+ },
35
+ language: {
36
+ name: keyword_field(service.language.name),
37
+ version: keyword_field(service.language.version)
38
+ },
39
+ runtime: {
40
+ name: keyword_field(service.runtime.name),
41
+ version: keyword_field(service.runtime.version)
42
+ }
43
+ }
44
+ end
45
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
46
+
47
+ def build_process(process)
48
+ {
49
+ pid: process.pid,
50
+ title: keyword_field(process.title),
51
+ argv: process.argv
52
+ }
53
+ end
54
+
55
+ def build_system(system)
56
+ {
57
+ hostname: keyword_field(system.hostname),
58
+ architecture: keyword_field(system.architecture),
59
+ platform: keyword_field(system.platform),
60
+ kubernetes: keyword_object(system.kubernetes)
61
+ }
62
+ end
63
+
64
+ def build_labels(labels)
65
+ keyword_object(labels)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Serializers
6
+ # @api private
7
+ class MetricsetSerializer < Serializer
8
+ def build(metricset)
9
+ {
10
+ metricset: {
11
+ timestamp: metricset.timestamp.to_i,
12
+ tags: keyword_object(metricset.labels),
13
+ samples: build_samples(metricset.samples)
14
+ }
15
+ }
16
+ end
17
+
18
+ private
19
+
20
+ def build_samples(samples)
21
+ samples.each_with_object({}) do |(key, value), hsh|
22
+ hsh[key] = { value: value }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Serializers
6
+ # @api private
7
+ class SpanSerializer < Serializer
8
+ def initialize(config)
9
+ super
10
+
11
+ @context_serializer = ContextSerializer.new(config)
12
+ end
13
+
14
+ attr_reader :context_serializer
15
+
16
+ # rubocop:disable Metrics/MethodLength
17
+ def build(span)
18
+ {
19
+ span: {
20
+ id: span.id,
21
+ transaction_id: span.transaction_id,
22
+ parent_id: span.parent_id,
23
+ name: keyword_field(span.name),
24
+ type: join_type(span),
25
+ duration: ms(span.duration),
26
+ context: context_serializer.build(span.context),
27
+ stacktrace: span.stacktrace.to_a,
28
+ timestamp: span.timestamp,
29
+ trace_id: span.trace_id
30
+ }
31
+ }
32
+ end
33
+ # rubocop:enable Metrics/MethodLength
34
+
35
+ # @api private
36
+ class ContextSerializer < Serializer
37
+ def build(context)
38
+ return unless context
39
+
40
+ { sync: context.sync }.tap do |base|
41
+ base[:db] = build_db(context.db) if context.db
42
+ base[:http] = build_http(context.http) if context.http
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def build_db(db)
49
+ return unless db
50
+
51
+ {
52
+ instance: db.instance,
53
+ statement: Util.truncate(db.statement, max_length: 10_000),
54
+ type: db.type,
55
+ user: db.user
56
+ }
57
+ end
58
+
59
+ def build_http(http)
60
+ return unless http
61
+
62
+ {
63
+ url: http.url,
64
+ status_code: http.status_code.to_i,
65
+ method: keyword_field(http.method)
66
+ }
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def join_type(span)
73
+ combined = [span.type, span.subtype, span.action]
74
+ combined.compact!
75
+ combined.join '.'
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ module Serializers
6
+ # @api private
7
+ class TransactionSerializer < Serializer
8
+ def context_serializer
9
+ @context_serializer ||= ContextSerializer.new(config)
10
+ end
11
+
12
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
13
+ def build(transaction)
14
+ {
15
+ transaction: {
16
+ id: transaction.id,
17
+ trace_id: transaction.trace_id,
18
+ parent_id: transaction.parent_id,
19
+ name: keyword_field(transaction.name),
20
+ type: keyword_field(transaction.type),
21
+ result: keyword_field(transaction.result.to_s),
22
+ duration: ms(transaction.duration),
23
+ timestamp: transaction.timestamp,
24
+ sampled: transaction.sampled?,
25
+ context: context_serializer.build(transaction.context),
26
+ span_count: {
27
+ started: transaction.started_spans,
28
+ dropped: transaction.dropped_spans
29
+ }
30
+ }
31
+ }
32
+ end
33
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ module Transport
5
+ # @api private
6
+ class Worker
7
+ include Logging
8
+
9
+ # @api private
10
+ class StopMessage; end
11
+
12
+ # @api private
13
+ class FlushMessage; end
14
+
15
+ def initialize(
16
+ config,
17
+ queue,
18
+ serializers:,
19
+ filters:,
20
+ conn_adapter: Connection
21
+ )
22
+ @config = config
23
+ @queue = queue
24
+
25
+ @serializers = serializers
26
+ @filters = filters
27
+
28
+ metadata = serializers.serialize(Metadata.new(config))
29
+ @connection = conn_adapter.new(config, metadata)
30
+ end
31
+
32
+ attr_reader :queue, :filters, :name, :connection, :serializers
33
+
34
+ # rubocop:disable Metrics/MethodLength
35
+ def work_forever
36
+ while (msg = queue.pop)
37
+ case msg
38
+ when StopMessage
39
+ debug 'Stopping worker -- %s', self
40
+ connection.flush(:halt)
41
+ break
42
+ else
43
+ process msg
44
+ end
45
+ end
46
+ rescue Exception => e
47
+ warn 'Worker died with exception: %s', e.inspect
48
+ debug e.backtrace.join("\n")
49
+ end
50
+ # rubocop:enable Metrics/MethodLength
51
+
52
+ def process(resource)
53
+ return unless (json = serialize_and_filter(resource))
54
+ connection.write(json)
55
+ end
56
+
57
+ private
58
+
59
+ def serialize_and_filter(resource)
60
+ serialized = serializers.serialize(resource)
61
+
62
+ # if a filter returns nil, it means skip the event
63
+ return nil if @filters.apply!(serialized) == Filters::SKIP
64
+
65
+ JSON.fast_generate(serialized)
66
+ rescue Exception
67
+ error format('Failed converting event to JSON: %s', resource.inspect)
68
+ error serialized.inspect
69
+ nil
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ # @api private
5
+ module Util
6
+
7
+ def self.ms(micros)
8
+ micros.to_f / 1_000
9
+ end
10
+
11
+ def self.micros(target = Time.now)
12
+ utc = target.utc
13
+ utc.to_i * 1_000_000 + utc.usec
14
+ end
15
+
16
+ def self.monotonic_micros
17
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
18
+ end
19
+
20
+ def self.git_sha
21
+ sha = `git rev-parse --verify HEAD 2>&1`.chomp
22
+ $? && $?.success? ? sha : nil
23
+ end
24
+
25
+ def self.hex_to_bits(str)
26
+ str.hex.to_s(2).rjust(str.size * 4, '0')
27
+ end
28
+
29
+ def self.reverse_merge!(first, *others)
30
+ others.reduce(first) do |curr, other|
31
+ curr.merge!(other) { |_, _, new| new }
32
+ end
33
+ end
34
+
35
+ def self.truncate(value, max_length: 1024)
36
+ return unless value
37
+ return value if value.length <= max_length
38
+
39
+ value[0...(max_length - 1)] + '…'
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atatus
4
+ # rubocop:disable all
5
+ module Util
6
+ # From https://github.com/rails/rails/blob/v5.2.0/activesupport/lib/active_support/inflector/methods.rb#L254-L332
7
+ module Inflector
8
+ extend self
9
+
10
+ #
11
+ # Tries to find a constant with the name specified in the argument string.
12
+ #
13
+ # constantize('Module') # => Module
14
+ # constantize('Foo::Bar') # => Foo::Bar
15
+ #
16
+ # The name is assumed to be the one of a top-level constant, no matter
17
+ # whether it starts with "::" or not. No lexical context is taken into
18
+ # account:
19
+ #
20
+ # C = 'outside'
21
+ # module M
22
+ # C = 'inside'
23
+ # C # => 'inside'
24
+ # constantize('C') # => 'outside', same as ::C
25
+ # end
26
+ #
27
+ # NameError is raised when the name is not in CamelCase or the constant is
28
+ # unknown.
29
+ def constantize(camel_cased_word)
30
+ names = camel_cased_word.split("::".freeze)
31
+
32
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
33
+ Object.const_get(camel_cased_word) if names.empty?
34
+
35
+ # Remove the first blank element in case of '::ClassName' notation.
36
+ names.shift if names.size > 1 && names.first.empty?
37
+
38
+ names.inject(Object) do |constant, name|
39
+ if constant == Object
40
+ constant.const_get(name)
41
+ else
42
+ candidate = constant.const_get(name)
43
+ next candidate if constant.const_defined?(name, false)
44
+ next candidate unless Object.const_defined?(name)
45
+
46
+ # Go down the ancestors to check if it is owned directly. The check
47
+ # stops when we reach Object or the end of ancestors tree.
48
+ constant = constant.ancestors.inject(constant) do |const, ancestor|
49
+ break const if ancestor == Object
50
+ break ancestor if ancestor.const_defined?(name, false)
51
+ const
52
+ end
53
+
54
+ # owner is in Object, so raise
55
+ constant.const_get(name, false)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Tries to find a constant with the name specified in the argument string.
61
+ #
62
+ # safe_constantize('Module') # => Module
63
+ # safe_constantize('Foo::Bar') # => Foo::Bar
64
+ #
65
+ # The name is assumed to be the one of a top-level constant, no matter
66
+ # whether it starts with "::" or not. No lexical context is taken into
67
+ # account:
68
+ #
69
+ # C = 'outside'
70
+ # module M
71
+ # C = 'inside'
72
+ # C # => 'inside'
73
+ # safe_constantize('C') # => 'outside', same as ::C
74
+ # end
75
+ #
76
+ # +nil+ is returned when the name is not in CamelCase or the constant (or
77
+ # part of it) is unknown.
78
+ #
79
+ # safe_constantize('blargle') # => nil
80
+ # safe_constantize('UnknownModule') # => nil
81
+ # safe_constantize('UnknownModule::Foo::Bar') # => nil
82
+ def safe_constantize(camel_cased_word)
83
+ constantize(camel_cased_word)
84
+ rescue NameError => e
85
+ raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
86
+ e.name.to_s == camel_cased_word.to_s)
87
+ rescue ArgumentError => e
88
+ raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match(e.message)
89
+ end
90
+ end
91
+ end
92
+ # rubocop:enable all
93
+ end