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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +94 -0
  5. data/docs/advanced-configuration.md +17 -0
  6. data/docs/capture-and-filtering.md +102 -0
  7. data/docs/configuration.md +83 -0
  8. data/docs/development.md +21 -0
  9. data/docs/events-and-errors.md +46 -0
  10. data/docs/lifecycle.md +24 -0
  11. data/docs/request-logging.md +49 -0
  12. data/julewire-rails.gemspec +44 -0
  13. data/lib/generators/julewire/install_generator.rb +15 -0
  14. data/lib/generators/julewire/templates/julewire.rb +16 -0
  15. data/lib/julewire/rails/configuration.rb +74 -0
  16. data/lib/julewire/rails/context_body_proxy.rb +54 -0
  17. data/lib/julewire/rails/debug_exception_log_silencer.rb +53 -0
  18. data/lib/julewire/rails/doctor_app.rb +233 -0
  19. data/lib/julewire/rails/exception_severity.rb +27 -0
  20. data/lib/julewire/rails/lifecycle_hooks.rb +76 -0
  21. data/lib/julewire/rails/log_subscriber_silencer.rb +52 -0
  22. data/lib/julewire/rails/logger.rb +185 -0
  23. data/lib/julewire/rails/logger_outputs.rb +36 -0
  24. data/lib/julewire/rails/output_requirement.rb +38 -0
  25. data/lib/julewire/rails/parameter_filter_plan.rb +100 -0
  26. data/lib/julewire/rails/parameter_filter_processor.rb +117 -0
  27. data/lib/julewire/rails/railtie.rb +84 -0
  28. data/lib/julewire/rails/request_attributes.rb +126 -0
  29. data/lib/julewire/rails/request_completion.rb +120 -0
  30. data/lib/julewire/rails/request_context.rb +91 -0
  31. data/lib/julewire/rails/request_error_ownership.rb +63 -0
  32. data/lib/julewire/rails/request_fields.rb +61 -0
  33. data/lib/julewire/rails/request_lifecycle.rb +109 -0
  34. data/lib/julewire/rails/request_middleware.rb +130 -0
  35. data/lib/julewire/rails/request_summary_timeout_scheduler.rb +38 -0
  36. data/lib/julewire/rails/structured_event_record.rb +128 -0
  37. data/lib/julewire/rails/subscribers/controller_response.rb +118 -0
  38. data/lib/julewire/rails/subscribers/error.rb +86 -0
  39. data/lib/julewire/rails/subscribers/event.rb +118 -0
  40. data/lib/julewire/rails/subscribers/rendered_exception.rb +141 -0
  41. data/lib/julewire/rails/suppression.rb +29 -0
  42. data/lib/julewire/rails/version.rb +7 -0
  43. data/lib/julewire/rails.rb +37 -0
  44. data/lib/julewire-rails.rb +3 -0
  45. 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