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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/logger"
4
+
5
+ module Julewire
6
+ module Rails
7
+ module LoggerOutputs
8
+ class << self
9
+ def install!
10
+ return if @installed
11
+
12
+ ::ActiveSupport::Logger.singleton_class.prepend(Patch)
13
+ @installed = true
14
+ end
15
+
16
+ def julewire_logger?(logger)
17
+ loggers = logger.respond_to?(:broadcasts) ? logger.broadcasts : [logger]
18
+ loggers.any?(Logger)
19
+ end
20
+
21
+ def console_sources?(sources)
22
+ sources.any? { it.equal?($stdout) || it.equal?($stderr) }
23
+ end
24
+ end
25
+
26
+ module Patch
27
+ def logger_outputs_to?(logger, *sources)
28
+ return true if LoggerOutputs.julewire_logger?(logger) && LoggerOutputs.console_sources?(sources)
29
+
30
+ super
31
+ end
32
+ end
33
+ private_constant :Patch
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Rails
5
+ module OutputRequirement
6
+ MESSAGE = "julewire-rails installed Rails.logger, but Julewire has no configured destinations. " \
7
+ "Configure Julewire.destinations or set config.julewire_rails.require_output = false."
8
+
9
+ class << self
10
+ def check!(settings, health: Julewire.health, warning: Warning)
11
+ return unless settings.logger?
12
+
13
+ mode = normalized_mode(settings.require_output)
14
+ return if mode == false || health.dig(:pipeline, :configured)
15
+
16
+ case mode
17
+ when :warn
18
+ warning.warn("#{MESSAGE}\n")
19
+ when :raise
20
+ raise Error, MESSAGE
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def normalized_mode(value)
27
+ case value
28
+ when false, nil then false
29
+ when true, :warn, "warn" then :warn
30
+ when :raise, "raise" then :raise
31
+ else
32
+ raise Error, "config.julewire_rails.require_output must be false, :warn, or :raise"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Rails
5
+ class ParameterFilterPlan
6
+ FILTERED = ::ActiveSupport::ParameterFilter::FILTERED
7
+ RECORD_CONTAINER_KEYS = Core::Processing::RecordFieldTransform.container_keys
8
+ RECORD_SCALAR_KEYS = Core::Processing::RecordFieldTransform.scalar_keys
9
+ private_constant :FILTERED
10
+ private_constant :RECORD_CONTAINER_KEYS
11
+ private_constant :RECORD_SCALAR_KEYS
12
+
13
+ attr_reader :filtered_field_keys
14
+
15
+ class << self
16
+ def build(filters)
17
+ filters = Array(filters)
18
+ return if filters.any? { it.is_a?(Regexp) || it.is_a?(Proc) }
19
+
20
+ new(filters)
21
+ end
22
+ end
23
+
24
+ def initialize(filters)
25
+ simple, deep = partition_filters(filters)
26
+ @filtered_field_keys = build_filtered_field_keys(simple, deep)
27
+ @direct_container_filter = simple.any? && deep.empty?
28
+ @simple_pattern = simple_filter_pattern(simple) if @direct_container_filter
29
+ end
30
+
31
+ def direct_container_filter? = @direct_container_filter
32
+
33
+ def filter_value(value)
34
+ return filter_hash(value) if value.is_a?(Hash)
35
+ return filter_array(value) if value.is_a?(Array)
36
+
37
+ value
38
+ end
39
+
40
+ private
41
+
42
+ def partition_filters(filters)
43
+ filters.map { it.to_s.downcase }.partition { !it.include?(".") }
44
+ end
45
+
46
+ def build_filtered_field_keys(simple, deep)
47
+ (container_filter_keys(simple, deep) + scalar_filter_keys(simple)).uniq.freeze
48
+ end
49
+
50
+ def container_filter_keys(simple, deep)
51
+ return RECORD_CONTAINER_KEYS if simple.any?
52
+
53
+ deep.filter_map { it.split(".", 2).first&.to_sym }
54
+ end
55
+
56
+ def scalar_filter_keys(simple)
57
+ RECORD_SCALAR_KEYS.filter do |key|
58
+ name = key_name(key)
59
+ simple.any? { name.include?(it) }
60
+ end
61
+ end
62
+
63
+ def simple_filter_pattern(filters)
64
+ Regexp.new(filters.map { Regexp.escape(it) }.join("|"), Regexp::IGNORECASE)
65
+ end
66
+
67
+ def filter_hash(value)
68
+ result = nil
69
+ value.each do |key, item|
70
+ filtered = simple_key_match?(key) ? FILTERED : filter_value(item)
71
+ next if filtered.equal?(item)
72
+
73
+ result ||= value.dup
74
+ result[key] = filtered
75
+ end
76
+ result || value
77
+ end
78
+
79
+ def filter_array(value)
80
+ result = nil
81
+ value.each_with_index do |item, index|
82
+ filtered = filter_value(item)
83
+ next if filtered.equal?(item)
84
+
85
+ result ||= value.dup
86
+ result[index] = filtered
87
+ end
88
+ result || value
89
+ end
90
+
91
+ def simple_key_match?(key)
92
+ @simple_pattern.match?(key_name(key))
93
+ end
94
+
95
+ def key_name(key)
96
+ key.is_a?(Symbol) ? key.name : key.to_s
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/parameter_filter"
4
+
5
+ module Julewire
6
+ module Rails
7
+ class ParameterFilterProcessor
8
+ EMPTY_CONTAINER_MARKER = Core.sentinel(:empty_container)
9
+ private_constant :EMPTY_CONTAINER_MARKER
10
+
11
+ def initialize(filters = rails_filter_parameters)
12
+ @filter = build_filter(filters)
13
+ # Rails exposes filter_param for scalar fields; use it to avoid whole-record
14
+ # copies when the filter list has no Proc semantics to preserve. Regex-only
15
+ # filters fall back to whole-record filtering unless they can scalarize a
16
+ # required record container.
17
+ @filter_param_fast_path = filter_param_safe?(filters) && @filter.respond_to?(:filter_param)
18
+ @field_plan = ParameterFilterPlan.build(filters) if @filter_param_fast_path
19
+ end
20
+
21
+ def call(draft)
22
+ validate_draft!(draft)
23
+ return if @filter.nil?
24
+
25
+ @filter_param_fast_path ? filter_draft_fields!(draft) : filter_whole_record!(draft)
26
+ end
27
+
28
+ private
29
+
30
+ def validate_draft!(draft)
31
+ return draft if draft.is_a?(Julewire::RecordDraft)
32
+
33
+ raise TypeError, "expected Julewire::RecordDraft"
34
+ end
35
+
36
+ def build_filter(filters)
37
+ return filters if filters.respond_to?(:filter) && !filters.is_a?(Array)
38
+
39
+ filters = Array(filters)
40
+ return if filters.empty?
41
+
42
+ ::ActiveSupport::ParameterFilter.new(::ActiveSupport::ParameterFilter.precompile_filters(filters))
43
+ end
44
+
45
+ def filter_param_safe?(filters)
46
+ return false if filters.respond_to?(:filter) && !filters.is_a?(Array)
47
+
48
+ filters = Array(filters)
49
+ return false if filters.any?(Proc)
50
+
51
+ filters.none?(Regexp) || filters.any? { regexp_matches_record_container?(it) }
52
+ end
53
+
54
+ def regexp_matches_record_container?(filter)
55
+ filter.is_a?(Regexp) && Core::Processing::RecordFieldTransform.container_keys.any? { filter.match?(it.name) }
56
+ end
57
+
58
+ def filter_draft_fields!(draft)
59
+ each_filter_field_key(draft) do |key|
60
+ next unless draft.key?(key)
61
+
62
+ value = draft[key]
63
+ next if skip_empty_container?(key, value)
64
+
65
+ filtered = filter_record_param(key, value)
66
+ draft.transform_field!(key) { filtered } unless filtered.equal?(value)
67
+ end
68
+ draft
69
+ end
70
+
71
+ def each_filter_field_key(draft, &)
72
+ return @field_plan.filtered_field_keys.each(&) if @field_plan&.filtered_field_keys
73
+
74
+ draft.each_key(&)
75
+ end
76
+
77
+ def filter_whole_record!(draft)
78
+ draft.transform_record! { @filter.filter(it) }
79
+ end
80
+
81
+ def filter_record_param(key, value)
82
+ if @field_plan&.direct_container_filter? && record_container_key?(key) && value.is_a?(Hash)
83
+ return @field_plan.filter_value(value)
84
+ end
85
+
86
+ filtered = @filter.filter_param(key, value)
87
+ return filtered unless record_container_key?(key) && !filtered.is_a?(Hash)
88
+ return value unless value.is_a?(Hash)
89
+
90
+ @filter.filter(value)
91
+ end
92
+
93
+ def skip_empty_container?(key, value)
94
+ return false unless empty_container?(value)
95
+ return true if record_container_key?(key)
96
+
97
+ @filter.filter_param(key, EMPTY_CONTAINER_MARKER).equal?(EMPTY_CONTAINER_MARKER)
98
+ end
99
+
100
+ def empty_container?(value)
101
+ (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
102
+ end
103
+
104
+ def record_container_key?(key)
105
+ Core::Processing::RecordFieldTransform.container_key?(key)
106
+ end
107
+
108
+ def rails_filter_parameters
109
+ app = ::Rails.application if defined?(::Rails) && ::Rails.respond_to?(:application)
110
+ config = app.config if app.respond_to?(:config)
111
+ config.filter_parameters if config.respond_to?(:filter_parameters)
112
+ rescue StandardError
113
+ []
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/tagged_logging"
4
+ require "rails/rack/logger"
5
+
6
+ module Julewire
7
+ module Rails
8
+ class Railtie < ::Rails::Railtie
9
+ config.julewire_rails = Configuration.new
10
+
11
+ initializer "julewire_rails.logger", before: :initialize_logger do |app|
12
+ settings = app.config.julewire_rails
13
+ settings.validate!
14
+ next unless settings.logger?
15
+
16
+ logger = Logger.new(name: settings.logger_name, source: settings.source)
17
+ logger.level = ::Logger::Severity.const_get(app.config.log_level.to_s.upcase)
18
+ logger.formatter = app.config.log_formatter if app.config.respond_to?(:log_formatter)
19
+ app.config.logger = ::ActiveSupport::TaggedLogging.new(logger)
20
+ LoggerOutputs.install!
21
+ end
22
+
23
+ initializer "julewire_rails.request_middleware", before: :build_middleware_stack do |app|
24
+ settings = app.config.julewire_rails
25
+ settings.validate!
26
+ next unless settings.request_middleware?
27
+
28
+ self.class.install_request_middleware(app, settings, app.config.log_tags)
29
+ end
30
+
31
+ initializer "julewire_rails.exception_logging", before: :build_middleware_stack do |app|
32
+ settings = app.config.julewire_rails
33
+ settings.validate!
34
+ self.class.configure_exception_logging(app, settings)
35
+ end
36
+
37
+ config.after_initialize do |app|
38
+ settings = app.config.julewire_rails
39
+ settings.validate!
40
+ OutputRequirement.check!(settings)
41
+ LifecycleHooks.install!(settings)
42
+ Railtie.install_subscribers(settings)
43
+ DebugExceptionLogSilencer.install!(settings)
44
+ end
45
+
46
+ class << self
47
+ def install_subscribers(settings)
48
+ Subscribers::ControllerResponse.install!(settings)
49
+ settings.error_reports? ? Subscribers::Error.install!(settings) : Subscribers::Error.reset!
50
+ Subscribers::RenderedException.install!(settings)
51
+ if settings.structured_events?
52
+ Subscribers::Event.install!(settings)
53
+ LogSubscriberSilencer.silence! if settings.silence_log_subscribers?
54
+ else
55
+ Subscribers::Event.reset!
56
+ end
57
+ end
58
+
59
+ def install_request_middleware(app, settings, log_tags = nil)
60
+ operation = settings.replace_rack_logger? ? :swap : :insert_after
61
+ app.config.middleware.public_send(operation, ::Rails::Rack::Logger, RequestMiddleware, settings, log_tags)
62
+ rescue StandardError => e
63
+ IntegrationHealth.record_failure(e, component: :request_middleware, action: :install)
64
+ raise
65
+ end
66
+
67
+ def configure_exception_logging(app, settings)
68
+ value = log_rescued_responses_value(settings)
69
+ app.config.action_dispatch.log_rescued_responses = value unless value.nil?
70
+ end
71
+
72
+ def log_rescued_responses_value(settings)
73
+ if settings.log_rescued_responses == :auto
74
+ return false if settings.logger? && settings.request_summary?
75
+
76
+ return
77
+ end
78
+
79
+ settings.log_rescued_responses
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Rails
5
+ module RequestAttributes
6
+ class << self
7
+ def context_fields(request)
8
+ request = request_fields(request)
9
+ values = Core::Integration::Values::Shape
10
+ {}.tap do |fields|
11
+ values.append_field(fields, :request_id, request.id)
12
+ values.append_field(fields, :http_method, request.method)
13
+ values.append_field(fields, :path, request.path)
14
+ values.append_field(fields, :remote_ip, request.remote_ip)
15
+ end
16
+ end
17
+
18
+ def request(request)
19
+ request_neutral_attributes(request)
20
+ end
21
+
22
+ def response_summary(request, status, headers)
23
+ {
24
+ attributes: { rails: rails_response_attributes(request, status, headers) },
25
+ neutral: neutral_fields(Core::Fields::AttributeKeys::HTTP_RESPONSE_STATUS_CODE => status)
26
+ }
27
+ end
28
+
29
+ def rendered_error_summary(request, rendered_error, status:)
30
+ error_summary(
31
+ request,
32
+ rendered_error.fetch(:error),
33
+ status: status,
34
+ wrapper: nil
35
+ ).tap do |fields|
36
+ fields[:attributes][:rails][:rescue_response] = rendered_error[:rescue_response]
37
+ fields[:attributes][:rails][:rescue_template] = rendered_error[:rescue_template]
38
+ end
39
+ end
40
+
41
+ def error_summary(request, error, status:, wrapper:)
42
+ {
43
+ attributes: { rails: rails_error_attributes(request, error, status: status, wrapper: wrapper) },
44
+ neutral: neutral_fields(Core::Fields::AttributeKeys::HTTP_RESPONSE_STATUS_CODE => status)
45
+ }
46
+ end
47
+
48
+ def request_id(request)
49
+ request_fields(request).id
50
+ end
51
+
52
+ private
53
+
54
+ def request_neutral_attributes(request)
55
+ request = request_fields(request)
56
+ values = Core::Integration::Values::Shape
57
+ fields = {}
58
+ values.append_field(
59
+ fields,
60
+ Core::Fields::AttributeKeys::HTTP_REQUEST_METHOD,
61
+ request.method
62
+ )
63
+ values.append_field(fields, Core::Fields::AttributeKeys::URL_FULL, request.filtered_url)
64
+ values.append_field(fields, Core::Fields::AttributeKeys::URL_PATH, request.path)
65
+ values.append_field(
66
+ fields,
67
+ Core::Fields::AttributeKeys::USER_AGENT_ORIGINAL,
68
+ request.user_agent
69
+ )
70
+ values.append_field(fields, Core::Fields::AttributeKeys::CLIENT_ADDRESS, request.remote_ip)
71
+ fields
72
+ end
73
+
74
+ def rails_response_attributes(request, status, headers)
75
+ rails_request_attributes(request).tap do |rails|
76
+ values = Core::Integration::Values::Shape
77
+ values.append_field(rails, :status, status)
78
+ values.append_field(rails, :response_content_type, response_header(headers, "content-type"))
79
+ end
80
+ end
81
+
82
+ def rails_error_attributes(request, error, status:, wrapper:)
83
+ rails_request_attributes(request).tap do |rails|
84
+ values = Core::Integration::Values::Shape
85
+ values.append_field(rails, :error_class, error.class.name)
86
+ values.append_field(rails, :status, status)
87
+ values.append_field(rails, :rescue_response, rescue_response?(wrapper))
88
+ values.append_field(rails, :rescue_template, rescue_template(wrapper))
89
+ end
90
+ end
91
+
92
+ def rails_request_attributes(request)
93
+ request = request_fields(request)
94
+ values = Core::Integration::Values::Shape
95
+ {}.tap do |fields|
96
+ values.append_field(fields, :filtered_url, request.filtered_url)
97
+ values.append_field(fields, :filtered_path, request.filtered_path)
98
+ values.append_field(fields, :request_method, request.method)
99
+ values.append_field(fields, :path, request.path)
100
+ values.append_field(fields, :user_agent, request.user_agent)
101
+ end
102
+ end
103
+
104
+ def rescue_response?(wrapper)
105
+ wrapper.rescue_response?
106
+ rescue StandardError
107
+ false
108
+ end
109
+
110
+ def rescue_template(wrapper)
111
+ wrapper.rescue_template
112
+ rescue StandardError
113
+ nil
114
+ end
115
+
116
+ def response_header(headers, key)
117
+ Julewire::Rack::Capture::BodyContentType.header_value(headers, key)
118
+ end
119
+
120
+ def request_fields(request) = RequestFields.new(request)
121
+
122
+ def neutral_fields(fields) = Core::Fields::AttributeKeys.fields(fields)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Rails
5
+ class RequestCompletion
6
+ class << self
7
+ def finish_instrumentation(instrumenter_handle)
8
+ instrumenter_handle&.finish
9
+ rescue StandardError
10
+ nil
11
+ ensure
12
+ begin
13
+ ::ActiveSupport::LogSubscriber.flush_all!
14
+ rescue StandardError
15
+ nil
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(configuration:, execution_handle:, instrumenter_handle:, env:, request:, request_error:)
21
+ @configuration = configuration
22
+ @execution_handle = execution_handle
23
+ @instrumenter_handle = instrumenter_handle
24
+ @env = env
25
+ @request = request
26
+ @request_error = request_error
27
+ end
28
+
29
+ def attach(response)
30
+ status, headers, body = response
31
+ timeout_token = nil
32
+ finish_once = completion_callback { RequestSummaryTimeoutScheduler.cancel(timeout_token) }
33
+ install_response_finished_callback(finish_once)
34
+ timeout_token = install_completion_timeout
35
+ body = ContextBodyProxy.new(body, handle: @execution_handle, on_close: -> { finish_once.call(nil) })
36
+ response.frozen? ? [status, headers, body] : response.tap { it[2] = body }
37
+ end
38
+
39
+ private
40
+
41
+ def install_completion_timeout
42
+ timeout = @configuration.request_summary_timeout
43
+ return unless timeout
44
+
45
+ context = completion_timeout_context
46
+ RequestSummaryTimeoutScheduler.schedule(timeout) { emit_completion_timeout_warning(timeout, context) }
47
+ end
48
+
49
+ def install_response_finished_callback(finish_once)
50
+ response_finished = @env["rack.response_finished"]
51
+ return unless response_finished.respond_to?(:<<)
52
+
53
+ response_finished << proc do |_rack_env, _status, _headers, error|
54
+ finish_once.call(error)
55
+ end
56
+ end
57
+
58
+ def completion_callback
59
+ mutex = Mutex.new
60
+ finished = false
61
+ lambda do |error|
62
+ mutex.synchronize do
63
+ return if finished
64
+
65
+ finished = true
66
+ end
67
+ yield if block_given?
68
+ @execution_handle.with_context { finish_completion(error) }
69
+ end
70
+ end
71
+
72
+ def finish_completion(error)
73
+ self.class.finish_instrumentation(@instrumenter_handle)
74
+ if error
75
+ @execution_handle.finish(
76
+ reason: :error,
77
+ attributes: { rails: { completion: "error", completion_error_class: error.class.name } },
78
+ error: error
79
+ )
80
+ elsif @request_error
81
+ @execution_handle.finish(
82
+ reason: :error,
83
+ attributes: { rails: { completion: "error" } },
84
+ error: @request_error.fetch(:error),
85
+ severity: @request_error.fetch(:severity)
86
+ )
87
+ else
88
+ @execution_handle.finish(reason: :closed, attributes: { rails: { completion: "closed" } })
89
+ end
90
+ end
91
+
92
+ def completion_timeout_context
93
+ return {} unless @configuration.request_context?
94
+
95
+ {
96
+ request_id: request_id,
97
+ path: @request.path
98
+ }.compact
99
+ end
100
+
101
+ def emit_completion_timeout_warning(timeout, context)
102
+ record = {
103
+ event: "request.completion_timeout",
104
+ logger: @configuration.logger_name,
105
+ source: @configuration.source,
106
+ attributes: { rails: { completion_timeout_ms: (timeout * 1000).round } }
107
+ }
108
+ record[:context] = context unless context.empty?
109
+ Julewire.warn(record)
110
+ rescue StandardError
111
+ nil
112
+ end
113
+
114
+ def request_id
115
+ value = @request.request_id if @request.respond_to?(:request_id)
116
+ value || @request.get_header("action_dispatch.request_id") || @request.get_header("HTTP_X_REQUEST_ID")
117
+ end
118
+ end
119
+ end
120
+ end