rails_observatory 0.1.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.
- checksums.yaml +7 -0
- data/README.md +42 -0
- data/Rakefile +8 -0
- data/app/assets/config/rails_observatory_manifest.js +2 -0
- data/app/assets/images/rails_observatory/logo.svg +8 -0
- data/app/assets/js/application.js +88 -0
- data/app/assets/js/controllers/chart_controller.js +176 -0
- data/app/assets/js/controllers/event_details_controller.js +15 -0
- data/app/assets/js/controllers/index.js +9 -0
- data/app/assets/js/controllers/sparkline_controller.js +72 -0
- data/app/assets/stylesheets/application/card.css +51 -0
- data/app/assets/stylesheets/application/chart.css +34 -0
- data/app/assets/stylesheets/application/dropdown.css +62 -0
- data/app/assets/stylesheets/application/global_modifiers.css +10 -0
- data/app/assets/stylesheets/application/query_table.css +68 -0
- data/app/assets/stylesheets/application/side_nav.css +62 -0
- data/app/assets/stylesheets/application/side_panel.css +35 -0
- data/app/assets/stylesheets/application/tab_nav.css +64 -0
- data/app/assets/stylesheets/application/table_chart.css +66 -0
- data/app/assets/stylesheets/application/tbd.css +70 -0
- data/app/assets/stylesheets/application/top_nav.css +33 -0
- data/app/assets/stylesheets/application.css +42 -0
- data/app/assets/stylesheets/elements/a.css +8 -0
- data/app/assets/stylesheets/elements/button.css +21 -0
- data/app/assets/stylesheets/elements/details.css +12 -0
- data/app/assets/stylesheets/elements/root.css +26 -0
- data/app/assets/stylesheets/elements/section.css +9 -0
- data/app/assets/stylesheets/errors/show/details.css +13 -0
- data/app/assets/stylesheets/layout/app.css +23 -0
- data/app/assets/stylesheets/layout/details-side-panel.css +15 -0
- data/app/assets/stylesheets/layout/requests.css +45 -0
- data/app/assets/stylesheets/layout/two-column.css +17 -0
- data/app/assets/stylesheets/mixins/nav_button.css +19 -0
- data/app/assets/stylesheets/requests/stats.css +35 -0
- data/app/controllers/rails_observatory/application_controller.rb +24 -0
- data/app/controllers/rails_observatory/errors_controller.rb +27 -0
- data/app/controllers/rails_observatory/jobs_controller.rb +25 -0
- data/app/controllers/rails_observatory/mailers_controller.rb +11 -0
- data/app/controllers/rails_observatory/requests_controller.rb +33 -0
- data/app/helpers/rails_observatory/application_helper.rb +110 -0
- data/app/jobs/rails_observatory/application_job.rb +4 -0
- data/app/mailers/rails_observatory/application_mailer.rb +6 -0
- data/app/views/layouts/rails_observatory/application.html.erb +93 -0
- data/app/views/new_user_mailer/greeting.html.erb +1 -0
- data/app/views/posts/index.html.erb +1 -0
- data/app/views/rails_observatory/application/_chart.html.erb +23 -0
- data/app/views/rails_observatory/application/_events_table.html.erb +24 -0
- data/app/views/rails_observatory/application/_sparkline.html.erb +17 -0
- data/app/views/rails_observatory/application/_trace.html.erb +122 -0
- data/app/views/rails_observatory/errors/index.html.erb +87 -0
- data/app/views/rails_observatory/errors/show.html.erb +193 -0
- data/app/views/rails_observatory/jobs/_table_chart.html.erb +29 -0
- data/app/views/rails_observatory/jobs/index.html.erb +20 -0
- data/app/views/rails_observatory/jobs/show.html.erb +8 -0
- data/app/views/rails_observatory/logs/index.html.erb +18 -0
- data/app/views/rails_observatory/mailers/index.html.erb +11 -0
- data/app/views/rails_observatory/mailers/show.html.erb +10 -0
- data/app/views/rails_observatory/requests/_text_gauge.html.erb +4 -0
- data/app/views/rails_observatory/requests/index.html.erb +56 -0
- data/app/views/rails_observatory/requests/show.html.erb +16 -0
- data/config/routes.rb +7 -0
- data/lib/rails_observatory/action_mailer_subscriber.rb +14 -0
- data/lib/rails_observatory/engine.rb +49 -0
- data/lib/rails_observatory/event_collector.rb +43 -0
- data/lib/rails_observatory/log_collector.rb +46 -0
- data/lib/rails_observatory/mailer_previews/delivered_mail_preview.rb +9 -0
- data/lib/rails_observatory/middleware.rb +77 -0
- data/lib/rails_observatory/models/error.rb +67 -0
- data/lib/rails_observatory/models/event_collection.rb +137 -0
- data/lib/rails_observatory/models/events.rb +22 -0
- data/lib/rails_observatory/models/job_trace.rb +28 -0
- data/lib/rails_observatory/models/logs.rb +9 -0
- data/lib/rails_observatory/models/mail_delivery.rb +33 -0
- data/lib/rails_observatory/models/redis_model.rb +112 -0
- data/lib/rails_observatory/models/request_trace.rb +29 -0
- data/lib/rails_observatory/railties/active_job_instrumentation.rb +48 -0
- data/lib/rails_observatory/railties/redis_runtime.rb +11 -0
- data/lib/rails_observatory/redis/logging_middleware.rb +22 -0
- data/lib/rails_observatory/redis/redis_client_instrumentation.rb +18 -0
- data/lib/rails_observatory/redis/time_series/increment_script.lua +67 -0
- data/lib/rails_observatory/redis/time_series/insertion.rb +73 -0
- data/lib/rails_observatory/redis/time_series/query_builder.rb +149 -0
- data/lib/rails_observatory/redis/time_series/timing_script.lua +89 -0
- data/lib/rails_observatory/redis/time_series.rb +91 -0
- data/lib/rails_observatory/serializers/event_serializer.rb +19 -0
- data/lib/rails_observatory/serializers/headers_serializer.rb +12 -0
- data/lib/rails_observatory/serializers/job_serializer.rb +11 -0
- data/lib/rails_observatory/serializers/mail_delivery_job_serializer.rb +14 -0
- data/lib/rails_observatory/serializers/request_serializer.rb +17 -0
- data/lib/rails_observatory/serializers/response_serializer.rb +14 -0
- data/lib/rails_observatory/serializers/serializer.rb +51 -0
- data/lib/rails_observatory/version.rb +3 -0
- data/lib/rails_observatory.rb +3 -0
- data/public/assets/js/application.js +11186 -0
- data/public/assets/logo_with_text.svg +21 -0
- data/public/assets/stylesheets/application.css +757 -0
- 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,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,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
|