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,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
class RequestContext
|
|
6
|
+
def initialize(configuration:, request:, active_support_context: Core::UNSET, event_reporter: Core::UNSET)
|
|
7
|
+
@configuration = configuration
|
|
8
|
+
@request = request
|
|
9
|
+
@active_support_context = default_provider(active_support_context) { ::ActiveSupport::ExecutionContext }
|
|
10
|
+
@event_reporter = default_provider(event_reporter) { Julewire::RailsSupport::EventReporter.default }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def neutral_fields
|
|
14
|
+
RequestAttributes.request(@request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(&)
|
|
18
|
+
with_request_carry do
|
|
19
|
+
with_request_context(&)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def default_provider(value)
|
|
26
|
+
value.equal?(Core::UNSET) ? yield : value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with_request_context(&)
|
|
30
|
+
return yield unless @configuration.request_context?
|
|
31
|
+
|
|
32
|
+
fields = RequestAttributes.context_fields(@request)
|
|
33
|
+
Core::Integration::Facade.with_context(fields) do
|
|
34
|
+
with_active_support_execution_context(fields) do
|
|
35
|
+
with_rails_event_context(fields, &)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def with_request_carry(&)
|
|
41
|
+
return yield unless @configuration.carry_request_headers
|
|
42
|
+
raise ArgumentError, "carry_request_headers must be an explicit header list" if all_carry_headers?
|
|
43
|
+
|
|
44
|
+
headers = Julewire::Rack::Capture::Headers.request(@request, selector: @configuration.carry_request_headers)
|
|
45
|
+
return yield if headers.empty?
|
|
46
|
+
|
|
47
|
+
Core::Integration::Facade.with_carry(http: { request_headers: headers }, &)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def all_carry_headers?
|
|
51
|
+
@configuration.carry_request_headers == true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def with_active_support_execution_context(fields, &)
|
|
55
|
+
return yield unless @active_support_context.respond_to?(:set)
|
|
56
|
+
|
|
57
|
+
@active_support_context.set(**fields, &)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_rails_event_context(fields)
|
|
61
|
+
return yield unless rails_event_context_supported?
|
|
62
|
+
|
|
63
|
+
previous = nil
|
|
64
|
+
context_set = false
|
|
65
|
+
begin
|
|
66
|
+
previous = @event_reporter.context
|
|
67
|
+
@event_reporter.set_context(fields)
|
|
68
|
+
context_set = true
|
|
69
|
+
rescue StandardError
|
|
70
|
+
return yield
|
|
71
|
+
end
|
|
72
|
+
yield
|
|
73
|
+
ensure
|
|
74
|
+
restore_rails_event_context(previous) if context_set
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rails_event_context_supported?
|
|
78
|
+
@event_reporter.respond_to?(:context) &&
|
|
79
|
+
@event_reporter.respond_to?(:set_context) &&
|
|
80
|
+
@event_reporter.respond_to?(:clear_context)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def restore_rails_event_context(previous)
|
|
84
|
+
@event_reporter.clear_context
|
|
85
|
+
@event_reporter.set_context(previous) unless previous.nil? || previous.empty?
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/isolated_execution_state"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Rails
|
|
7
|
+
module RequestErrorOwnership
|
|
8
|
+
KEY = :julewire_rails_request_error_objects
|
|
9
|
+
private_constant :KEY
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def clear
|
|
13
|
+
::ActiveSupport::IsolatedExecutionState.delete(KEY)
|
|
14
|
+
rescue StandardError
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def mark(error)
|
|
19
|
+
return unless error
|
|
20
|
+
|
|
21
|
+
errors = error_map
|
|
22
|
+
each_exception(error) { errors[it] = true }
|
|
23
|
+
rescue StandardError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def consume?(error)
|
|
28
|
+
errors = current_error_map
|
|
29
|
+
return false unless errors
|
|
30
|
+
|
|
31
|
+
each_exception(error).any? { errors.delete(it) }
|
|
32
|
+
rescue StandardError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def error_map
|
|
39
|
+
current_error_map || set_error_map
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_error_map
|
|
43
|
+
ObjectSpace::WeakKeyMap.new.tap { ::ActiveSupport::IsolatedExecutionState[KEY] = it }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def current_error_map
|
|
47
|
+
::ActiveSupport::IsolatedExecutionState[KEY]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def each_exception(error)
|
|
51
|
+
return enum_for(:each_exception, error) unless block_given?
|
|
52
|
+
|
|
53
|
+
seen = {}.compare_by_identity
|
|
54
|
+
while error && !seen.key?(error)
|
|
55
|
+
yield error
|
|
56
|
+
seen[error] = true
|
|
57
|
+
error = error.cause
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
class RequestFields
|
|
6
|
+
REMOTE_IP_ENV_KEY = "julewire.rails.remote_ip"
|
|
7
|
+
|
|
8
|
+
def initialize(request)
|
|
9
|
+
@request = request
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def id
|
|
13
|
+
value = @request.request_id if @request.respond_to?(:request_id)
|
|
14
|
+
value || header("action_dispatch.request_id") || header("HTTP_X_REQUEST_ID")
|
|
15
|
+
rescue StandardError
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def method
|
|
20
|
+
@request.request_method
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def path
|
|
24
|
+
@request.path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filtered_path
|
|
28
|
+
@request.filtered_path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def filtered_url
|
|
32
|
+
"#{@request.protocol}#{@request.host_with_port}#{filtered_path}"
|
|
33
|
+
rescue StandardError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def user_agent
|
|
38
|
+
header("HTTP_USER_AGENT")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remote_ip
|
|
42
|
+
env = @request.env if @request.respond_to?(:env)
|
|
43
|
+
return env[REMOTE_IP_ENV_KEY] if env&.key?(REMOTE_IP_ENV_KEY)
|
|
44
|
+
|
|
45
|
+
value = @request.remote_ip
|
|
46
|
+
env[REMOTE_IP_ENV_KEY] = value if env
|
|
47
|
+
value
|
|
48
|
+
rescue StandardError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def header(key)
|
|
55
|
+
@request.get_header(key)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
class RequestLifecycle
|
|
6
|
+
attr_reader :execution_handle
|
|
7
|
+
|
|
8
|
+
def initialize(configuration:, env:, request:, taggers:)
|
|
9
|
+
@configuration = configuration
|
|
10
|
+
@env = env
|
|
11
|
+
@request = request
|
|
12
|
+
@taggers = taggers
|
|
13
|
+
@completion_attached = false
|
|
14
|
+
@execution_handle = nil
|
|
15
|
+
@instrumenter_handle = nil
|
|
16
|
+
@tag_count = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start
|
|
20
|
+
@tag_count = push_tags
|
|
21
|
+
@instrumenter_handle = start_request_instrumentation
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start_execution!(neutral:)
|
|
26
|
+
@execution_handle = Julewire.start_execution(
|
|
27
|
+
type: :request,
|
|
28
|
+
id: RequestAttributes.request_id(@request),
|
|
29
|
+
neutral: neutral,
|
|
30
|
+
emit_summary: @configuration.request_summary?,
|
|
31
|
+
summary_event: @configuration.summary_event,
|
|
32
|
+
summary_source: @configuration.source
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def attach_body_finalizer(response)
|
|
37
|
+
finish_request_thread_logging
|
|
38
|
+
@completion_attached = true
|
|
39
|
+
RequestCompletion.new(
|
|
40
|
+
configuration: @configuration,
|
|
41
|
+
execution_handle: @execution_handle,
|
|
42
|
+
instrumenter_handle: @instrumenter_handle,
|
|
43
|
+
env: @env,
|
|
44
|
+
request: @request,
|
|
45
|
+
request_error: @env[RequestMiddleware::REQUEST_ERROR_ENV_KEY]
|
|
46
|
+
).attach(response)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def finish_error(error)
|
|
50
|
+
@execution_handle&.finish(reason: :error, error: error)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def finish_unattached
|
|
54
|
+
return if @completion_attached
|
|
55
|
+
|
|
56
|
+
finish_unattached_request
|
|
57
|
+
finish_request_thread_logging
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def push_tags
|
|
63
|
+
return 0 unless ::Rails.logger.respond_to?(:push_tags)
|
|
64
|
+
|
|
65
|
+
::Rails.logger.push_tags(*compute_tags).size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def compute_tags
|
|
69
|
+
@taggers.collect do |tag|
|
|
70
|
+
case tag
|
|
71
|
+
when Proc
|
|
72
|
+
tag.call(@request)
|
|
73
|
+
when Symbol
|
|
74
|
+
@request.public_send(tag)
|
|
75
|
+
else
|
|
76
|
+
tag
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def start_request_instrumentation
|
|
82
|
+
handle = ::ActiveSupport::Notifications.instrumenter.build_handle(
|
|
83
|
+
"request.action_dispatch",
|
|
84
|
+
request: @request
|
|
85
|
+
)
|
|
86
|
+
handle.start
|
|
87
|
+
handle
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def finish_request_thread_logging
|
|
91
|
+
return unless @tag_count.to_i.positive? && ::Rails.logger.respond_to?(:pop_tags)
|
|
92
|
+
|
|
93
|
+
::Rails.logger.pop_tags(@tag_count)
|
|
94
|
+
@tag_count = 0
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def finish_unattached_request
|
|
100
|
+
return RequestCompletion.finish_instrumentation(@instrumenter_handle) unless @execution_handle
|
|
101
|
+
|
|
102
|
+
@execution_handle.with_context do
|
|
103
|
+
RequestCompletion.finish_instrumentation(@instrumenter_handle)
|
|
104
|
+
@execution_handle.finish(reason: :closed)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_dispatch/http/request"
|
|
4
|
+
require "action_dispatch/middleware/exception_wrapper"
|
|
5
|
+
require "action_view"
|
|
6
|
+
require "active_support/log_subscriber"
|
|
7
|
+
|
|
8
|
+
module Julewire
|
|
9
|
+
module Rails
|
|
10
|
+
class RequestMiddleware
|
|
11
|
+
REQUEST_ERROR_ENV_KEY = "julewire.rails.request_error"
|
|
12
|
+
RENDERED_EXCEPTION_ENV_KEY = "julewire.rails.rendered_exception"
|
|
13
|
+
|
|
14
|
+
def initialize(app, configuration = Configuration.new, taggers = nil)
|
|
15
|
+
@app = app
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
@taggers = taggers || []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call(env)
|
|
21
|
+
lifecycle = nil
|
|
22
|
+
request = ::ActionDispatch::Request.new(env)
|
|
23
|
+
RequestErrorOwnership.clear
|
|
24
|
+
return Suppression.suppress { @app.call(env) } if excluded_request?(request)
|
|
25
|
+
|
|
26
|
+
lifecycle = RequestLifecycle.new(
|
|
27
|
+
configuration: @configuration,
|
|
28
|
+
env: env,
|
|
29
|
+
request: request,
|
|
30
|
+
taggers: @taggers
|
|
31
|
+
).start
|
|
32
|
+
request_context = RequestContext.new(configuration: @configuration, request: request)
|
|
33
|
+
|
|
34
|
+
response = request_context.call do
|
|
35
|
+
execution_handle = lifecycle.start_execution!(neutral: request_context.neutral_fields)
|
|
36
|
+
execution_handle.run do
|
|
37
|
+
call_app(request, env, execution_handle)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
lifecycle.attach_body_finalizer(response)
|
|
42
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- Rack middleware must re-raise all application exits.
|
|
43
|
+
lifecycle&.finish_error(e)
|
|
44
|
+
raise
|
|
45
|
+
ensure
|
|
46
|
+
lifecycle&.finish_unattached
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def excluded_request?(request)
|
|
52
|
+
@configuration.request_exclude_prefixes.any? { excluded_path?(request.path, it) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def excluded_path?(path, prefix)
|
|
56
|
+
return true if prefix == "/"
|
|
57
|
+
|
|
58
|
+
path == prefix || path.start_with?("#{prefix}/")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def call_app(request, env, execution_handle)
|
|
62
|
+
status, headers, body = @app.call(env)
|
|
63
|
+
add_summary_fields(response_summary_attributes(request, status, headers))
|
|
64
|
+
capture_rendered_request_error(request, env, status)
|
|
65
|
+
[status, headers, body]
|
|
66
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- Rack middleware must re-raise all application exits.
|
|
67
|
+
severity = request_exception_severity(request)
|
|
68
|
+
wrapper = exception_wrapper(request, e)
|
|
69
|
+
own_request_error(env, e, severity: severity)
|
|
70
|
+
add_summary_fields(error_summary_attributes(request, e, status: 500, wrapper: wrapper))
|
|
71
|
+
execution_handle.finish(reason: :error, error: e, severity: severity)
|
|
72
|
+
raise
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def response_summary_attributes(request, status, headers)
|
|
76
|
+
RequestAttributes.response_summary(request, status, headers)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def capture_rendered_request_error(request, env, status)
|
|
80
|
+
rendered_error = env[RENDERED_EXCEPTION_ENV_KEY]
|
|
81
|
+
if rendered_error
|
|
82
|
+
own_request_error(env, rendered_error.fetch(:error), severity: rendered_error.fetch(:severity))
|
|
83
|
+
add_summary_fields(rendered_error_summary_attributes(request, rendered_error, status: status))
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
error = env["action_dispatch.exception"]
|
|
88
|
+
return unless error
|
|
89
|
+
|
|
90
|
+
severity = request_exception_severity(request)
|
|
91
|
+
wrapper = exception_wrapper(request, error)
|
|
92
|
+
own_request_error(env, error, severity: severity)
|
|
93
|
+
add_summary_fields(error_summary_attributes(request, error, status: status, wrapper: wrapper))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def own_request_error(env, error, severity:)
|
|
97
|
+
return unless @configuration.request_summary?
|
|
98
|
+
|
|
99
|
+
env[REQUEST_ERROR_ENV_KEY] = { error: error, severity: severity }
|
|
100
|
+
RequestErrorOwnership.mark(error)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def rendered_error_summary_attributes(request, rendered_error, status:)
|
|
104
|
+
RequestAttributes.rendered_error_summary(
|
|
105
|
+
request,
|
|
106
|
+
rendered_error,
|
|
107
|
+
status: status
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def error_summary_attributes(request, error, status:, wrapper:)
|
|
112
|
+
RequestAttributes.error_summary(request, error, status: status, wrapper: wrapper)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def request_exception_severity(request)
|
|
116
|
+
ExceptionSeverity.for_request(request)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def exception_wrapper(request, error)
|
|
120
|
+
cleaner = request.get_header("action_dispatch.backtrace_cleaner")
|
|
121
|
+
::ActionDispatch::ExceptionWrapper.new(cleaner, error)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def add_summary_fields(fields)
|
|
125
|
+
Core::Integration::Facade.add_summary_attributes(fields[:attributes])
|
|
126
|
+
Core::Integration::Facade.add_summary_neutral(fields[:neutral])
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Rails
|
|
5
|
+
module RequestSummaryTimeoutScheduler
|
|
6
|
+
class << self
|
|
7
|
+
def schedule(timeout, &block)
|
|
8
|
+
return unless timeout && block
|
|
9
|
+
|
|
10
|
+
Core::Scheduling::SharedScheduler.schedule(timeout, &block)
|
|
11
|
+
rescue StandardError
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cancel(token)
|
|
16
|
+
return unless token
|
|
17
|
+
|
|
18
|
+
Core::Scheduling::SharedScheduler.cancel(token)
|
|
19
|
+
rescue StandardError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Private testing seam for request-timeout isolation.
|
|
24
|
+
def reset_for_test!
|
|
25
|
+
Core::Scheduling::SharedScheduler.__send__(:reset_for_test!)
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def after_fork!
|
|
30
|
+
Core::Scheduling::SharedScheduler.after_fork!
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private :reset_for_test!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/parameter_filter"
|
|
4
|
+
|
|
5
|
+
module Julewire
|
|
6
|
+
module Rails
|
|
7
|
+
class StructuredEventRecord
|
|
8
|
+
DEBUG_EVENT_PREFIXES = %w[
|
|
9
|
+
action_view.
|
|
10
|
+
active_record.
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
DEBUG_EVENTS = %w[
|
|
14
|
+
action_controller.unpermitted_parameters
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def initialize(configuration, parameter_filter: Core::UNSET)
|
|
18
|
+
@configuration = configuration
|
|
19
|
+
@parameter_filter_override = parameter_filter
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(event, name:, payload:)
|
|
23
|
+
values = Core::Integration::Values::Shape
|
|
24
|
+
{
|
|
25
|
+
timestamp: values.timestamp(event[:timestamp]),
|
|
26
|
+
severity: severity_for(name),
|
|
27
|
+
event: name,
|
|
28
|
+
logger: "Rails.event",
|
|
29
|
+
source: @configuration.source,
|
|
30
|
+
context: values.hash_or_empty(event[:context]),
|
|
31
|
+
attributes: attributes_for(event, payload),
|
|
32
|
+
neutral: neutral_for(event)
|
|
33
|
+
}.compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def payload_hash(payload)
|
|
37
|
+
case payload
|
|
38
|
+
when nil
|
|
39
|
+
{}
|
|
40
|
+
when Hash
|
|
41
|
+
values = Core::Integration::Values::Shape
|
|
42
|
+
values.payload_hash(payload)
|
|
43
|
+
else
|
|
44
|
+
serialize_payload_object(payload)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def attributes_for(event, payload)
|
|
51
|
+
values = Core::Integration::Values::Shape
|
|
52
|
+
rails = payload.empty? ? {} : payload
|
|
53
|
+
values.append_compact_field(rails, :tags, values.hash_or_empty(event[:tags]))
|
|
54
|
+
{ rails: rails }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def neutral_for(event)
|
|
58
|
+
values = Core::Integration::Values::Shape
|
|
59
|
+
values.source_location_attributes(event[:source_location])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def severity_for(name)
|
|
63
|
+
return :debug if DEBUG_EVENTS.include?(name)
|
|
64
|
+
return :debug if DEBUG_EVENT_PREFIXES.any? { name.start_with?(it) }
|
|
65
|
+
|
|
66
|
+
:info
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def serialize_payload_object(payload)
|
|
70
|
+
if payload.respond_to?(:serialize)
|
|
71
|
+
serialized = payload.serialize
|
|
72
|
+
return object_payload_hash(serialized) if serialized.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
{ Julewire::Core::Fields::FieldSet::VALUE_KEY => serialized }
|
|
75
|
+
else
|
|
76
|
+
{ Julewire::Core::Fields::FieldSet::VALUE_KEY => payload }
|
|
77
|
+
end
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
{
|
|
80
|
+
Julewire::Core::Fields::FieldSet::VALUE_KEY => payload,
|
|
81
|
+
serialize_error_class: e.class.name
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def object_payload_hash(serialized)
|
|
86
|
+
Julewire::Core::Fields::FieldSet.deep_symbolize_keys(filter_event_payload(serialized))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def filter_event_payload(payload)
|
|
90
|
+
return payload unless @configuration.filter_event_payloads?
|
|
91
|
+
|
|
92
|
+
filter = rails_parameter_filter
|
|
93
|
+
return payload unless filter
|
|
94
|
+
|
|
95
|
+
filtered = filter.filter(payload)
|
|
96
|
+
filtered.is_a?(Hash) ? filtered : payload
|
|
97
|
+
rescue StandardError
|
|
98
|
+
payload
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def rails_parameter_filter
|
|
102
|
+
return @parameter_filter_override unless @parameter_filter_override.equal?(Core::UNSET)
|
|
103
|
+
return @rails_parameter_filter if @rails_parameter_filter_loaded
|
|
104
|
+
|
|
105
|
+
@rails_parameter_filter_loaded = true
|
|
106
|
+
filters = rails_filter_parameters
|
|
107
|
+
@rails_parameter_filter = build_parameter_filter(filters)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_parameter_filter(filters)
|
|
111
|
+
return if filters.empty?
|
|
112
|
+
|
|
113
|
+
::ActiveSupport::ParameterFilter.new(::ActiveSupport::ParameterFilter.precompile_filters(filters))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def rails_filter_parameters
|
|
117
|
+
app = ::Rails.application if defined?(::Rails) && ::Rails.respond_to?(:application)
|
|
118
|
+
return Array(app.filter_parameters) if app.respond_to?(:filter_parameters)
|
|
119
|
+
|
|
120
|
+
config = app.config if app.respond_to?(:config)
|
|
121
|
+
filters = config.filter_parameters if config.respond_to?(:filter_parameters)
|
|
122
|
+
Array(filters)
|
|
123
|
+
rescue StandardError
|
|
124
|
+
[]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|