rails_observatory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +42 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/rails_observatory_manifest.js +2 -0
  5. data/app/assets/images/rails_observatory/logo.svg +8 -0
  6. data/app/assets/js/application.js +88 -0
  7. data/app/assets/js/controllers/chart_controller.js +176 -0
  8. data/app/assets/js/controllers/event_details_controller.js +15 -0
  9. data/app/assets/js/controllers/index.js +9 -0
  10. data/app/assets/js/controllers/sparkline_controller.js +72 -0
  11. data/app/assets/stylesheets/application/card.css +51 -0
  12. data/app/assets/stylesheets/application/chart.css +34 -0
  13. data/app/assets/stylesheets/application/dropdown.css +62 -0
  14. data/app/assets/stylesheets/application/global_modifiers.css +10 -0
  15. data/app/assets/stylesheets/application/query_table.css +68 -0
  16. data/app/assets/stylesheets/application/side_nav.css +62 -0
  17. data/app/assets/stylesheets/application/side_panel.css +35 -0
  18. data/app/assets/stylesheets/application/tab_nav.css +64 -0
  19. data/app/assets/stylesheets/application/table_chart.css +66 -0
  20. data/app/assets/stylesheets/application/tbd.css +70 -0
  21. data/app/assets/stylesheets/application/top_nav.css +33 -0
  22. data/app/assets/stylesheets/application.css +42 -0
  23. data/app/assets/stylesheets/elements/a.css +8 -0
  24. data/app/assets/stylesheets/elements/button.css +21 -0
  25. data/app/assets/stylesheets/elements/details.css +12 -0
  26. data/app/assets/stylesheets/elements/root.css +26 -0
  27. data/app/assets/stylesheets/elements/section.css +9 -0
  28. data/app/assets/stylesheets/errors/show/details.css +13 -0
  29. data/app/assets/stylesheets/layout/app.css +23 -0
  30. data/app/assets/stylesheets/layout/details-side-panel.css +15 -0
  31. data/app/assets/stylesheets/layout/requests.css +45 -0
  32. data/app/assets/stylesheets/layout/two-column.css +17 -0
  33. data/app/assets/stylesheets/mixins/nav_button.css +19 -0
  34. data/app/assets/stylesheets/requests/stats.css +35 -0
  35. data/app/controllers/rails_observatory/application_controller.rb +24 -0
  36. data/app/controllers/rails_observatory/errors_controller.rb +27 -0
  37. data/app/controllers/rails_observatory/jobs_controller.rb +25 -0
  38. data/app/controllers/rails_observatory/mailers_controller.rb +11 -0
  39. data/app/controllers/rails_observatory/requests_controller.rb +33 -0
  40. data/app/helpers/rails_observatory/application_helper.rb +110 -0
  41. data/app/jobs/rails_observatory/application_job.rb +4 -0
  42. data/app/mailers/rails_observatory/application_mailer.rb +6 -0
  43. data/app/views/layouts/rails_observatory/application.html.erb +93 -0
  44. data/app/views/new_user_mailer/greeting.html.erb +1 -0
  45. data/app/views/posts/index.html.erb +1 -0
  46. data/app/views/rails_observatory/application/_chart.html.erb +23 -0
  47. data/app/views/rails_observatory/application/_events_table.html.erb +24 -0
  48. data/app/views/rails_observatory/application/_sparkline.html.erb +17 -0
  49. data/app/views/rails_observatory/application/_trace.html.erb +122 -0
  50. data/app/views/rails_observatory/errors/index.html.erb +87 -0
  51. data/app/views/rails_observatory/errors/show.html.erb +193 -0
  52. data/app/views/rails_observatory/jobs/_table_chart.html.erb +29 -0
  53. data/app/views/rails_observatory/jobs/index.html.erb +20 -0
  54. data/app/views/rails_observatory/jobs/show.html.erb +8 -0
  55. data/app/views/rails_observatory/logs/index.html.erb +18 -0
  56. data/app/views/rails_observatory/mailers/index.html.erb +11 -0
  57. data/app/views/rails_observatory/mailers/show.html.erb +10 -0
  58. data/app/views/rails_observatory/requests/_text_gauge.html.erb +4 -0
  59. data/app/views/rails_observatory/requests/index.html.erb +56 -0
  60. data/app/views/rails_observatory/requests/show.html.erb +16 -0
  61. data/config/routes.rb +7 -0
  62. data/lib/rails_observatory/action_mailer_subscriber.rb +14 -0
  63. data/lib/rails_observatory/engine.rb +49 -0
  64. data/lib/rails_observatory/event_collector.rb +43 -0
  65. data/lib/rails_observatory/log_collector.rb +46 -0
  66. data/lib/rails_observatory/mailer_previews/delivered_mail_preview.rb +9 -0
  67. data/lib/rails_observatory/middleware.rb +77 -0
  68. data/lib/rails_observatory/models/error.rb +67 -0
  69. data/lib/rails_observatory/models/event_collection.rb +137 -0
  70. data/lib/rails_observatory/models/events.rb +22 -0
  71. data/lib/rails_observatory/models/job_trace.rb +28 -0
  72. data/lib/rails_observatory/models/logs.rb +9 -0
  73. data/lib/rails_observatory/models/mail_delivery.rb +33 -0
  74. data/lib/rails_observatory/models/redis_model.rb +112 -0
  75. data/lib/rails_observatory/models/request_trace.rb +29 -0
  76. data/lib/rails_observatory/railties/active_job_instrumentation.rb +48 -0
  77. data/lib/rails_observatory/railties/redis_runtime.rb +11 -0
  78. data/lib/rails_observatory/redis/logging_middleware.rb +22 -0
  79. data/lib/rails_observatory/redis/redis_client_instrumentation.rb +18 -0
  80. data/lib/rails_observatory/redis/time_series/increment_script.lua +67 -0
  81. data/lib/rails_observatory/redis/time_series/insertion.rb +73 -0
  82. data/lib/rails_observatory/redis/time_series/query_builder.rb +149 -0
  83. data/lib/rails_observatory/redis/time_series/timing_script.lua +89 -0
  84. data/lib/rails_observatory/redis/time_series.rb +91 -0
  85. data/lib/rails_observatory/serializers/event_serializer.rb +19 -0
  86. data/lib/rails_observatory/serializers/headers_serializer.rb +12 -0
  87. data/lib/rails_observatory/serializers/job_serializer.rb +11 -0
  88. data/lib/rails_observatory/serializers/mail_delivery_job_serializer.rb +14 -0
  89. data/lib/rails_observatory/serializers/request_serializer.rb +17 -0
  90. data/lib/rails_observatory/serializers/response_serializer.rb +14 -0
  91. data/lib/rails_observatory/serializers/serializer.rb +51 -0
  92. data/lib/rails_observatory/version.rb +3 -0
  93. data/lib/rails_observatory.rb +3 -0
  94. data/public/assets/js/application.js +11186 -0
  95. data/public/assets/logo_with_text.svg +21 -0
  96. data/public/assets/stylesheets/application.css +757 -0
  97. metadata +197 -0
@@ -0,0 +1,149 @@
1
+ module RailsObservatory
2
+ class TimeSeries
3
+ class QueryBuilder
4
+ include Enumerable
5
+
6
+ def initialize(series_class)
7
+ @series_class = series_class
8
+ @conditions = {}
9
+ @samples = nil
10
+ @range_set = false
11
+ @group = nil
12
+ @range = (nil..)
13
+ end
14
+
15
+ def where(**conditions)
16
+ clone = self.clone
17
+ clone.instance_variable_set(:@conditions, @conditions.merge(conditions))
18
+ clone
19
+ end
20
+
21
+ def group(label)
22
+ clone = self.clone
23
+ clone.instance_variable_set(:@group, label)
24
+ clone
25
+ end
26
+
27
+ def slice(range)
28
+ clone = self.clone
29
+ clone.instance_variable_set(:@range_set, true)
30
+ clone.instance_variable_set(:@range, range)
31
+ clone
32
+ end
33
+
34
+ def downsample(samples, using:)
35
+ clone = self.clone
36
+ clone.instance_variable_set(:@samples, samples)
37
+ clone.instance_variable_set(:@agg_type, using)
38
+ clone
39
+ end
40
+
41
+
42
+
43
+ def sum
44
+ if @group
45
+ @agg_type = :sum
46
+ @samples = 1
47
+ to_a.index_by { _1.labels[@group] }.transform_values { _1.value }
48
+ else
49
+ raise "Cannot sum without grouping"
50
+ end
51
+ end
52
+
53
+ def avg
54
+ if @group
55
+ @agg_type = :avg
56
+ @samples = 1
57
+ to_a.index_by { _1.labels[@group] }
58
+ else
59
+ raise "Cannot avg without grouping"
60
+ end
61
+ end
62
+
63
+ def last
64
+ if @group
65
+ @agg_type = :last
66
+ @samples = 1
67
+ to_a.index_by { _1.labels[@group] }
68
+ else
69
+ raise "Cannot last without grouping"
70
+ end
71
+ end
72
+
73
+ def each
74
+ @range = ActiveSupport::IsolatedExecutionState[:observatory_slice] || (nil..) unless @range_set
75
+ agg_duration = build_agg_duration
76
+ mrange_args = ['TS.MRANGE', from_ts, to_ts, 'WITHLABELS']
77
+ mrange_args.push('LATEST') if @range.end.nil?
78
+ if @agg_type && @samples
79
+ if @range.end.present?
80
+ mrange_args.push("ALIGN", 'end')
81
+ elsif @range.begin.present?
82
+ mrange_args.push("ALIGN", 'start')
83
+ end
84
+ mrange_args.push("AGGREGATION", @agg_type.to_s.upcase, agg_duration, "EMPTY")
85
+ end
86
+ mrange_args.push('FILTER', *ts_filters)
87
+
88
+ # puts mrange_args.join(" ")
89
+ res = @series_class.redis.call(mrange_args)
90
+ return if res.nil?
91
+
92
+ res.each do |name, labels, data|
93
+ yield @series_class.new(
94
+ name:,
95
+ labels: Hash[*labels.flatten],
96
+ data:,
97
+ time_range: @range,
98
+ agg_duration: agg_duration
99
+ )
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def build_agg_duration
106
+ end_time = @range.end || Time.now
107
+ start_time = @range.begin || 12.months.ago.to_time
108
+ available_datapoints = ((end_time - start_time) / 10.0).to_i
109
+ datapoints = [@samples, available_datapoints].min
110
+ ((end_time - start_time) * 1000 / datapoints).to_i
111
+ end
112
+
113
+ def from_ts
114
+ if @range.begin.nil?
115
+ "-"
116
+ else
117
+ @range.begin.to_i * 1000
118
+ end
119
+ end
120
+
121
+ def to_ts
122
+ if @range.end.nil?
123
+ "+"
124
+ else
125
+ @range.end.to_i * 1000
126
+ end
127
+ end
128
+
129
+ def ts_filters
130
+ raise 'Must specify name' if @conditions[:name].blank?
131
+
132
+ @conditions[@group] = "*" if @group
133
+ labels = @series_class.redis.call('SMEMBERS', "#{@conditions[:name]}:labels")
134
+ labels = labels.map { |l| [l.to_sym, nil] }.to_h
135
+ @conditions.reverse_merge!(labels)
136
+ @conditions.map do |k, v|
137
+ if v == "*"
138
+ "#{k}!="
139
+ elsif v.is_a? Array
140
+ "#{k}=(#{v.join(',')})"
141
+ else
142
+ "#{k}=#{v}"
143
+ end
144
+ end.to_a
145
+ end
146
+
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,89 @@
1
+ -- Helper function to get all combinations of a table
2
+ local function generate_key_combinations(keys)
3
+ local n = #keys
4
+ local combs = {}
5
+ table.insert(combs, {})
6
+
7
+ local function helper(curr_comb, start_idx)
8
+ if start_idx <= n then
9
+ for i = start_idx, n do
10
+ local new_comb = {}
11
+ for _, v in ipairs(curr_comb) do
12
+ table.insert(new_comb, v)
13
+ end
14
+ table.insert(new_comb, keys[i])
15
+ table.insert(combs, new_comb)
16
+ helper(new_comb, i + 1)
17
+ end
18
+ end
19
+ end
20
+
21
+ helper({}, 1)
22
+ return combs
23
+ end
24
+
25
+ local function extract_parent_label(base_name)
26
+ local index = string.find(base_name, "/")
27
+ if index then
28
+ return string.sub(base_name, 1, index - 1)
29
+ else
30
+ return nil
31
+ end
32
+ end
33
+
34
+ -- Main script begins here
35
+ local metric_name = tostring(ARGV[1]) -- Ensure it's a string
36
+ local value_to_add = tonumber(ARGV[2]) -- Ensure it's a number
37
+ local raw_retention = 10000 -- Hardcoded to 10ms
38
+ local compaction_retention = 31536000000 -- Hardcoded to 1 year in ms (365*24*60*60*1000)
39
+ local compactions = {"avg", "min", "max"}
40
+
41
+ -- Assuming base_name is defined somewhere above
42
+ local parent_label = extract_parent_label(metric_name)
43
+
44
+ -- Extracting labels
45
+ local labels = {}
46
+ local keys = {}
47
+ for i=3, #ARGV, 2 do
48
+ local key = tostring(ARGV[i])
49
+ local value = tostring(ARGV[i+1])
50
+ labels[key] = value
51
+ redis.call("SADD", metric_name .. ':labels', key)
52
+ table.insert(keys, key)
53
+ end
54
+
55
+ local key_combinations = generate_key_combinations(keys)
56
+
57
+ -- For each combination, upsert and add labels
58
+ for _, comb_keys in ipairs(key_combinations) do
59
+ local ts_name = metric_name
60
+ local label_set = {}
61
+
62
+ if parent_label then
63
+ table.insert(label_set, "parent")
64
+ table.insert(label_set, parent_label)
65
+ end
66
+
67
+ for _, key in ipairs(comb_keys) do
68
+ ts_name = ts_name .. ":" .. labels[key]
69
+ table.insert(label_set, key)
70
+ table.insert(label_set, labels[key])
71
+ end
72
+
73
+ if redis.call("EXISTS", ts_name) == 0 then
74
+ redis.call("TS.CREATE", ts_name, "RETENTION", raw_retention, "CHUNK_SIZE", 48)
75
+ end
76
+
77
+ -- Handle the compactions (avg, min, max)
78
+ for _, compaction in ipairs(compactions) do
79
+ local compaction_key = ts_name .. "_" .. compaction
80
+ if redis.call("EXISTS", compaction_key) == 0 then
81
+ redis.call("TS.CREATE", compaction_key, "RETENTION", compaction_retention, "CHUNK_SIZE", 48, "LABELS","name", metric_name, "compaction", compaction, unpack(label_set))
82
+ redis.call("TS.CREATERULE", ts_name, compaction_key, "AGGREGATION", compaction, 10000)
83
+ return redis.call("TS.INFO", compaction_key)
84
+ end
85
+ end
86
+ redis.call("TS.ADD", ts_name, "*", value_to_add)
87
+ end
88
+
89
+ return "OK"
@@ -0,0 +1,91 @@
1
+ require_relative 'time_series/insertion'
2
+ require_relative 'time_series/query_builder'
3
+
4
+ module RailsObservatory
5
+ class TimeSeries
6
+
7
+ extend Insertion
8
+
9
+ attr_reader :labels, :name, :data
10
+
11
+ def self.redis
12
+ Rails.configuration.rails_observatory.redis
13
+ end
14
+
15
+ def redis
16
+ self.class.redis
17
+ end
18
+
19
+ def self.with_slice(time_range)
20
+ ActiveSupport::IsolatedExecutionState[:observatory_slice] = time_range
21
+ yield
22
+ ensure
23
+ ActiveSupport::IsolatedExecutionState[:observatory_slice] = nil
24
+ end
25
+
26
+ def self.where(**conditions)
27
+ QueryBuilder.new(self).where(**conditions)
28
+ end
29
+
30
+ def initialize(name:, labels: {}, data:, time_range:, agg_duration:)
31
+ @name = name
32
+ @time_range = time_range
33
+ @agg_duration = agg_duration
34
+ @labels = labels.deep_symbolize_keys
35
+ @data = data
36
+ end
37
+
38
+ def start_time
39
+ @time_range.begin.nil? ? Time.utc(2023, 1, 1, 0, 0, 0) : @time_range.begin
40
+ end
41
+
42
+ def start_time_ms
43
+ start_time.to_i.in_milliseconds
44
+ end
45
+
46
+ def end_time_ms
47
+ end_time.to_i.in_milliseconds
48
+ end
49
+
50
+ def end_time
51
+ @time_range.end.nil? ? Time.now : @time_range.end
52
+ end
53
+
54
+ def filled_data
55
+ if @time_range.end.nil?
56
+ Enumerator
57
+ .produce(to_ms(start_time)) { |t| t + @agg_duration }
58
+ .take_while { |t| t < to_ms(end_time) }
59
+ .map do |t|
60
+ match = data.find { |ts, _| ts == t }
61
+ if match
62
+ timestamp, val = match
63
+ [timestamp, val.to_f]
64
+ else
65
+ [t, 0]
66
+ end
67
+ end
68
+ else
69
+ data.map do |ts, value|
70
+ [ts, value.to_i || 0]
71
+ end
72
+ end
73
+ end
74
+
75
+ def empty?
76
+ data.empty?
77
+ end
78
+
79
+ def value
80
+ data.dig(0, 1).to_i
81
+ end
82
+
83
+ def to_ms(duration)
84
+ self.class.to_ms(duration)
85
+ end
86
+
87
+ def self.to_ms(duration)
88
+ duration.to_i * 1_000
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,19 @@
1
+ module RailsObservatory
2
+ class EventSerializer
3
+ def serialize(event)
4
+ {
5
+ name: event.name,
6
+ payload: Serializer.serialize(event.payload),
7
+ start_at: event.time,
8
+ end_at: event.end,
9
+ duration: event.duration,
10
+ allocations: event.allocations,
11
+ failed: event.payload.include?(:exception),
12
+ }
13
+ end
14
+
15
+ def self.klass
16
+ ActiveSupport::Notifications::Event
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module RailsObservatory
2
+ class HeadersSerializer
3
+ def serialize(headers)
4
+ http_headers = Hash[*headers.select { |k, v| k.start_with?("HTTP_") }.flatten]
5
+ http_headers.transform_keys! { |k| k.sub("HTTP_", "").downcase.capitalize.dasherize }
6
+ end
7
+
8
+ def self.klass
9
+ ActionDispatch::Http::Headers
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module RailsObservatory
2
+ class JobSerializer
3
+ def serialize(job)
4
+ { class: job.class.name, job_id: job.job_id, queue_name: job.queue_name }
5
+ end
6
+
7
+ def self.klass
8
+ ActiveJob::Base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ module RailsObservatory
2
+ class MailDeliveryJobSerializer < JobSerializer
3
+ def serialize(job)
4
+ super.merge(
5
+ mailer_class: job.arguments.first,
6
+ mailer_method: job.arguments.second
7
+ )
8
+ end
9
+
10
+ def self.klass
11
+ ActionMailer::MailDeliveryJob
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ module RailsObservatory
2
+ class RequestSerializer
3
+ def serialize(request)
4
+ {
5
+ method: request.method,
6
+ path: request.path,
7
+ format: request.format,
8
+ route_pattern: request.route_uri_pattern,
9
+ headers: Serializer.serialize(request.headers),
10
+ }
11
+ end
12
+
13
+ def self.klass
14
+ ActionDispatch::Request
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module RailsObservatory
2
+ class ResponseSerializer
3
+ def serialize(response)
4
+ {
5
+ status: response.status,
6
+ # headers: Serializer.serialize(response.headers),
7
+ }
8
+ end
9
+
10
+ def self.klass
11
+ ActionDispatch::Response
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ require_relative './job_serializer'
2
+ require_relative './mail_delivery_job_serializer'
3
+ require_relative './event_serializer'
4
+ require_relative './request_serializer'
5
+ require_relative './headers_serializer'
6
+ require_relative './response_serializer'
7
+
8
+ module RailsObservatory
9
+ class Serializer
10
+
11
+ PERMITTED_TYPES = [NilClass, String, Integer, Float, TrueClass, FalseClass]
12
+
13
+ ADDITIONAL_SERIALIZERS = [JobSerializer, MailDeliveryJobSerializer, EventSerializer, RequestSerializer, HeadersSerializer, ResponseSerializer]
14
+
15
+ class << self
16
+
17
+
18
+ def serialize(argument)
19
+ serialize_payload(argument)
20
+ end
21
+
22
+ def serialize_payload(argument)
23
+ case argument
24
+ when *PERMITTED_TYPES
25
+ argument
26
+ when Array
27
+ argument.map { serialize_payload(_1) }
28
+ when ActiveSupport::HashWithIndifferentAccess
29
+ serialize_hash(argument)
30
+ when Hash
31
+ serialize_hash(argument)
32
+ when -> (arg) { arg.respond_to?(:permitted?) && arg.respond_to?(:to_h) }
33
+ serialize_hash(argument.to_h)
34
+ when Symbol
35
+ argument.to_s
36
+ else
37
+ ADDITIONAL_SERIALIZERS.find { argument.is_a?(_1.klass) }&.new&.serialize(argument) || "Unable to serialize #{argument.class.name}"
38
+ end
39
+ end
40
+
41
+ def serialize_hash(argument)
42
+ argument.each_with_object({}) do |(key, value), hash|
43
+ case key
44
+ when String, Symbol
45
+ hash[key] = serialize_payload(value)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ module RailsObservatory
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,3 @@
1
+ require 'rails_observatory/engine'
2
+ module RailsObservatory
3
+ end