julewire-rails 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +94 -0
- data/docs/advanced-configuration.md +17 -0
- data/docs/capture-and-filtering.md +102 -0
- data/docs/configuration.md +83 -0
- data/docs/development.md +21 -0
- data/docs/events-and-errors.md +46 -0
- data/docs/lifecycle.md +24 -0
- data/docs/request-logging.md +49 -0
- data/julewire-rails.gemspec +44 -0
- data/lib/generators/julewire/install_generator.rb +15 -0
- data/lib/generators/julewire/templates/julewire.rb +16 -0
- data/lib/julewire/rails/configuration.rb +74 -0
- data/lib/julewire/rails/context_body_proxy.rb +54 -0
- data/lib/julewire/rails/debug_exception_log_silencer.rb +53 -0
- data/lib/julewire/rails/doctor_app.rb +233 -0
- data/lib/julewire/rails/exception_severity.rb +27 -0
- data/lib/julewire/rails/lifecycle_hooks.rb +76 -0
- data/lib/julewire/rails/log_subscriber_silencer.rb +52 -0
- data/lib/julewire/rails/logger.rb +185 -0
- data/lib/julewire/rails/logger_outputs.rb +36 -0
- data/lib/julewire/rails/output_requirement.rb +38 -0
- data/lib/julewire/rails/parameter_filter_plan.rb +100 -0
- data/lib/julewire/rails/parameter_filter_processor.rb +117 -0
- data/lib/julewire/rails/railtie.rb +84 -0
- data/lib/julewire/rails/request_attributes.rb +126 -0
- data/lib/julewire/rails/request_completion.rb +120 -0
- data/lib/julewire/rails/request_context.rb +91 -0
- data/lib/julewire/rails/request_error_ownership.rb +63 -0
- data/lib/julewire/rails/request_fields.rb +61 -0
- data/lib/julewire/rails/request_lifecycle.rb +109 -0
- data/lib/julewire/rails/request_middleware.rb +130 -0
- data/lib/julewire/rails/request_summary_timeout_scheduler.rb +38 -0
- data/lib/julewire/rails/structured_event_record.rb +128 -0
- data/lib/julewire/rails/subscribers/controller_response.rb +118 -0
- data/lib/julewire/rails/subscribers/error.rb +86 -0
- data/lib/julewire/rails/subscribers/event.rb +118 -0
- data/lib/julewire/rails/subscribers/rendered_exception.rb +141 -0
- data/lib/julewire/rails/suppression.rb +29 -0
- data/lib/julewire/rails/version.rb +7 -0
- data/lib/julewire/rails.rb +37 -0
- data/lib/julewire-rails.rb +3 -0
- metadata +201 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
class ContextBodyProxy
|
|
6
|
+
def initialize(body, handle:, on_close:)
|
|
7
|
+
@body = body
|
|
8
|
+
@handle = handle
|
|
9
|
+
@on_close = on_close
|
|
10
|
+
@closed = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def each(&block)
|
|
14
|
+
return enum_for(:each) unless block_given?
|
|
15
|
+
|
|
16
|
+
@handle.with_context do
|
|
17
|
+
@body.each { block.yield(it) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def close
|
|
22
|
+
return if @closed
|
|
23
|
+
|
|
24
|
+
@closed = true
|
|
25
|
+
begin
|
|
26
|
+
@handle.with_context { @body.close if @body.respond_to?(:close) }
|
|
27
|
+
ensure
|
|
28
|
+
@on_close.call
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def closed? = @closed
|
|
33
|
+
|
|
34
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
35
|
+
(method_name != :to_str && @body.respond_to?(method_name, include_private)) || super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def method_missing(method_name, ...)
|
|
39
|
+
case method_name
|
|
40
|
+
when :to_str
|
|
41
|
+
super
|
|
42
|
+
when :to_ary
|
|
43
|
+
begin
|
|
44
|
+
@handle.with_context { @body.public_send(method_name, ...) }
|
|
45
|
+
ensure
|
|
46
|
+
close
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
@handle.with_context { @body.public_send(method_name, ...) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_dispatch/middleware/debug_exceptions"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Rails
|
|
7
|
+
module DebugExceptionLogSilencer
|
|
8
|
+
Patch = Module.new do
|
|
9
|
+
def log_error(request, wrapper)
|
|
10
|
+
return if Julewire::Rails::DebugExceptionLogSilencer.suppress?(request, wrapper)
|
|
11
|
+
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
private_constant :Patch
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def install!(configuration)
|
|
19
|
+
@configuration = configuration
|
|
20
|
+
return false unless defined?(::ActionDispatch::DebugExceptions)
|
|
21
|
+
return true if @installed
|
|
22
|
+
|
|
23
|
+
::ActionDispatch::DebugExceptions.prepend(Patch)
|
|
24
|
+
@installed = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def suppress?(_request, _wrapper)
|
|
28
|
+
suppress_reported_logs?
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
IntegrationHealth.record_failure(
|
|
31
|
+
e,
|
|
32
|
+
action: :suppress?,
|
|
33
|
+
component: :debug_exception_log_silencer
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def suppress_reported_logs?
|
|
40
|
+
configuration = @configuration
|
|
41
|
+
return false unless configuration
|
|
42
|
+
|
|
43
|
+
case configuration.reported_exception_logs
|
|
44
|
+
when :auto
|
|
45
|
+
configuration.logger? && (configuration.request_summary? || configuration.error_reports?)
|
|
46
|
+
else
|
|
47
|
+
!configuration.reported_exception_logs
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rack/request"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module Julewire
|
|
9
|
+
module Rails
|
|
10
|
+
class DoctorApp
|
|
11
|
+
CONTENT_TYPE = { "content-type" => "text/html; charset=utf-8" }.freeze
|
|
12
|
+
JSON_TYPE = { "content-type" => "application/json; charset=utf-8" }.freeze
|
|
13
|
+
SSE_TYPE = {
|
|
14
|
+
"cache-control" => "no-cache",
|
|
15
|
+
"content-type" => "text/event-stream; charset=utf-8"
|
|
16
|
+
}.freeze
|
|
17
|
+
TAIL_LIMIT = 50
|
|
18
|
+
|
|
19
|
+
def initialize(runtime: Julewire, tail: nil)
|
|
20
|
+
@runtime = runtime
|
|
21
|
+
@tail = tail
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
request = ::Rack::Request.new(env)
|
|
26
|
+
case request.path_info
|
|
27
|
+
when "", "/", "/doctor"
|
|
28
|
+
html_response(render_doctor(request))
|
|
29
|
+
when "/doctor.json"
|
|
30
|
+
json_response(@runtime.doctor)
|
|
31
|
+
when "/tail"
|
|
32
|
+
html_response(render_tail(request))
|
|
33
|
+
when "/tail.json"
|
|
34
|
+
json_response(tail_records)
|
|
35
|
+
when "/tail/events"
|
|
36
|
+
sse_response(tail_events(request))
|
|
37
|
+
else
|
|
38
|
+
not_found_response
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def html_response(body)
|
|
45
|
+
[200, CONTENT_TYPE.dup, [body]]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def json_response(value)
|
|
49
|
+
[200, JSON_TYPE.dup, [JSON.generate(value)]]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sse_response(events)
|
|
53
|
+
[200, SSE_TYPE.dup, events]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def not_found_response
|
|
57
|
+
[404, CONTENT_TYPE.dup, ["not found"]]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_doctor(request)
|
|
61
|
+
report = @runtime.doctor
|
|
62
|
+
warning_items = report.fetch(:warnings).map do |warning|
|
|
63
|
+
"<li><code>#{escape(warning.fetch(:code))}</code> #{escape(warning.fetch(:message))}</li>"
|
|
64
|
+
end.join
|
|
65
|
+
|
|
66
|
+
page(
|
|
67
|
+
"Julewire Doctor",
|
|
68
|
+
[
|
|
69
|
+
"<p>Status: <strong>#{escape(report.fetch(:status))}</strong></p>",
|
|
70
|
+
"<p>Level: <code>#{escape(report.dig(:runtime, :level))}</code></p>",
|
|
71
|
+
"<p>Pipeline: <strong>#{escape(report.dig(:pipeline, :status))}</strong></p>",
|
|
72
|
+
"<h2>Warnings</h2>",
|
|
73
|
+
warning_items.empty? ? "<p>None</p>" : "<ul>#{warning_items}</ul>",
|
|
74
|
+
nav_links(request, ["/tail", "Tail"], ["/doctor.json", "JSON"])
|
|
75
|
+
].join
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render_tail(request)
|
|
80
|
+
return page("Julewire Tail", "<p>Tail is not attached.</p>") unless @tail
|
|
81
|
+
|
|
82
|
+
body = [
|
|
83
|
+
"<p>#{@tail.health.fetch(:size)} / #{@tail.capacity} records</p>",
|
|
84
|
+
[
|
|
85
|
+
"<table><thead><tr><th>Severity</th><th>Event</th><th>Message</th></tr></thead>",
|
|
86
|
+
%(<tbody data-tail-records data-tail-events-path="#{escape(app_path(request, "/tail/events"))}">),
|
|
87
|
+
tail_rows,
|
|
88
|
+
"</tbody></table>"
|
|
89
|
+
].join,
|
|
90
|
+
tail_nav(request),
|
|
91
|
+
tail_script
|
|
92
|
+
].join
|
|
93
|
+
page("Julewire Tail", body)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def tail_rows
|
|
97
|
+
tail_entries.reverse.map { tail_row(it) }.join
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def tail_row(entry)
|
|
101
|
+
record = entry.record
|
|
102
|
+
[
|
|
103
|
+
"<tr data-sequence=\"#{entry.sequence}\">",
|
|
104
|
+
"<td><code>#{escape(record["severity"])}</code></td>",
|
|
105
|
+
"<td><code>#{escape(record["event"])}</code></td>",
|
|
106
|
+
"<td>#{escape(record["message"])}</td>",
|
|
107
|
+
"</tr>"
|
|
108
|
+
].join
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def tail_nav(request)
|
|
112
|
+
nav_links(request, ["/doctor", "Doctor"], ["/tail.json", "JSON"])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def tail_records
|
|
116
|
+
@tail ? @tail.records(limit: TAIL_LIMIT) : []
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def tail_entries
|
|
120
|
+
@tail ? @tail.entries(limit: TAIL_LIMIT) : []
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def tail_events(request)
|
|
124
|
+
return ["event: unavailable\ndata: {}\n\n"] unless @tail
|
|
125
|
+
|
|
126
|
+
after = event_cursor(request)
|
|
127
|
+
events = tail_entries.filter_map do |entry|
|
|
128
|
+
next unless entry.sequence > after
|
|
129
|
+
|
|
130
|
+
"id: #{entry.sequence}\nevent: record\ndata: #{JSON.generate(tail_event(entry))}\n\n"
|
|
131
|
+
end
|
|
132
|
+
events.empty? ? [": empty\nretry: 1000\n\n"] : ["retry: 1000\n\n", *events]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def event_cursor(request)
|
|
136
|
+
value = request.get_header("HTTP_LAST_EVENT_ID")
|
|
137
|
+
value = request.params["after"] if value.nil? || value.empty?
|
|
138
|
+
Integer(value || 0)
|
|
139
|
+
rescue ArgumentError, TypeError
|
|
140
|
+
0
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def tail_event(entry)
|
|
144
|
+
{
|
|
145
|
+
at: entry.at.iso8601(6),
|
|
146
|
+
message: entry.record["message"],
|
|
147
|
+
record: entry.record,
|
|
148
|
+
sequence: entry.sequence
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def tail_script
|
|
153
|
+
<<~HTML
|
|
154
|
+
<script>
|
|
155
|
+
(() => {
|
|
156
|
+
const rows = document.querySelector("[data-tail-records]");
|
|
157
|
+
if (!rows || !window.EventSource) return;
|
|
158
|
+
const eventsPath = rows.dataset.tailEventsPath;
|
|
159
|
+
if (!eventsPath) return;
|
|
160
|
+
const seen = new Set(Array.from(rows.querySelectorAll("[data-sequence]")).map(row => row.dataset.sequence));
|
|
161
|
+
const cell = value => {
|
|
162
|
+
const td = document.createElement("td");
|
|
163
|
+
td.textContent = value == null ? "" : String(value);
|
|
164
|
+
return td;
|
|
165
|
+
};
|
|
166
|
+
const codeCell = value => {
|
|
167
|
+
const td = document.createElement("td");
|
|
168
|
+
const code = document.createElement("code");
|
|
169
|
+
code.textContent = value == null ? "" : String(value);
|
|
170
|
+
td.appendChild(code);
|
|
171
|
+
return td;
|
|
172
|
+
};
|
|
173
|
+
const prepend = entry => {
|
|
174
|
+
const sequence = String(entry.sequence);
|
|
175
|
+
if (seen.has(sequence)) return;
|
|
176
|
+
seen.add(sequence);
|
|
177
|
+
const record = entry.record || {};
|
|
178
|
+
const row = document.createElement("tr");
|
|
179
|
+
row.dataset.sequence = sequence;
|
|
180
|
+
row.appendChild(codeCell(record.severity));
|
|
181
|
+
row.appendChild(codeCell(record.event));
|
|
182
|
+
row.appendChild(cell(entry.message));
|
|
183
|
+
rows.prepend(row);
|
|
184
|
+
};
|
|
185
|
+
const source = new EventSource(eventsPath);
|
|
186
|
+
source.addEventListener("record", event => prepend(JSON.parse(event.data)));
|
|
187
|
+
})();
|
|
188
|
+
</script>
|
|
189
|
+
HTML
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def app_path(request, path)
|
|
193
|
+
base = request.script_name.to_s
|
|
194
|
+
base = "" if base == "/"
|
|
195
|
+
"#{base}#{path}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def nav_links(request, *links)
|
|
199
|
+
%(<p>#{links.map { link_to(request, it.fetch(0), it.fetch(1)) }.join(" ")}</p>)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def link_to(request, path, label)
|
|
203
|
+
%(<a href="#{escape(app_path(request, path))}">#{escape(label)}</a>)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def page(title, body)
|
|
207
|
+
<<~HTML
|
|
208
|
+
<!doctype html>
|
|
209
|
+
<html>
|
|
210
|
+
<head>
|
|
211
|
+
<meta charset="utf-8">
|
|
212
|
+
<title>#{escape(title)}</title>
|
|
213
|
+
<style>
|
|
214
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 2rem; color: #171717; }
|
|
215
|
+
a { color: #0b5fff; margin-right: 1rem; }
|
|
216
|
+
table { border-collapse: collapse; width: 100%; }
|
|
217
|
+
th, td { border-bottom: 1px solid #ddd; padding: 0.45rem; text-align: left; vertical-align: top; }
|
|
218
|
+
</style>
|
|
219
|
+
</head>
|
|
220
|
+
<body>
|
|
221
|
+
<h1>#{escape(title)}</h1>
|
|
222
|
+
#{body}
|
|
223
|
+
</body>
|
|
224
|
+
</html>
|
|
225
|
+
HTML
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def escape(value)
|
|
229
|
+
::CGI.escapeHTML(value.to_s)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
module ExceptionSeverity
|
|
6
|
+
HEADER = "action_dispatch.debug_exception_log_level"
|
|
7
|
+
SEVERITY = ::Julewire::Core::Records::Severity
|
|
8
|
+
private_constant :HEADER, :SEVERITY
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def for_request(request)
|
|
12
|
+
SEVERITY.normalize(header_value(request))
|
|
13
|
+
rescue ArgumentError
|
|
14
|
+
:error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def header_value(request)
|
|
20
|
+
request.get_header(HEADER)
|
|
21
|
+
rescue StandardError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
module LifecycleHooks
|
|
6
|
+
@at_exit_installed = false
|
|
7
|
+
@fork_tracker_installed = false
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def install!(configuration, registrar: Kernel, fork_tracker: active_support_fork_tracker)
|
|
12
|
+
return unless configuration.lifecycle_hooks?
|
|
13
|
+
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
install_at_exit!(registrar, configuration)
|
|
16
|
+
register_after_fork!
|
|
17
|
+
install_fork_tracker!(fork_tracker)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def drain!(timeout:)
|
|
22
|
+
Julewire.flush(timeout: timeout)
|
|
23
|
+
ensure
|
|
24
|
+
Julewire.close(timeout: timeout)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def after_fork!
|
|
28
|
+
RequestSummaryTimeoutScheduler.after_fork!
|
|
29
|
+
RequestErrorOwnership.clear
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Private testing seam for isolating process lifecycle hooks.
|
|
33
|
+
def reset_for_test!
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@at_exit_installed = false
|
|
36
|
+
@fork_tracker_installed = false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
private :reset_for_test!
|
|
43
|
+
|
|
44
|
+
def install_at_exit!(registrar, configuration)
|
|
45
|
+
return if @at_exit_installed
|
|
46
|
+
|
|
47
|
+
registrar.at_exit { drain!(timeout: configuration.shutdown_timeout) }
|
|
48
|
+
@at_exit_installed = true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def register_after_fork!
|
|
52
|
+
Core::Integration::Lifecycle.register_after_fork(:rails, component: :lifecycle_hooks) { after_fork! }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def install_fork_tracker!(fork_tracker)
|
|
56
|
+
return if @fork_tracker_installed
|
|
57
|
+
return unless fork_tracker.respond_to?(:after_fork)
|
|
58
|
+
|
|
59
|
+
fork_tracker.after_fork { Julewire.after_fork! }
|
|
60
|
+
@fork_tracker_installed = true
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
IntegrationHealth.record_failure(
|
|
63
|
+
e,
|
|
64
|
+
action: :install_after_fork,
|
|
65
|
+
component: :lifecycle_hooks
|
|
66
|
+
)
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def active_support_fork_tracker
|
|
71
|
+
::ActiveSupport::ForkTracker if defined?(::ActiveSupport::ForkTracker)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/log_subscriber"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Rails
|
|
7
|
+
module LogSubscriberSilencer
|
|
8
|
+
SUBSCRIBERS = [
|
|
9
|
+
["ActionController::LogSubscriber", :action_controller],
|
|
10
|
+
["ActionDispatch::LogSubscriber", :action_dispatch],
|
|
11
|
+
["ActionView::LogSubscriber", :action_view],
|
|
12
|
+
["ActiveRecord::LogSubscriber", :active_record]
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
LOG_SUBSCRIBER_FILES = %w[
|
|
16
|
+
action_controller/log_subscriber
|
|
17
|
+
action_dispatch/log_subscriber
|
|
18
|
+
action_view/log_subscriber
|
|
19
|
+
active_record/log_subscriber
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def silence!
|
|
24
|
+
require_log_subscribers
|
|
25
|
+
SUBSCRIBERS.each { |class_name, namespace| detach(class_name, namespace) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def require_log_subscribers
|
|
31
|
+
LOG_SUBSCRIBER_FILES.each { Core::Integration::Lifecycle.require_optional(it) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def detach(class_name, namespace)
|
|
35
|
+
subscriber_class = constantize(class_name)
|
|
36
|
+
return if subscriber_class.nil?
|
|
37
|
+
|
|
38
|
+
subscriber_class.detach_from(namespace) if subscriber_class.respond_to?(:detach_from)
|
|
39
|
+
Julewire::RailsSupport::EventReporter.unsubscribe_log_subscriber(subscriber_class)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def constantize(class_name)
|
|
43
|
+
class_name.split("::").reject(&:empty?).inject(Object) do |namespace, constant_name|
|
|
44
|
+
break unless namespace.const_defined?(constant_name, false)
|
|
45
|
+
|
|
46
|
+
namespace.const_get(constant_name, false)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/isolated_execution_state"
|
|
4
|
+
require "logger"
|
|
5
|
+
|
|
6
|
+
module Julewire
|
|
7
|
+
module Rails
|
|
8
|
+
class Logger
|
|
9
|
+
FORGED_RECORD_KEYS = %i[kind execution carry attributes neutral].freeze
|
|
10
|
+
RECORD_KEYS = (Julewire::Core::Fields::Bags.required_record_keys - FORGED_RECORD_KEYS).freeze
|
|
11
|
+
private_constant :FORGED_RECORD_KEYS, :RECORD_KEYS
|
|
12
|
+
|
|
13
|
+
attr_accessor :datetime_format, :formatter, :progname
|
|
14
|
+
|
|
15
|
+
def initialize(name: "Rails", source: "rails")
|
|
16
|
+
@level = ::Logger::DEBUG
|
|
17
|
+
@progname = name
|
|
18
|
+
@source = source
|
|
19
|
+
@local_level_key = :"julewire_rails_logger_level_#{object_id}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add(severity, message = nil, progname = nil) # rubocop:disable Naming/PredicateMethod -- Logger API.
|
|
23
|
+
severity ||= ::Logger::UNKNOWN
|
|
24
|
+
return true if severity < level
|
|
25
|
+
return true if Suppression.active?
|
|
26
|
+
|
|
27
|
+
message, progname = resolve_message_and_progname(message, progname) { block_given? ? yield : nil }
|
|
28
|
+
Core::RuntimeLocator.current.emit_without_level(record_for(severity, message, progname))
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def <<(message)
|
|
33
|
+
add(::Logger::UNKNOWN, message)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def debug(progname = nil, &) = add(::Logger::DEBUG, nil, progname, &)
|
|
37
|
+
|
|
38
|
+
def info(progname = nil, &) = add(::Logger::INFO, nil, progname, &)
|
|
39
|
+
|
|
40
|
+
def warn(progname = nil, &) = add(::Logger::WARN, nil, progname, &)
|
|
41
|
+
|
|
42
|
+
def error(progname = nil, &) = add(::Logger::ERROR, nil, progname, &)
|
|
43
|
+
|
|
44
|
+
def fatal(progname = nil, &) = add(::Logger::FATAL, nil, progname, &)
|
|
45
|
+
|
|
46
|
+
def unknown(progname = nil, &) = add(::Logger::UNKNOWN, nil, progname, &)
|
|
47
|
+
|
|
48
|
+
def debug? = level <= ::Logger::DEBUG
|
|
49
|
+
|
|
50
|
+
def info? = level <= ::Logger::INFO
|
|
51
|
+
|
|
52
|
+
def warn? = level <= ::Logger::WARN
|
|
53
|
+
|
|
54
|
+
def error? = level <= ::Logger::ERROR
|
|
55
|
+
|
|
56
|
+
def fatal? = level <= ::Logger::FATAL
|
|
57
|
+
|
|
58
|
+
def unknown? = level <= ::Logger::UNKNOWN
|
|
59
|
+
|
|
60
|
+
def debug! = self.level = ::Logger::DEBUG
|
|
61
|
+
|
|
62
|
+
def info! = self.level = ::Logger::INFO
|
|
63
|
+
|
|
64
|
+
def warn! = self.level = ::Logger::WARN
|
|
65
|
+
|
|
66
|
+
def error! = self.level = ::Logger::ERROR
|
|
67
|
+
|
|
68
|
+
def fatal! = self.level = ::Logger::FATAL
|
|
69
|
+
|
|
70
|
+
def level
|
|
71
|
+
local_level || @level
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def level=(value)
|
|
75
|
+
@level = normalize_level(value)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def local_level
|
|
79
|
+
::ActiveSupport::IsolatedExecutionState[@local_level_key]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def local_level=(value)
|
|
83
|
+
if value.nil?
|
|
84
|
+
::ActiveSupport::IsolatedExecutionState.delete(@local_level_key)
|
|
85
|
+
else
|
|
86
|
+
::ActiveSupport::IsolatedExecutionState[@local_level_key] = normalize_level(value)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def silence(severity = ::Logger::ERROR)
|
|
91
|
+
previous_level = local_level
|
|
92
|
+
self.local_level = severity
|
|
93
|
+
yield self
|
|
94
|
+
ensure
|
|
95
|
+
self.local_level = previous_level
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def close = flush
|
|
99
|
+
|
|
100
|
+
def reopen(*) = flush
|
|
101
|
+
|
|
102
|
+
def flush
|
|
103
|
+
formatter.clear_tags! if formatter.respond_to?(:clear_tags!)
|
|
104
|
+
Julewire.flush
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def initialize_copy(other)
|
|
108
|
+
super
|
|
109
|
+
@progname = other.progname.is_a?(String) ? other.progname.dup : other.progname
|
|
110
|
+
@local_level_key = :"julewire_rails_logger_level_#{object_id}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def normalize_level(value)
|
|
116
|
+
case value
|
|
117
|
+
when Integer
|
|
118
|
+
value
|
|
119
|
+
when Symbol, String
|
|
120
|
+
::Logger::Severity.const_get(value.to_s.upcase)
|
|
121
|
+
else
|
|
122
|
+
raise ArgumentError, "invalid log level: #{value.inspect}"
|
|
123
|
+
end
|
|
124
|
+
rescue NameError
|
|
125
|
+
raise ArgumentError, "invalid log level: #{value.inspect}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def resolve_message_and_progname(message, progname)
|
|
129
|
+
return [message, progname || self.progname] unless message.nil?
|
|
130
|
+
|
|
131
|
+
block_message = yield
|
|
132
|
+
return [block_message, progname || self.progname] unless block_message.nil?
|
|
133
|
+
|
|
134
|
+
[progname, self.progname]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def record_for(severity, message, progname)
|
|
138
|
+
record = message.is_a?(Hash) ? structured_message(message) : scalar_message(message)
|
|
139
|
+
record[:severity] = Julewire::Core::Records::Severity.severity_symbol(severity) || :unknown
|
|
140
|
+
record[:logger] ||= (progname || self.progname).to_s
|
|
141
|
+
record[:source] ||= @source
|
|
142
|
+
merge_current_tags(record)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def scalar_message(message)
|
|
146
|
+
case message
|
|
147
|
+
when Exception
|
|
148
|
+
{ message: "#{message.class}: #{message.message}", error: message }
|
|
149
|
+
else
|
|
150
|
+
{ message: message.to_s }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def structured_message(message)
|
|
155
|
+
fields = Julewire::Core::Fields::FieldSet.deep_symbolize_keys(message)
|
|
156
|
+
record = fields.slice(*RECORD_KEYS)
|
|
157
|
+
payload = fields.except(*RECORD_KEYS)
|
|
158
|
+
|
|
159
|
+
unless payload.empty?
|
|
160
|
+
record[:payload] = Julewire::Core::Fields::FieldSet.merge(payload_hash(record[:payload]), payload)
|
|
161
|
+
end
|
|
162
|
+
record
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def payload_hash(payload)
|
|
166
|
+
return {} if payload.nil?
|
|
167
|
+
return payload if payload.is_a?(Hash)
|
|
168
|
+
|
|
169
|
+
{ Julewire::Core::Fields::FieldSet::VALUE_KEY => payload }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def merge_current_tags(record)
|
|
173
|
+
current_tags = formatter.respond_to?(:current_tags) ? formatter.current_tags : nil
|
|
174
|
+
return record if current_tags.nil? || current_tags.empty?
|
|
175
|
+
|
|
176
|
+
attributes = record[:attributes].is_a?(Hash) ? record[:attributes] : {}
|
|
177
|
+
rails = attributes[:rails].is_a?(Hash) ? attributes[:rails] : {}
|
|
178
|
+
rails[:tags] = Julewire::Core::Fields::FieldSet.deep_dup(current_tags)
|
|
179
|
+
attributes[:rails] = rails
|
|
180
|
+
record[:attributes] = attributes
|
|
181
|
+
record
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|