logstruct 0.0.1 → 0.0.2.pre.rc1
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,21 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
module Integrations
|
6
|
+
# Interface that all integrations must implement
|
7
|
+
# This ensures consistent behavior across all integration modules
|
8
|
+
module IntegrationInterface
|
9
|
+
extend T::Sig
|
10
|
+
extend T::Helpers
|
11
|
+
|
12
|
+
# This is an interface that should be implemented by all integration modules
|
13
|
+
interface!
|
14
|
+
|
15
|
+
# All integrations must implement this method to set up their functionality
|
16
|
+
# @return [Boolean, nil] Returns true if setup was successful, nil if skipped
|
17
|
+
sig { abstract.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
18
|
+
def setup(config); end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "lograge"
|
6
|
+
rescue LoadError
|
7
|
+
# Lograge gem is not available, integration will be skipped
|
8
|
+
end
|
9
|
+
|
10
|
+
module LogStruct
|
11
|
+
module Integrations
|
12
|
+
# Lograge integration for structured request logging
|
13
|
+
module Lograge
|
14
|
+
extend IntegrationInterface
|
15
|
+
|
16
|
+
class << self
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
# Set up lograge for structured request logging
|
20
|
+
sig { override.params(logstruct_config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
21
|
+
def setup(logstruct_config)
|
22
|
+
return nil unless defined?(::Lograge)
|
23
|
+
return nil unless logstruct_config.enabled
|
24
|
+
return nil unless logstruct_config.integrations.enable_lograge
|
25
|
+
|
26
|
+
configure_lograge(logstruct_config)
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
private_class_method
|
32
|
+
|
33
|
+
sig { params(logstruct_config: LogStruct::Configuration).void }
|
34
|
+
def configure_lograge(logstruct_config)
|
35
|
+
::Rails.application.configure do
|
36
|
+
config.lograge.enabled = true
|
37
|
+
# Use a raw formatter that just returns the log struct.
|
38
|
+
# The struct is converted to JSON by our Formatter (after filtering, etc.)
|
39
|
+
config.lograge.formatter = T.let(
|
40
|
+
lambda do |data|
|
41
|
+
# Convert the data hash to a Log::Request struct
|
42
|
+
Log::Request.new(
|
43
|
+
source: Source::Rails,
|
44
|
+
event: Event::Request,
|
45
|
+
timestamp: Time.now,
|
46
|
+
http_method: data[:method],
|
47
|
+
path: data[:path],
|
48
|
+
format: data[:format],
|
49
|
+
controller: data[:controller],
|
50
|
+
action: data[:action],
|
51
|
+
status: data[:status],
|
52
|
+
duration: data[:duration],
|
53
|
+
view: data[:view],
|
54
|
+
db: data[:db],
|
55
|
+
params: data[:params]
|
56
|
+
)
|
57
|
+
end,
|
58
|
+
T.proc.params(hash: T::Hash[Symbol, T.untyped]).returns(Log::Request)
|
59
|
+
)
|
60
|
+
|
61
|
+
# Add custom options to lograge
|
62
|
+
config.lograge.custom_options = lambda do |event|
|
63
|
+
Integrations::Lograge.lograge_default_options(event)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(event: ActiveSupport::Notifications::Event).returns(T::Hash[Symbol, T.untyped]) }
|
69
|
+
def lograge_default_options(event)
|
70
|
+
# Extract essential fields from the payload
|
71
|
+
options = event.payload.slice(
|
72
|
+
:request_id,
|
73
|
+
:host,
|
74
|
+
:source_ip
|
75
|
+
).compact
|
76
|
+
|
77
|
+
if event.payload[:params].present?
|
78
|
+
options[:params] = event.payload[:params].except("controller", "action")
|
79
|
+
end
|
80
|
+
|
81
|
+
# Process headers if available
|
82
|
+
process_headers(event, options)
|
83
|
+
|
84
|
+
# Apply custom options from application if provided
|
85
|
+
apply_custom_options(event, options)
|
86
|
+
|
87
|
+
options
|
88
|
+
end
|
89
|
+
|
90
|
+
# Process headers from the event payload
|
91
|
+
sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
|
92
|
+
def process_headers(event, options)
|
93
|
+
headers = event.payload[:headers]
|
94
|
+
return if headers.blank?
|
95
|
+
|
96
|
+
options[:user_agent] = headers["HTTP_USER_AGENT"]
|
97
|
+
options[:content_type] = headers["CONTENT_TYPE"]
|
98
|
+
options[:accept] = headers["HTTP_ACCEPT"]
|
99
|
+
end
|
100
|
+
|
101
|
+
# Apply custom options from the application's configuration
|
102
|
+
sig { params(event: ActiveSupport::Notifications::Event, options: T::Hash[Symbol, T.untyped]).void }
|
103
|
+
def apply_custom_options(event, options)
|
104
|
+
custom_options_proc = LogStruct.config.integrations.lograge_custom_options
|
105
|
+
return unless custom_options_proc&.respond_to?(:call)
|
106
|
+
|
107
|
+
# Call the proc with the event and options
|
108
|
+
# The proc can modify the options hash directly
|
109
|
+
custom_options_proc.call(event, options)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "rack"
|
5
|
+
require "action_dispatch/middleware/show_exceptions"
|
6
|
+
require_relative "rack/error_handling_middleware"
|
7
|
+
|
8
|
+
module LogStruct
|
9
|
+
module Integrations
|
10
|
+
# Rack middleware integration for structured logging
|
11
|
+
module Rack
|
12
|
+
extend T::Sig
|
13
|
+
extend IntegrationInterface
|
14
|
+
|
15
|
+
# Set up Rack middleware for structured error logging
|
16
|
+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
17
|
+
def self.setup(config)
|
18
|
+
return nil unless config.enabled
|
19
|
+
return nil unless config.integrations.enable_rack_error_handler
|
20
|
+
|
21
|
+
# Add structured logging middleware for security violations and errors
|
22
|
+
# Need to insert after ShowExceptions to catch IP spoofing errors
|
23
|
+
::Rails.application.middleware.insert_after(
|
24
|
+
::ActionDispatch::ShowExceptions,
|
25
|
+
Integrations::RackErrorHandler::Middleware
|
26
|
+
)
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
module Integrations
|
6
|
+
module RackErrorHandler
|
7
|
+
# Custom middleware to enhance Rails error logging with JSON format and request details
|
8
|
+
class Middleware
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
# IP Spoofing error response
|
12
|
+
IP_SPOOF_HTML = T.let(
|
13
|
+
"<html><head><title>IP Spoofing Detected</title></head><body>" \
|
14
|
+
"<h1>Forbidden</h1>" \
|
15
|
+
"<p>IP spoofing detected. This request has been blocked for security reasons.</p>" \
|
16
|
+
"</body></html>",
|
17
|
+
String
|
18
|
+
)
|
19
|
+
|
20
|
+
# CSRF error response
|
21
|
+
CSRF_HTML = T.let(
|
22
|
+
"<html><head><title>CSRF Error</title></head><body>" \
|
23
|
+
"<h1>Forbidden</h1>" \
|
24
|
+
"<p>Invalid authenticity token. This request has been blocked to prevent cross-site request forgery.</p>" \
|
25
|
+
"</body></html>",
|
26
|
+
String
|
27
|
+
)
|
28
|
+
|
29
|
+
# Response headers calculated at load time
|
30
|
+
IP_SPOOF_HEADERS = T.let(
|
31
|
+
{
|
32
|
+
"Content-Type" => "text/html",
|
33
|
+
"Content-Length" => IP_SPOOF_HTML.bytesize.to_s
|
34
|
+
}.freeze,
|
35
|
+
T::Hash[String, String]
|
36
|
+
)
|
37
|
+
|
38
|
+
CSRF_HEADERS = T.let(
|
39
|
+
{
|
40
|
+
"Content-Type" => "text/html",
|
41
|
+
"Content-Length" => CSRF_HTML.bytesize.to_s
|
42
|
+
}.freeze,
|
43
|
+
T::Hash[String, String]
|
44
|
+
)
|
45
|
+
|
46
|
+
# HTTP status code for forbidden responses
|
47
|
+
FORBIDDEN_STATUS = T.let(403, Integer)
|
48
|
+
|
49
|
+
sig { params(app: T.untyped).void }
|
50
|
+
def initialize(app)
|
51
|
+
@app = app
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(env: T.untyped).returns(T.untyped) }
|
55
|
+
def call(env)
|
56
|
+
return @app.call(env) unless LogStruct.enabled?
|
57
|
+
|
58
|
+
# Try to process the request
|
59
|
+
begin
|
60
|
+
@app.call(env)
|
61
|
+
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
|
62
|
+
# Create a security log for IP spoofing
|
63
|
+
security_log = Log::Security.new(
|
64
|
+
event: Event::IPSpoof,
|
65
|
+
message: ip_spoof_error.message,
|
66
|
+
# Can't call .remote_ip on the request because that's what raises the error.
|
67
|
+
# Have to pass the client_ip and x_forwarded_for headers.
|
68
|
+
client_ip: env["HTTP_CLIENT_IP"],
|
69
|
+
x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
|
70
|
+
path: env["PATH_INFO"],
|
71
|
+
http_method: env["REQUEST_METHOD"],
|
72
|
+
user_agent: env["HTTP_USER_AGENT"],
|
73
|
+
referer: env["HTTP_REFERER"],
|
74
|
+
request_id: env["action_dispatch.request_id"]
|
75
|
+
)
|
76
|
+
|
77
|
+
# Log the structured data
|
78
|
+
::Rails.logger.warn(security_log)
|
79
|
+
|
80
|
+
# Report the error
|
81
|
+
context = extract_request_context(env)
|
82
|
+
LogStruct.handle_exception(ip_spoof_error, source: Source::Security, context: context)
|
83
|
+
|
84
|
+
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
|
85
|
+
# If we are only logging or reporting these security errors, then return a default response
|
86
|
+
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS, [IP_SPOOF_HTML]]
|
87
|
+
rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
|
88
|
+
# Create a security log for CSRF error
|
89
|
+
request = ::ActionDispatch::Request.new(env)
|
90
|
+
security_log = Log::Security.new(
|
91
|
+
event: Event::CSRFViolation,
|
92
|
+
message: invalid_auth_token_error.message,
|
93
|
+
path: request.path,
|
94
|
+
http_method: request.method,
|
95
|
+
source_ip: request.remote_ip,
|
96
|
+
user_agent: request.user_agent,
|
97
|
+
referer: request.referer,
|
98
|
+
request_id: request.request_id
|
99
|
+
)
|
100
|
+
LogStruct.error(security_log)
|
101
|
+
|
102
|
+
# Report to error reporting service and/or re-raise
|
103
|
+
context = extract_request_context(env)
|
104
|
+
LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
|
105
|
+
|
106
|
+
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
|
107
|
+
# If we are only logging or reporting these security errors, then return a default response
|
108
|
+
[FORBIDDEN_STATUS, CSRF_HEADERS, [CSRF_HTML]]
|
109
|
+
rescue => error
|
110
|
+
# Extract request context for error reporting
|
111
|
+
context = extract_request_context(env)
|
112
|
+
|
113
|
+
# Create and log a structured exception with request context
|
114
|
+
exception_log = Log::Error.from_exception(
|
115
|
+
Source::Rails,
|
116
|
+
error,
|
117
|
+
context
|
118
|
+
)
|
119
|
+
LogStruct.error(exception_log)
|
120
|
+
|
121
|
+
# Re-raise any standard errors to let Rails or error reporter handle it.
|
122
|
+
# Rails will also log the request details separately
|
123
|
+
raise error
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
sig { params(env: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
130
|
+
def extract_request_context(env)
|
131
|
+
request = ::ActionDispatch::Request.new(env)
|
132
|
+
{
|
133
|
+
request_id: request.request_id,
|
134
|
+
path: request.path,
|
135
|
+
method: request.method,
|
136
|
+
user_agent: request.user_agent,
|
137
|
+
referer: request.referer
|
138
|
+
}
|
139
|
+
rescue => error
|
140
|
+
# If we can't extract request context, return minimal info
|
141
|
+
{error_extracting_context: error.message}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "rack"
|
5
|
+
require "action_dispatch/middleware/show_exceptions"
|
6
|
+
require_relative "rack_error_handler/middleware"
|
7
|
+
|
8
|
+
module LogStruct
|
9
|
+
module Integrations
|
10
|
+
# Rack middleware integration for structured logging
|
11
|
+
module RackErrorHandler
|
12
|
+
extend T::Sig
|
13
|
+
extend IntegrationInterface
|
14
|
+
|
15
|
+
# Set up Rack middleware for structured error logging
|
16
|
+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
17
|
+
def self.setup(config)
|
18
|
+
return nil unless config.enabled
|
19
|
+
return nil unless config.integrations.enable_rack_error_handler
|
20
|
+
|
21
|
+
# Add structured logging middleware for security violations and errors
|
22
|
+
# Need to insert after ShowExceptions to catch IP spoofing errors
|
23
|
+
::Rails.application.middleware.insert_after(
|
24
|
+
::ActionDispatch::ShowExceptions,
|
25
|
+
Integrations::RackErrorHandler::Middleware
|
26
|
+
)
|
27
|
+
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "shrine"
|
6
|
+
rescue LoadError
|
7
|
+
# Shrine gem is not available, integration will be skipped
|
8
|
+
end
|
9
|
+
|
10
|
+
module LogStruct
|
11
|
+
module Integrations
|
12
|
+
# Shrine integration for structured logging
|
13
|
+
module Shrine
|
14
|
+
extend T::Sig
|
15
|
+
extend IntegrationInterface
|
16
|
+
|
17
|
+
# Set up Shrine structured logging
|
18
|
+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
19
|
+
def self.setup(config)
|
20
|
+
return nil unless defined?(::Shrine)
|
21
|
+
return nil unless config.enabled
|
22
|
+
return nil unless config.integrations.enable_shrine
|
23
|
+
|
24
|
+
# Create a structured log subscriber for Shrine
|
25
|
+
# ActiveSupport::Notifications::Event has name, time, end, transaction_id, payload, and duration
|
26
|
+
shrine_log_subscriber = T.unsafe(lambda do |event|
|
27
|
+
payload = event.payload.except(:io, :metadata, :name).dup
|
28
|
+
|
29
|
+
# Map event name to Event type
|
30
|
+
event_type = case event.name
|
31
|
+
when :upload then Event::Upload
|
32
|
+
when :download then Event::Download
|
33
|
+
when :delete then Event::Delete
|
34
|
+
when :metadata then Event::Metadata
|
35
|
+
when :exists then Event::Exist # ActiveStorage uses 'exist', may as well use that
|
36
|
+
else Event::Unknown
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create structured log data
|
40
|
+
log_data = Log::Shrine.new(
|
41
|
+
source: Source::Shrine,
|
42
|
+
event: event_type,
|
43
|
+
duration: event.duration,
|
44
|
+
storage: payload[:storage],
|
45
|
+
location: payload[:location],
|
46
|
+
uploader: payload[:uploader],
|
47
|
+
upload_options: payload[:upload_options],
|
48
|
+
download_options: payload[:download_options],
|
49
|
+
options: payload[:options],
|
50
|
+
# Data is flattened by the JSON formatter
|
51
|
+
additional_data: payload.except(
|
52
|
+
:storage,
|
53
|
+
:location,
|
54
|
+
:uploader,
|
55
|
+
:upload_options,
|
56
|
+
:download_options,
|
57
|
+
:options
|
58
|
+
)
|
59
|
+
)
|
60
|
+
|
61
|
+
# Pass the structured hash to the logger
|
62
|
+
# If Rails.logger has our Formatter, it will handle JSON conversion
|
63
|
+
::Shrine.logger.info log_data
|
64
|
+
end)
|
65
|
+
|
66
|
+
# Configure Shrine to use our structured log subscriber
|
67
|
+
::Shrine.plugin :instrumentation,
|
68
|
+
events: %i[upload exists download delete],
|
69
|
+
log_subscriber: shrine_log_subscriber
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../../semantic_logger/logger"
|
5
|
+
require_relative "../../log/sidekiq"
|
6
|
+
require_relative "../../enums/source"
|
7
|
+
|
8
|
+
module LogStruct
|
9
|
+
module Integrations
|
10
|
+
module Sidekiq
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
# Get thread ID for Sidekiq logging
|
14
|
+
sig { returns(String) }
|
15
|
+
def self.tid
|
16
|
+
Thread.current["sidekiq_tid"] ||= (Thread.current.object_id ^ ::Process.pid).to_s(36)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Custom Logger for Sidekiq that creates LogStruct::Log::Sidekiq entries
|
20
|
+
class Logger < LogStruct::SemanticLogger::Logger
|
21
|
+
extend T::Sig
|
22
|
+
|
23
|
+
# Override log methods to create Sidekiq log structs
|
24
|
+
%i[debug info warn error fatal].each do |level|
|
25
|
+
define_method(level) do |message = nil, payload = nil, &block|
|
26
|
+
# Create a Sidekiq log struct with the message
|
27
|
+
log_struct = Log::Sidekiq.new(
|
28
|
+
level: LogStruct::Level.from_severity(level.to_s.upcase),
|
29
|
+
event: Event::Log,
|
30
|
+
message: message || (block ? block.call : ""),
|
31
|
+
process_id: ::Process.pid,
|
32
|
+
thread_id: Sidekiq.tid,
|
33
|
+
context: ::Sidekiq::Context.current || {}
|
34
|
+
)
|
35
|
+
|
36
|
+
# Pass the struct to SemanticLogger
|
37
|
+
super(log_struct, payload, &nil)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "sidekiq"
|
6
|
+
rescue LoadError
|
7
|
+
# Sidekiq gem is not available, integration will be skipped
|
8
|
+
end
|
9
|
+
require_relative "sidekiq/logger" if defined?(::Sidekiq)
|
10
|
+
|
11
|
+
module LogStruct
|
12
|
+
module Integrations
|
13
|
+
# Sidekiq integration for structured logging
|
14
|
+
module Sidekiq
|
15
|
+
extend T::Sig
|
16
|
+
extend IntegrationInterface
|
17
|
+
|
18
|
+
# Set up Sidekiq structured logging
|
19
|
+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
20
|
+
def self.setup(config)
|
21
|
+
return nil unless defined?(::Sidekiq)
|
22
|
+
return nil unless config.enabled
|
23
|
+
return nil unless config.integrations.enable_sidekiq
|
24
|
+
|
25
|
+
# Configure Sidekiq server (worker) to use our logger
|
26
|
+
::Sidekiq.configure_server do |sidekiq_config|
|
27
|
+
sidekiq_config.logger = LogStruct::Integrations::Sidekiq::Logger.new("Sidekiq-Server")
|
28
|
+
end
|
29
|
+
|
30
|
+
# Configure Sidekiq client (Rails app) to use our logger
|
31
|
+
::Sidekiq.configure_client do |sidekiq_config|
|
32
|
+
sidekiq_config.logger = LogStruct::Integrations::Sidekiq::Logger.new("Sidekiq-Client")
|
33
|
+
end
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module LogStruct
|
7
|
+
module Integrations
|
8
|
+
# Integration for Sorbet runtime type checking error handlers
|
9
|
+
# This module installs error handlers that report type errors through LogStruct
|
10
|
+
# These handlers can be enabled/disabled using configuration
|
11
|
+
module Sorbet
|
12
|
+
extend T::Sig
|
13
|
+
extend IntegrationInterface
|
14
|
+
|
15
|
+
# Set up Sorbet error handlers to report errors through LogStruct
|
16
|
+
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
17
|
+
def self.setup(config)
|
18
|
+
return nil unless config.integrations.enable_sorbet_error_handlers
|
19
|
+
|
20
|
+
# Install inline type error handler
|
21
|
+
# Called when T.let, T.cast, T.must, etc. fail
|
22
|
+
T::Configuration.inline_type_error_handler = lambda do |error, _opts|
|
23
|
+
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Install call validation error handler
|
27
|
+
# Called when method signature validation fails
|
28
|
+
T::Configuration.call_validation_error_handler = lambda do |_signature, opts|
|
29
|
+
error = TypeError.new(opts[:pretty_message])
|
30
|
+
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Install sig builder error handler
|
34
|
+
# Called when there's a problem with a signature definition
|
35
|
+
T::Configuration.sig_builder_error_handler = lambda do |error, _location|
|
36
|
+
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Install sig validation error handler
|
40
|
+
# Called when there's a problem with a signature validation
|
41
|
+
T::Configuration.sig_validation_error_handler = lambda do |error, _opts|
|
42
|
+
LogStruct.handle_exception(error, source: LogStruct::Source::TypeChecking)
|
43
|
+
end
|
44
|
+
|
45
|
+
true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "integrations/integration_interface"
|
5
|
+
require_relative "integrations/active_job"
|
6
|
+
require_relative "integrations/active_record"
|
7
|
+
require_relative "integrations/rack_error_handler"
|
8
|
+
require_relative "integrations/host_authorization"
|
9
|
+
require_relative "integrations/action_mailer"
|
10
|
+
require_relative "integrations/lograge"
|
11
|
+
require_relative "integrations/shrine"
|
12
|
+
require_relative "integrations/sidekiq"
|
13
|
+
require_relative "integrations/good_job"
|
14
|
+
require_relative "integrations/active_storage"
|
15
|
+
require_relative "integrations/carrierwave"
|
16
|
+
require_relative "integrations/sorbet"
|
17
|
+
|
18
|
+
module LogStruct
|
19
|
+
module Integrations
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
sig { void }
|
23
|
+
def self.setup_integrations
|
24
|
+
config = LogStruct.config
|
25
|
+
|
26
|
+
# Set up each integration with consistent configuration pattern
|
27
|
+
Integrations::Lograge.setup(config) if config.integrations.enable_lograge
|
28
|
+
Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
|
29
|
+
Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
|
30
|
+
Integrations::ActiveRecord.setup(config) if config.integrations.enable_sql_logging
|
31
|
+
Integrations::Sidekiq.setup(config) if config.integrations.enable_sidekiq
|
32
|
+
Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
|
33
|
+
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
|
34
|
+
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
|
35
|
+
Integrations::Shrine.setup(config) if config.integrations.enable_shrine
|
36
|
+
Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
|
37
|
+
Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
|
38
|
+
Integrations::Sorbet.setup(config) if config.integrations.enable_sorbet_error_handlers
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "interfaces/common_fields"
|
5
|
+
require_relative "interfaces/additional_data_field"
|
6
|
+
require_relative "shared/serialize_common"
|
7
|
+
require_relative "shared/merge_additional_data_fields"
|
8
|
+
require_relative "../enums/source"
|
9
|
+
require_relative "../enums/event"
|
10
|
+
require_relative "../enums/level"
|
11
|
+
require_relative "../log_keys"
|
12
|
+
|
13
|
+
module LogStruct
|
14
|
+
module Log
|
15
|
+
# Email log entry for structured logging
|
16
|
+
class ActionMailer < T::Struct
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
include Interfaces::CommonFields
|
20
|
+
include Interfaces::AdditionalDataField
|
21
|
+
include SerializeCommon
|
22
|
+
include MergeAdditionalDataFields
|
23
|
+
|
24
|
+
ActionMailerEvent = T.type_alias {
|
25
|
+
T.any(Event::Delivery, Event::Delivered)
|
26
|
+
}
|
27
|
+
|
28
|
+
# Common fields
|
29
|
+
const :source, Source::Mailer, default: T.let(Source::Mailer, Source::Mailer)
|
30
|
+
const :event, ActionMailerEvent
|
31
|
+
const :timestamp, Time, factory: -> { Time.now }
|
32
|
+
const :level, Level, default: T.let(Level::Info, Level)
|
33
|
+
|
34
|
+
# Email-specific fields
|
35
|
+
const :to, T.nilable(T.any(String, T::Array[String])), default: nil
|
36
|
+
const :from, T.nilable(String), default: nil
|
37
|
+
const :subject, T.nilable(String), default: nil
|
38
|
+
const :additional_data, T::Hash[Symbol, T.untyped], default: {}
|
39
|
+
|
40
|
+
# Convert the log entry to a hash for serialization
|
41
|
+
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
42
|
+
def serialize(strict = true)
|
43
|
+
hash = serialize_common(strict)
|
44
|
+
merge_additional_data_fields(hash)
|
45
|
+
|
46
|
+
# Add email-specific fields if they're present
|
47
|
+
hash[LOG_KEYS.fetch(:to)] = to if to
|
48
|
+
hash[LOG_KEYS.fetch(:from)] = from if from
|
49
|
+
hash[LOG_KEYS.fetch(:subject)] = subject if subject
|
50
|
+
|
51
|
+
hash
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|