logstruct 0.0.1 → 0.0.2.pre.rc2
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 +4 -4
- data/CHANGELOG.md +26 -2
- data/LICENSE +21 -0
- data/README.md +67 -0
- data/lib/log_struct/concerns/configuration.rb +93 -0
- data/lib/log_struct/concerns/error_handling.rb +94 -0
- data/lib/log_struct/concerns/logging.rb +45 -0
- data/lib/log_struct/config_struct/error_handling_modes.rb +25 -0
- data/lib/log_struct/config_struct/filters.rb +80 -0
- data/lib/log_struct/config_struct/integrations.rb +89 -0
- data/lib/log_struct/configuration.rb +59 -0
- data/lib/log_struct/enums/error_handling_mode.rb +22 -0
- data/lib/log_struct/enums/error_reporter.rb +14 -0
- data/lib/log_struct/enums/event.rb +48 -0
- data/lib/log_struct/enums/level.rb +66 -0
- data/lib/log_struct/enums/source.rb +26 -0
- data/lib/log_struct/enums.rb +9 -0
- data/lib/log_struct/formatter.rb +224 -0
- data/lib/log_struct/handlers.rb +27 -0
- data/lib/log_struct/hash_utils.rb +21 -0
- data/lib/log_struct/integrations/action_mailer/callbacks.rb +100 -0
- data/lib/log_struct/integrations/action_mailer/error_handling.rb +173 -0
- data/lib/log_struct/integrations/action_mailer/event_logging.rb +90 -0
- data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +78 -0
- data/lib/log_struct/integrations/action_mailer.rb +50 -0
- data/lib/log_struct/integrations/active_job/log_subscriber.rb +104 -0
- data/lib/log_struct/integrations/active_job.rb +38 -0
- data/lib/log_struct/integrations/active_record.rb +258 -0
- data/lib/log_struct/integrations/active_storage.rb +94 -0
- data/lib/log_struct/integrations/carrierwave.rb +111 -0
- data/lib/log_struct/integrations/good_job/log_subscriber.rb +228 -0
- data/lib/log_struct/integrations/good_job/logger.rb +73 -0
- data/lib/log_struct/integrations/good_job.rb +111 -0
- data/lib/log_struct/integrations/host_authorization.rb +81 -0
- data/lib/log_struct/integrations/integration_interface.rb +21 -0
- data/lib/log_struct/integrations/lograge.rb +114 -0
- data/lib/log_struct/integrations/rack.rb +31 -0
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +146 -0
- data/lib/log_struct/integrations/rack_error_handler.rb +32 -0
- data/lib/log_struct/integrations/shrine.rb +75 -0
- data/lib/log_struct/integrations/sidekiq/logger.rb +43 -0
- data/lib/log_struct/integrations/sidekiq.rb +39 -0
- data/lib/log_struct/integrations/sorbet.rb +49 -0
- data/lib/log_struct/integrations.rb +41 -0
- data/lib/log_struct/log/action_mailer.rb +55 -0
- data/lib/log_struct/log/active_job.rb +64 -0
- data/lib/log_struct/log/active_storage.rb +78 -0
- data/lib/log_struct/log/carrierwave.rb +82 -0
- data/lib/log_struct/log/error.rb +76 -0
- data/lib/log_struct/log/good_job.rb +151 -0
- data/lib/log_struct/log/interfaces/additional_data_field.rb +20 -0
- data/lib/log_struct/log/interfaces/common_fields.rb +42 -0
- data/lib/log_struct/log/interfaces/message_field.rb +20 -0
- data/lib/log_struct/log/interfaces/request_fields.rb +36 -0
- data/lib/log_struct/log/plain.rb +53 -0
- data/lib/log_struct/log/request.rb +76 -0
- data/lib/log_struct/log/security.rb +80 -0
- data/lib/log_struct/log/shared/add_request_fields.rb +29 -0
- data/lib/log_struct/log/shared/merge_additional_data_fields.rb +28 -0
- data/lib/log_struct/log/shared/serialize_common.rb +36 -0
- data/lib/log_struct/log/shrine.rb +70 -0
- data/lib/log_struct/log/sidekiq.rb +50 -0
- data/lib/log_struct/log/sql.rb +126 -0
- data/lib/log_struct/log.rb +43 -0
- data/lib/log_struct/log_keys.rb +102 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +36 -0
- data/lib/log_struct/multi_error_reporter.rb +149 -0
- data/lib/log_struct/param_filters.rb +89 -0
- data/lib/log_struct/railtie.rb +31 -0
- data/lib/log_struct/semantic_logger/color_formatter.rb +209 -0
- data/lib/log_struct/semantic_logger/formatter.rb +94 -0
- data/lib/log_struct/semantic_logger/logger.rb +129 -0
- data/lib/log_struct/semantic_logger/setup.rb +219 -0
- data/lib/log_struct/sorbet/serialize_symbol_keys.rb +23 -0
- data/lib/log_struct/sorbet.rb +13 -0
- data/lib/log_struct/string_scrubber.rb +84 -0
- data/lib/log_struct/version.rb +6 -0
- data/lib/log_struct.rb +37 -0
- data/lib/logstruct.rb +2 -6
- data/logstruct.gemspec +52 -0
- metadata +221 -5
- data/Rakefile +0 -5
@@ -0,0 +1,48 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
# Define log event types as an enum
|
6
|
+
class Event < T::Enum
|
7
|
+
enums do
|
8
|
+
# Plain log messages
|
9
|
+
Log = new(:log)
|
10
|
+
|
11
|
+
# Request events
|
12
|
+
Request = new(:request)
|
13
|
+
|
14
|
+
# Job events
|
15
|
+
Enqueue = new(:enqueue)
|
16
|
+
Schedule = new(:schedule)
|
17
|
+
Start = new(:start)
|
18
|
+
Finish = new(:finish)
|
19
|
+
|
20
|
+
# File storage events (ActiveStorage, Shrine, CarrierWave, etc.)
|
21
|
+
Upload = new(:upload)
|
22
|
+
Download = new(:download)
|
23
|
+
Delete = new(:delete)
|
24
|
+
Metadata = new(:metadata)
|
25
|
+
Exist = new(:exist)
|
26
|
+
Stream = new(:stream)
|
27
|
+
Url = new(:url)
|
28
|
+
|
29
|
+
# Email events
|
30
|
+
Delivery = new(:delivery)
|
31
|
+
Delivered = new(:delivered)
|
32
|
+
|
33
|
+
# Security events
|
34
|
+
IPSpoof = new(:ip_spoof)
|
35
|
+
CSRFViolation = new(:csrf_violation)
|
36
|
+
BlockedHost = new(:blocked_host)
|
37
|
+
|
38
|
+
# Database events
|
39
|
+
Database = new(:database)
|
40
|
+
|
41
|
+
# Error events
|
42
|
+
Error = new(:error)
|
43
|
+
|
44
|
+
# Fallback
|
45
|
+
Unknown = new(:unknown)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "logger"
|
5
|
+
|
6
|
+
module LogStruct
|
7
|
+
# Define log levels as an enum
|
8
|
+
class Level < T::Enum
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
enums do
|
12
|
+
# Standard log levels
|
13
|
+
Debug = new(:debug)
|
14
|
+
Info = new(:info)
|
15
|
+
Warn = new(:warn)
|
16
|
+
Error = new(:error)
|
17
|
+
Fatal = new(:fatal)
|
18
|
+
Unknown = new(:unknown)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Convert a Level to the corresponding Logger integer constant
|
22
|
+
sig { returns(Integer) }
|
23
|
+
def to_severity_int
|
24
|
+
case serialize
|
25
|
+
when :debug then ::Logger::DEBUG
|
26
|
+
when :info then ::Logger::INFO
|
27
|
+
when :warn then ::Logger::WARN
|
28
|
+
when :error then ::Logger::ERROR
|
29
|
+
when :fatal then ::Logger::FATAL
|
30
|
+
else ::Logger::UNKNOWN
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert a string or integer severity to a Level
|
35
|
+
sig { params(severity: T.any(String, Symbol, Integer, NilClass)).returns(Level) }
|
36
|
+
def self.from_severity(severity)
|
37
|
+
return Unknown if severity.nil?
|
38
|
+
return from_severity_int(severity) if severity.is_a?(Integer)
|
39
|
+
from_severity_sym(severity.downcase.to_sym)
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(severity: Symbol).returns(Level) }
|
43
|
+
def self.from_severity_sym(severity)
|
44
|
+
case severity.to_s.downcase.to_sym
|
45
|
+
when :debug then Debug
|
46
|
+
when :info then Info
|
47
|
+
when :warn then Warn
|
48
|
+
when :error then Error
|
49
|
+
when :fatal then Fatal
|
50
|
+
else Unknown
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(severity: Integer).returns(Level) }
|
55
|
+
def self.from_severity_int(severity)
|
56
|
+
case severity
|
57
|
+
when ::Logger::DEBUG then Debug
|
58
|
+
when ::Logger::INFO then Info
|
59
|
+
when ::Logger::WARN then Warn
|
60
|
+
when ::Logger::ERROR then Error
|
61
|
+
when ::Logger::FATAL then Fatal
|
62
|
+
else Unknown
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
# Combined Source class that unifies log and error sources
|
6
|
+
class Source < T::Enum
|
7
|
+
enums do
|
8
|
+
# Error sources
|
9
|
+
TypeChecking = new(:type_checking) # For type checking errors (Sorbet)
|
10
|
+
LogStruct = new(:logstruct) # Errors from LogStruct itself
|
11
|
+
Security = new(:security) # Security-related events
|
12
|
+
|
13
|
+
# Application sources
|
14
|
+
Rails = new(:rails) # For request-related logs/errors
|
15
|
+
Job = new(:job) # ActiveJob logs/errors
|
16
|
+
Storage = new(:storage) # ActiveStorage logs/errors
|
17
|
+
Mailer = new(:mailer) # ActionMailer logs/errors
|
18
|
+
App = new(:app) # General application logs/errors
|
19
|
+
|
20
|
+
# Third-party gem sources
|
21
|
+
Shrine = new(:shrine)
|
22
|
+
CarrierWave = new(:carrierwave)
|
23
|
+
Sidekiq = new(:sidekiq)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Require all enums in this directory
|
5
|
+
require_relative "enums/error_handling_mode"
|
6
|
+
require_relative "enums/error_reporter"
|
7
|
+
require_relative "enums/event"
|
8
|
+
require_relative "enums/level"
|
9
|
+
require_relative "enums/source"
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "logger"
|
5
|
+
require "active_support/core_ext/object/blank"
|
6
|
+
require "json"
|
7
|
+
require "globalid"
|
8
|
+
require_relative "enums/source"
|
9
|
+
require_relative "enums/event"
|
10
|
+
require_relative "string_scrubber"
|
11
|
+
require_relative "log"
|
12
|
+
require_relative "param_filters"
|
13
|
+
require_relative "multi_error_reporter"
|
14
|
+
|
15
|
+
module LogStruct
|
16
|
+
class Formatter < ::Logger::Formatter
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
# Add current_tags method to support ActiveSupport::TaggedLogging
|
20
|
+
sig { returns(T::Array[String]) }
|
21
|
+
def current_tags
|
22
|
+
Thread.current[:activesupport_tagged_logging_tags] ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add tagged method to support ActiveSupport::TaggedLogging
|
26
|
+
sig { params(tags: T::Array[String], blk: T.proc.params(formatter: Formatter).void).returns(T.untyped) }
|
27
|
+
def tagged(*tags, &blk)
|
28
|
+
new_tags = tags.flatten
|
29
|
+
current_tags.concat(new_tags) if new_tags.any?
|
30
|
+
yield self
|
31
|
+
ensure
|
32
|
+
current_tags.pop(new_tags.size) if new_tags&.any?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Add clear_tags! method to support ActiveSupport::TaggedLogging
|
36
|
+
sig { void }
|
37
|
+
def clear_tags!
|
38
|
+
Thread.current[:activesupport_tagged_logging_tags] = []
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { params(tags: T::Array[String]).returns(T.untyped) }
|
42
|
+
def push_tags(*tags)
|
43
|
+
current_tags.concat(tags)
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(string: String).returns(String) }
|
47
|
+
def scrub_string(string)
|
48
|
+
# Use StringScrubber module to scrub sensitive information from strings
|
49
|
+
StringScrubber.scrub(string)
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(arg: T.untyped, recursion_depth: Integer).returns(T.untyped) }
|
53
|
+
def process_values(arg, recursion_depth: 0)
|
54
|
+
# Prevent infinite recursion in case any args have circular references
|
55
|
+
# or are too deeply nested. Just return args.
|
56
|
+
return arg if recursion_depth > 20
|
57
|
+
|
58
|
+
case arg
|
59
|
+
when Hash
|
60
|
+
result = {}
|
61
|
+
|
62
|
+
# Process each key-value pair
|
63
|
+
arg.each do |key, value|
|
64
|
+
# Check if this key should be filtered at any depth
|
65
|
+
result[key] = if ParamFilters.should_filter_key?(key)
|
66
|
+
# Filter the value
|
67
|
+
{_filtered: ParamFilters.summarize_json_attribute(key, value)}
|
68
|
+
else
|
69
|
+
# Process the value normally
|
70
|
+
process_values(value, recursion_depth: recursion_depth + 1)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
result
|
75
|
+
when Array
|
76
|
+
result = arg.map { |value| process_values(value, recursion_depth: recursion_depth + 1) }
|
77
|
+
|
78
|
+
# Filter large arrays, but don't truncate backtraces (arrays of strings that look like file:line)
|
79
|
+
if result.size > 10 && !looks_like_backtrace?(result)
|
80
|
+
result = result.take(10) + ["... and #{result.size - 10} more items"]
|
81
|
+
end
|
82
|
+
result
|
83
|
+
when GlobalID::Identification
|
84
|
+
begin
|
85
|
+
arg.to_global_id
|
86
|
+
rescue
|
87
|
+
begin
|
88
|
+
case arg
|
89
|
+
when ActiveRecord::Base
|
90
|
+
"#{arg.class}(##{arg.id})"
|
91
|
+
else
|
92
|
+
# For non-ActiveRecord objects that failed to_global_id, try to get a string representation
|
93
|
+
# If this also fails, we want to catch it and return the error placeholder
|
94
|
+
T.unsafe(arg).to_s
|
95
|
+
end
|
96
|
+
rescue => e
|
97
|
+
LogStruct.handle_exception(e, source: Source::LogStruct)
|
98
|
+
"[GLOBALID_ERROR]"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
when Source, Event
|
102
|
+
arg.serialize
|
103
|
+
when String
|
104
|
+
scrub_string(arg)
|
105
|
+
when Time
|
106
|
+
arg.iso8601(3)
|
107
|
+
else
|
108
|
+
# Any other type (e.g. Symbol, Integer, Float, Boolean etc.)
|
109
|
+
arg
|
110
|
+
end
|
111
|
+
rescue => e
|
112
|
+
# Report error through LogStruct's framework
|
113
|
+
context = {
|
114
|
+
processor_method: "process_values",
|
115
|
+
value_type: arg.class.name,
|
116
|
+
recursion_depth: recursion_depth
|
117
|
+
}
|
118
|
+
LogStruct.handle_exception(e, source: Source::LogStruct, context: context)
|
119
|
+
arg
|
120
|
+
end
|
121
|
+
|
122
|
+
sig { params(log_value: T.untyped, time: Time).returns(T::Hash[Symbol, T.untyped]) }
|
123
|
+
def log_value_to_hash(log_value, time:)
|
124
|
+
case log_value
|
125
|
+
when Log::Interfaces::CommonFields
|
126
|
+
# Our log classes all implement a custom #serialize method that use symbol keys
|
127
|
+
log_value.serialize
|
128
|
+
|
129
|
+
when T::Struct
|
130
|
+
# Default T::Struct.serialize methods returns a hash with string keys, so convert them to symbols
|
131
|
+
log_value.serialize.deep_symbolize_keys
|
132
|
+
|
133
|
+
when Hash
|
134
|
+
# Use hash as is and convert string keys to symbols
|
135
|
+
log_value.dup.deep_symbolize_keys
|
136
|
+
|
137
|
+
else
|
138
|
+
# Create a Plain log with the message as a string and serialize it with symbol keys
|
139
|
+
# log_value can be literally anything: Integer, Float, Boolean, NilClass, etc.
|
140
|
+
log_message = case log_value
|
141
|
+
# Handle all the basic types without any further processing
|
142
|
+
when String, Symbol, TrueClass, FalseClass, NilClass, Array, Hash, Time, Numeric
|
143
|
+
log_value
|
144
|
+
else
|
145
|
+
# Handle the serialization of complex objects in a useful way:
|
146
|
+
#
|
147
|
+
# 1. For ActiveRecord models: Use as_json which includes attributes
|
148
|
+
# 2. For objects with custom as_json implementations: Use their implementation
|
149
|
+
# 3. For basic objects that only have ActiveSupport's as_json: Use to_s
|
150
|
+
begin
|
151
|
+
method_owner = log_value.method(:as_json).owner
|
152
|
+
|
153
|
+
# If it's ActiveRecord, ActiveModel, or a custom implementation, use as_json
|
154
|
+
if method_owner.to_s.include?("ActiveRecord") ||
|
155
|
+
method_owner.to_s.include?("ActiveModel") ||
|
156
|
+
method_owner.to_s.exclude?("ActiveSupport::CoreExtensions") &&
|
157
|
+
method_owner.to_s.exclude?("Object")
|
158
|
+
log_value.as_json
|
159
|
+
else
|
160
|
+
# For plain objects with only the default ActiveSupport as_json
|
161
|
+
log_value.to_s
|
162
|
+
end
|
163
|
+
rescue => e
|
164
|
+
# Handle serialization errors
|
165
|
+
context = {
|
166
|
+
object_class: log_value.class.name,
|
167
|
+
object_inspect: log_value.inspect.truncate(100)
|
168
|
+
}
|
169
|
+
LogStruct.handle_exception(e, source: Source::LogStruct, context: context)
|
170
|
+
|
171
|
+
# Fall back to the string representation to ensure we continue processing
|
172
|
+
log_value.to_s
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
Log::Plain.new(
|
177
|
+
message: log_message,
|
178
|
+
timestamp: time
|
179
|
+
).serialize
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Serializes Log (or string) into JSON
|
184
|
+
sig { params(severity: T.any(String, Symbol, Integer), time: Time, progname: T.nilable(String), log_value: T.untyped).returns(String) }
|
185
|
+
def call(severity, time, progname, log_value)
|
186
|
+
level_enum = Level.from_severity(severity)
|
187
|
+
|
188
|
+
data = log_value_to_hash(log_value, time: time)
|
189
|
+
|
190
|
+
# Filter params, scrub sensitive values, format ActiveJob GlobalID arguments
|
191
|
+
data = process_values(data)
|
192
|
+
|
193
|
+
# Add standard fields if not already present
|
194
|
+
data[:src] ||= Source::App
|
195
|
+
data[:evt] ||= Event::Log
|
196
|
+
data[:ts] ||= time.iso8601(3)
|
197
|
+
data[:lvl] = level_enum # Set level from severity parameter
|
198
|
+
data[:prog] = progname if progname.present?
|
199
|
+
|
200
|
+
generate_json(data)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Output as JSON with a newline. We mock this method in tests so we can
|
204
|
+
# inspect the data right before it gets turned into a JSON string.
|
205
|
+
sig { params(data: T::Hash[T.untyped, T.untyped]).returns(String) }
|
206
|
+
def generate_json(data)
|
207
|
+
"#{data.to_json}\n"
|
208
|
+
end
|
209
|
+
|
210
|
+
# Check if an array looks like a backtrace (array of strings with file:line pattern)
|
211
|
+
sig { params(array: T::Array[T.untyped]).returns(T::Boolean) }
|
212
|
+
def looks_like_backtrace?(array)
|
213
|
+
return false if array.empty?
|
214
|
+
|
215
|
+
# Check if most elements look like backtrace lines (file.rb:123 or similar patterns)
|
216
|
+
backtrace_like_count = array.first(5).count do |element|
|
217
|
+
element.is_a?(String) && element.match?(/\A[^:\s]+:\d+/)
|
218
|
+
end
|
219
|
+
|
220
|
+
# If at least 3 out of the first 5 elements look like backtrace lines, treat as backtrace
|
221
|
+
backtrace_like_count >= 3
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
# Module for custom handlers used throughout the library
|
6
|
+
module Handlers
|
7
|
+
# Type for Lograge custom options
|
8
|
+
LogrageCustomOptions = T.type_alias {
|
9
|
+
T.proc.params(
|
10
|
+
event: ActiveSupport::Notifications::Event,
|
11
|
+
options: T::Hash[Symbol, T.untyped]
|
12
|
+
).returns(T.untyped)
|
13
|
+
}
|
14
|
+
|
15
|
+
# Type for error reporting handlers
|
16
|
+
ErrorReporter = T.type_alias {
|
17
|
+
T.proc.params(
|
18
|
+
error: StandardError,
|
19
|
+
context: T.nilable(T::Hash[Symbol, T.untyped]),
|
20
|
+
source: Source
|
21
|
+
).void
|
22
|
+
}
|
23
|
+
|
24
|
+
# Type for string scrubbing handlers
|
25
|
+
StringScrubber = T.type_alias { T.proc.params(string: String).returns(String) }
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
module LogStruct
|
7
|
+
# Utility module for hashing sensitive data
|
8
|
+
module HashUtils
|
9
|
+
class << self
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
# Create a hash of a string value for tracing while preserving privacy
|
13
|
+
sig { params(value: String).returns(String) }
|
14
|
+
def hash_value(value)
|
15
|
+
salt = LogStruct.config.filters.hash_salt
|
16
|
+
length = LogStruct.config.filters.hash_length
|
17
|
+
Digest::SHA256.hexdigest("#{salt}#{value}")[0...length] || "error"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
module Integrations
|
6
|
+
module ActionMailer
|
7
|
+
# Backport of the *_deliver callbacks from Rails 7.1
|
8
|
+
module Callbacks
|
9
|
+
extend T::Sig
|
10
|
+
extend ::ActiveSupport::Concern
|
11
|
+
|
12
|
+
# Track if we've already patched MessageDelivery
|
13
|
+
@patched_message_delivery = T.let(false, T::Boolean)
|
14
|
+
|
15
|
+
# We can't use included block with strict typing
|
16
|
+
# This will be handled by ActiveSupport::Concern at runtime
|
17
|
+
included do
|
18
|
+
include ::ActiveSupport::Callbacks
|
19
|
+
if defined?(::ActiveSupport) && ::ActiveSupport.gem_version >= Gem::Version.new("7.1.0")
|
20
|
+
define_callbacks :deliver, skip_after_callbacks_if_terminated: true
|
21
|
+
else
|
22
|
+
define_callbacks :deliver
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# When this module is prepended (our integration uses prepend), ensure callbacks are defined
|
27
|
+
if respond_to?(:prepended)
|
28
|
+
prepended do
|
29
|
+
include ::ActiveSupport::Callbacks
|
30
|
+
if defined?(::ActiveSupport) && ::ActiveSupport.gem_version >= Gem::Version.new("7.1.0")
|
31
|
+
define_callbacks :deliver, skip_after_callbacks_if_terminated: true
|
32
|
+
else
|
33
|
+
define_callbacks :deliver
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Define class methods in a separate module
|
39
|
+
module ClassMethods
|
40
|
+
extend T::Sig
|
41
|
+
|
42
|
+
# Defines a callback that will get called right before the
|
43
|
+
# message is sent to the delivery method.
|
44
|
+
sig { params(filters: T.untyped, blk: T.nilable(T.proc.bind(T.untyped).void)).void }
|
45
|
+
def before_deliver(*filters, &blk)
|
46
|
+
# Use T.unsafe for splat arguments due to Sorbet limitation
|
47
|
+
T.unsafe(self).set_callback(:deliver, :before, *filters, &blk)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines a callback that will get called right after the
|
51
|
+
# message's delivery method is finished.
|
52
|
+
sig { params(filters: T.untyped, blk: T.nilable(T.proc.bind(T.untyped).void)).void }
|
53
|
+
def after_deliver(*filters, &blk)
|
54
|
+
# Use T.unsafe for splat arguments due to Sorbet limitation
|
55
|
+
T.unsafe(self).set_callback(:deliver, :after, *filters, &blk)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Defines a callback that will get called around the message's deliver method.
|
59
|
+
sig { params(filters: T.untyped, blk: T.nilable(T.proc.bind(T.untyped).params(arg0: T.untyped).void)).void }
|
60
|
+
def around_deliver(*filters, &blk)
|
61
|
+
# Use T.unsafe for splat arguments due to Sorbet limitation
|
62
|
+
T.unsafe(self).set_callback(:deliver, :around, *filters, &blk)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Module to patch ActionMailer::MessageDelivery with callback support
|
67
|
+
module MessageDeliveryCallbacks
|
68
|
+
extend T::Sig
|
69
|
+
|
70
|
+
sig { returns(T.untyped) }
|
71
|
+
def deliver_now
|
72
|
+
processed_mailer.run_callbacks(:deliver) do
|
73
|
+
message.deliver
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { returns(T.untyped) }
|
78
|
+
def deliver_now!
|
79
|
+
processed_mailer.run_callbacks(:deliver) do
|
80
|
+
message.deliver!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
sig { returns(T::Boolean) }
|
86
|
+
def self.patch_message_delivery
|
87
|
+
# Return early if we've already patched
|
88
|
+
return true if @patched_message_delivery
|
89
|
+
|
90
|
+
# Prepend our module to add callback support to MessageDelivery
|
91
|
+
::ActionMailer::MessageDelivery.prepend(MessageDeliveryCallbacks)
|
92
|
+
|
93
|
+
# Mark as patched so we don't do it again
|
94
|
+
@patched_message_delivery = true
|
95
|
+
true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
module Integrations
|
6
|
+
module ActionMailer
|
7
|
+
# Handles error handling for ActionMailer
|
8
|
+
#
|
9
|
+
# IMPORTANT LIMITATIONS:
|
10
|
+
# 1. This module must be included BEFORE users define rescue_from handlers
|
11
|
+
# to ensure proper handler precedence (user handlers are checked first)
|
12
|
+
# 2. Rails rescue_from handlers don't bubble to parent class handlers after reraise
|
13
|
+
# 3. Handler order matters: Rails checks rescue_from handlers in reverse declaration order
|
14
|
+
module ErrorHandling
|
15
|
+
extend T::Sig
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
# NOTE: rescue_from handlers are checked in reverse order of declaration.
|
19
|
+
# We want LogStruct handlers to be checked AFTER user handlers (lower priority),
|
20
|
+
# so we need to add them BEFORE user handlers are declared.
|
21
|
+
|
22
|
+
# This will be called when the module is included/prepended
|
23
|
+
sig { params(base: T.untyped).void }
|
24
|
+
def self.install_handler(base)
|
25
|
+
# Only add the handler once per class
|
26
|
+
return if base.instance_variable_get(:@_logstruct_handler_installed)
|
27
|
+
|
28
|
+
# Add our handler FIRST so it has lower priority than user handlers
|
29
|
+
base.rescue_from StandardError, with: :log_and_reraise_error
|
30
|
+
|
31
|
+
# Mark as installed to prevent duplicates
|
32
|
+
base.instance_variable_set(:@_logstruct_handler_installed, true)
|
33
|
+
end
|
34
|
+
|
35
|
+
included do
|
36
|
+
LogStruct::Integrations::ActionMailer::ErrorHandling.install_handler(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Also support prepended (used by tests and manual setup)
|
40
|
+
sig { params(base: T.untyped).void }
|
41
|
+
def self.prepended(base)
|
42
|
+
install_handler(base)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Just log the error without reporting or retrying
|
48
|
+
sig { params(ex: StandardError).void }
|
49
|
+
def log_and_ignore_error(ex)
|
50
|
+
log_email_delivery_error(ex, notify: false, report: false, reraise: false)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Log and report to error service, but doesn't reraise.
|
54
|
+
sig { params(ex: StandardError).void }
|
55
|
+
def log_and_report_error(ex)
|
56
|
+
log_email_delivery_error(ex, notify: false, report: true, reraise: false)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Log, report to error service, and reraise for retry
|
60
|
+
sig { params(ex: StandardError).void }
|
61
|
+
def log_and_reraise_error(ex)
|
62
|
+
log_email_delivery_error(ex, notify: false, report: true, reraise: true)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Handle an error from a mailer
|
68
|
+
sig { params(mailer: T.untyped, error: StandardError, message: String).void }
|
69
|
+
def log_structured_error(mailer, error, message)
|
70
|
+
# Create a structured exception log with context
|
71
|
+
context = {
|
72
|
+
mailer_class: mailer.class.to_s,
|
73
|
+
mailer_action: mailer.respond_to?(:action_name) ? mailer.action_name : nil,
|
74
|
+
message: message
|
75
|
+
}
|
76
|
+
|
77
|
+
# Create the structured exception log
|
78
|
+
exception_data = Log::Error.from_exception(
|
79
|
+
Source::Mailer,
|
80
|
+
error,
|
81
|
+
context
|
82
|
+
)
|
83
|
+
|
84
|
+
# Log the structured error
|
85
|
+
LogStruct.error(exception_data)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Log when email delivery fails
|
89
|
+
sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
|
90
|
+
def log_email_delivery_error(error, notify: false, report: true, reraise: true)
|
91
|
+
# Generate appropriate error message
|
92
|
+
message = error_message_for(error, reraise)
|
93
|
+
|
94
|
+
# Use structured error logging
|
95
|
+
log_structured_error(self, error, message)
|
96
|
+
|
97
|
+
# Handle notifications and reporting
|
98
|
+
handle_error_notifications(error, notify, report, reraise)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Generate appropriate error message based on error handling strategy
|
102
|
+
sig { params(error: StandardError, reraise: T::Boolean).returns(String) }
|
103
|
+
def error_message_for(error, reraise)
|
104
|
+
if reraise
|
105
|
+
"#{error.class}: Email delivery error, will retry. Recipients: #{recipients(error)}"
|
106
|
+
else
|
107
|
+
"#{error.class}: Cannot send email to #{recipients(error)}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Handle error notifications, reporting, and reraising
|
112
|
+
sig { params(error: StandardError, notify: T::Boolean, report: T::Boolean, reraise: T::Boolean).void }
|
113
|
+
def handle_error_notifications(error, notify, report, reraise)
|
114
|
+
# Log a notification event if requested
|
115
|
+
log_notification_event(error) if notify
|
116
|
+
|
117
|
+
# Report to error reporting service if requested
|
118
|
+
if report
|
119
|
+
context = {
|
120
|
+
mailer_class: self.class.to_s,
|
121
|
+
mailer_action: respond_to?(:action_name) ? action_name : nil,
|
122
|
+
recipients: recipients(error)
|
123
|
+
}
|
124
|
+
|
125
|
+
# Create an exception log for structured logging
|
126
|
+
exception_data = Log::Error.from_exception(
|
127
|
+
Source::Mailer,
|
128
|
+
error,
|
129
|
+
context
|
130
|
+
)
|
131
|
+
|
132
|
+
# Log the exception with structured data
|
133
|
+
LogStruct.error(exception_data)
|
134
|
+
|
135
|
+
# Call the error handler
|
136
|
+
LogStruct.handle_exception(error, source: Source::Mailer, context: context)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Re-raise the error if requested
|
140
|
+
Kernel.raise error if reraise
|
141
|
+
end
|
142
|
+
|
143
|
+
# Log a notification event that can be picked up by external systems
|
144
|
+
sig { params(error: StandardError).void }
|
145
|
+
def log_notification_event(error)
|
146
|
+
# Create an error log data object
|
147
|
+
exception_data = Log::Error.from_exception(
|
148
|
+
Source::Mailer,
|
149
|
+
error,
|
150
|
+
{
|
151
|
+
mailer: self.class,
|
152
|
+
action: action_name,
|
153
|
+
recipients: recipients(error)
|
154
|
+
}
|
155
|
+
)
|
156
|
+
|
157
|
+
# Log the error at info level since it's not a critical error
|
158
|
+
LogStruct.info(exception_data)
|
159
|
+
end
|
160
|
+
|
161
|
+
sig { params(error: StandardError).returns(String) }
|
162
|
+
def recipients(error)
|
163
|
+
# Extract recipient info if available
|
164
|
+
if error.respond_to?(:recipients) && T.unsafe(error).recipients.present?
|
165
|
+
T.unsafe(error).recipients.join(", ")
|
166
|
+
else
|
167
|
+
"unknown"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|