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,76 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "interfaces/common_fields"
|
5
|
+
require_relative "interfaces/request_fields"
|
6
|
+
require_relative "shared/serialize_common"
|
7
|
+
require_relative "shared/add_request_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
|
+
# Request log entry for structured logging
|
16
|
+
class Request < T::Struct
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
include Interfaces::CommonFields
|
20
|
+
include Interfaces::RequestFields
|
21
|
+
include SerializeCommon
|
22
|
+
include AddRequestFields
|
23
|
+
|
24
|
+
RequestEvent = T.type_alias {
|
25
|
+
Event::Request
|
26
|
+
}
|
27
|
+
|
28
|
+
# Common fields
|
29
|
+
const :source, Source::Rails, default: T.let(Source::Rails, Source::Rails)
|
30
|
+
const :event, RequestEvent, default: T.let(Event::Request, RequestEvent)
|
31
|
+
const :timestamp, Time, factory: -> { Time.now }
|
32
|
+
const :level, Level, default: T.let(Level::Info, Level)
|
33
|
+
|
34
|
+
# Request-specific fields
|
35
|
+
# NOTE: `method` is a reserved word, so we use `http_method`
|
36
|
+
# prop while setting `method` in the serialized output
|
37
|
+
const :http_method, T.nilable(String), default: nil
|
38
|
+
const :path, T.nilable(String), default: nil
|
39
|
+
const :format, T.nilable(String), default: nil
|
40
|
+
const :controller, T.nilable(String), default: nil
|
41
|
+
const :action, T.nilable(String), default: nil
|
42
|
+
const :status, T.nilable(Integer), default: nil
|
43
|
+
const :duration, T.nilable(Float), default: nil
|
44
|
+
const :view, T.nilable(Float), default: nil
|
45
|
+
const :db, T.nilable(Float), default: nil
|
46
|
+
const :params, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
|
47
|
+
const :source_ip, T.nilable(String), default: nil
|
48
|
+
const :user_agent, T.nilable(String), default: nil
|
49
|
+
const :referer, T.nilable(String), default: nil
|
50
|
+
const :request_id, T.nilable(String), default: nil
|
51
|
+
|
52
|
+
# Convert the log entry to a hash for serialization
|
53
|
+
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
54
|
+
def serialize(strict = true)
|
55
|
+
hash = serialize_common(strict)
|
56
|
+
add_request_fields(hash)
|
57
|
+
hash[LOG_KEYS.fetch(:http_method)] = http_method if http_method
|
58
|
+
hash[LOG_KEYS.fetch(:path)] = path if path
|
59
|
+
hash[LOG_KEYS.fetch(:format)] = format if format
|
60
|
+
hash[LOG_KEYS.fetch(:controller)] = controller if controller
|
61
|
+
hash[LOG_KEYS.fetch(:action)] = action if action
|
62
|
+
hash[LOG_KEYS.fetch(:status)] = status if status
|
63
|
+
hash[LOG_KEYS.fetch(:duration)] = duration if duration
|
64
|
+
hash[LOG_KEYS.fetch(:view)] = view if view
|
65
|
+
hash[LOG_KEYS.fetch(:db)] = db if db
|
66
|
+
hash[LOG_KEYS.fetch(:params)] = params if params
|
67
|
+
hash[LOG_KEYS.fetch(:source_ip)] = source_ip if source_ip
|
68
|
+
hash[LOG_KEYS.fetch(:user_agent)] = user_agent if user_agent
|
69
|
+
hash[LOG_KEYS.fetch(:referer)] = referer if referer
|
70
|
+
hash[LOG_KEYS.fetch(:request_id)] = request_id if request_id
|
71
|
+
|
72
|
+
hash
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,80 @@
|
|
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 "interfaces/message_field"
|
7
|
+
require_relative "interfaces/request_fields"
|
8
|
+
require_relative "shared/add_request_fields"
|
9
|
+
require_relative "shared/merge_additional_data_fields"
|
10
|
+
require_relative "shared/serialize_common"
|
11
|
+
require_relative "../enums/event"
|
12
|
+
require_relative "../enums/level"
|
13
|
+
require_relative "../enums/source"
|
14
|
+
require_relative "../log_keys"
|
15
|
+
|
16
|
+
module LogStruct
|
17
|
+
module Log
|
18
|
+
# Security log entry for structured logging of security-related events
|
19
|
+
class Security < T::Struct
|
20
|
+
extend T::Sig
|
21
|
+
|
22
|
+
include Interfaces::CommonFields
|
23
|
+
include Interfaces::AdditionalDataField
|
24
|
+
include Interfaces::MessageField
|
25
|
+
include Interfaces::RequestFields
|
26
|
+
include SerializeCommon
|
27
|
+
include AddRequestFields
|
28
|
+
include MergeAdditionalDataFields
|
29
|
+
|
30
|
+
SecurityEvent = T.type_alias {
|
31
|
+
T.any(
|
32
|
+
Event::IPSpoof,
|
33
|
+
Event::CSRFViolation,
|
34
|
+
Event::BlockedHost
|
35
|
+
)
|
36
|
+
}
|
37
|
+
|
38
|
+
# Common fields
|
39
|
+
const :source, Source::Security, default: T.let(Source::Security, Source::Security)
|
40
|
+
const :event, SecurityEvent
|
41
|
+
const :timestamp, Time, factory: -> { Time.now }
|
42
|
+
const :level, Level, default: T.let(Level::Error, Level)
|
43
|
+
|
44
|
+
# Security-specific fields
|
45
|
+
const :message, T.nilable(String), default: nil
|
46
|
+
const :blocked_host, T.nilable(String), default: nil
|
47
|
+
const :blocked_hosts, T.nilable(T::Array[String]), default: nil
|
48
|
+
const :client_ip, T.nilable(String), default: nil
|
49
|
+
const :x_forwarded_for, T.nilable(String), default: nil
|
50
|
+
|
51
|
+
# Additional data (merged into hash)
|
52
|
+
const :additional_data, T::Hash[Symbol, T.untyped], default: {}
|
53
|
+
|
54
|
+
# Common request fields
|
55
|
+
const :path, T.nilable(String), default: nil
|
56
|
+
const :http_method, T.nilable(String), default: nil, name: "method"
|
57
|
+
const :source_ip, T.nilable(String), default: nil
|
58
|
+
const :user_agent, T.nilable(String), default: nil
|
59
|
+
const :referer, T.nilable(String), default: nil
|
60
|
+
const :request_id, T.nilable(String), default: nil
|
61
|
+
|
62
|
+
# Convert the log entry to a hash for serialization
|
63
|
+
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
64
|
+
def serialize(strict = true)
|
65
|
+
hash = serialize_common(strict)
|
66
|
+
add_request_fields(hash)
|
67
|
+
merge_additional_data_fields(hash)
|
68
|
+
|
69
|
+
# Add security-specific fields
|
70
|
+
hash[LOG_KEYS.fetch(:message)] = message if message
|
71
|
+
hash[LOG_KEYS.fetch(:blocked_host)] = blocked_host if blocked_host
|
72
|
+
hash[LOG_KEYS.fetch(:blocked_hosts)] = blocked_hosts if blocked_hosts
|
73
|
+
hash[LOG_KEYS.fetch(:client_ip)] = client_ip if client_ip
|
74
|
+
hash[LOG_KEYS.fetch(:x_forwarded_for)] = x_forwarded_for if x_forwarded_for
|
75
|
+
|
76
|
+
hash
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../../log_keys"
|
5
|
+
require_relative "../interfaces/request_fields"
|
6
|
+
|
7
|
+
module LogStruct
|
8
|
+
module Log
|
9
|
+
# Common log serialization method
|
10
|
+
module AddRequestFields
|
11
|
+
extend T::Sig
|
12
|
+
extend T::Helpers
|
13
|
+
|
14
|
+
requires_ancestor { Interfaces::RequestFields }
|
15
|
+
|
16
|
+
# Helper method to serialize request fields
|
17
|
+
sig { params(hash: T::Hash[Symbol, T.untyped]).void }
|
18
|
+
def add_request_fields(hash)
|
19
|
+
# Add request-specific fields if they're present
|
20
|
+
hash[LOG_KEYS.fetch(:path)] = path if path
|
21
|
+
hash[LOG_KEYS.fetch(:http_method)] = http_method if http_method # Use `method` in JSON
|
22
|
+
hash[LOG_KEYS.fetch(:source_ip)] = source_ip if source_ip
|
23
|
+
hash[LOG_KEYS.fetch(:user_agent)] = user_agent if user_agent
|
24
|
+
hash[LOG_KEYS.fetch(:referer)] = referer if referer
|
25
|
+
hash[LOG_KEYS.fetch(:request_id)] = request_id if request_id
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../../log_keys"
|
5
|
+
require_relative "../interfaces/additional_data_field"
|
6
|
+
require_relative "serialize_common"
|
7
|
+
|
8
|
+
module LogStruct
|
9
|
+
module Log
|
10
|
+
# Helper module for merging additional data into serialized logs
|
11
|
+
module MergeAdditionalDataFields
|
12
|
+
extend T::Sig
|
13
|
+
extend T::Helpers
|
14
|
+
|
15
|
+
include SerializeCommon
|
16
|
+
|
17
|
+
requires_ancestor { T::Struct }
|
18
|
+
requires_ancestor { Interfaces::AdditionalDataField }
|
19
|
+
|
20
|
+
sig { params(hash: T::Hash[Symbol, T.untyped]).void }
|
21
|
+
def merge_additional_data_fields(hash)
|
22
|
+
additional_data.each do |key, value|
|
23
|
+
hash[key.to_sym] = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../../log_keys"
|
5
|
+
require_relative "../interfaces/common_fields"
|
6
|
+
|
7
|
+
module LogStruct
|
8
|
+
module Log
|
9
|
+
# Common log serialization method
|
10
|
+
module SerializeCommon
|
11
|
+
extend T::Sig
|
12
|
+
extend T::Helpers
|
13
|
+
|
14
|
+
requires_ancestor { Interfaces::CommonFields }
|
15
|
+
|
16
|
+
# Convert the log entry to a hash for serialization.
|
17
|
+
# (strict param is unused, but need same signature as default T::Struct.serialize)
|
18
|
+
sig { params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
19
|
+
def serialize_common(strict = true)
|
20
|
+
{
|
21
|
+
LOG_KEYS.fetch(:source) => source.serialize.to_s,
|
22
|
+
LOG_KEYS.fetch(:event) => event.serialize.to_s,
|
23
|
+
LOG_KEYS.fetch(:level) => level.serialize.to_s,
|
24
|
+
LOG_KEYS.fetch(:timestamp) => timestamp.iso8601(3)
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Override as_json to use our custom serialize method instead of default T::Struct serialization
|
29
|
+
sig { params(options: T.untyped).returns(T::Hash[String, T.untyped]) }
|
30
|
+
def as_json(options = nil)
|
31
|
+
# Convert symbol keys to strings for JSON
|
32
|
+
serialize.transform_keys(&:to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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
|
+
# Shrine log entry for structured logging
|
16
|
+
class Shrine < T::Struct
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
include Interfaces::CommonFields
|
20
|
+
include Interfaces::AdditionalDataField
|
21
|
+
include SerializeCommon
|
22
|
+
include MergeAdditionalDataFields
|
23
|
+
|
24
|
+
ShrineEvent = T.type_alias {
|
25
|
+
T.any(
|
26
|
+
Event::Upload,
|
27
|
+
Event::Download,
|
28
|
+
Event::Delete,
|
29
|
+
Event::Metadata,
|
30
|
+
Event::Exist,
|
31
|
+
Event::Unknown
|
32
|
+
)
|
33
|
+
}
|
34
|
+
|
35
|
+
# Common fields
|
36
|
+
const :source, Source::Shrine, default: T.let(Source::Shrine, Source::Shrine)
|
37
|
+
const :event, ShrineEvent
|
38
|
+
const :timestamp, Time, factory: -> { Time.now }
|
39
|
+
const :level, Level, default: T.let(Level::Info, Level)
|
40
|
+
|
41
|
+
# Shrine-specific fields
|
42
|
+
const :storage, T.nilable(String), default: nil
|
43
|
+
const :location, T.nilable(String), default: nil
|
44
|
+
const :upload_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
|
45
|
+
const :download_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
|
46
|
+
const :options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
|
47
|
+
const :uploader, T.nilable(String), default: nil
|
48
|
+
const :duration, T.nilable(Float), default: nil
|
49
|
+
const :additional_data, T::Hash[Symbol, T.untyped], default: {}
|
50
|
+
|
51
|
+
# Convert the log entry to a hash for serialization
|
52
|
+
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
53
|
+
def serialize(strict = true)
|
54
|
+
hash = serialize_common(strict)
|
55
|
+
merge_additional_data_fields(hash)
|
56
|
+
|
57
|
+
# Add Shrine-specific fields if they're present
|
58
|
+
hash[LOG_KEYS.fetch(:storage)] = storage if storage
|
59
|
+
hash[LOG_KEYS.fetch(:location)] = location if location
|
60
|
+
hash[LOG_KEYS.fetch(:upload_options)] = upload_options if upload_options
|
61
|
+
hash[LOG_KEYS.fetch(:download_options)] = download_options if download_options
|
62
|
+
hash[LOG_KEYS.fetch(:options)] = options if options
|
63
|
+
hash[LOG_KEYS.fetch(:uploader)] = uploader if uploader
|
64
|
+
hash[LOG_KEYS.fetch(:duration)] = duration if duration
|
65
|
+
|
66
|
+
hash
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "interfaces/common_fields"
|
5
|
+
require_relative "shared/serialize_common"
|
6
|
+
require_relative "../enums/source"
|
7
|
+
require_relative "../enums/event"
|
8
|
+
require_relative "../enums/level"
|
9
|
+
require_relative "../log_keys"
|
10
|
+
|
11
|
+
module LogStruct
|
12
|
+
module Log
|
13
|
+
# Sidekiq log entry for structured logging
|
14
|
+
class Sidekiq < T::Struct
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
include Interfaces::CommonFields
|
18
|
+
include SerializeCommon
|
19
|
+
|
20
|
+
# Define valid event types for Sidekiq (currently only Log is used)
|
21
|
+
SidekiqEvent = T.type_alias { Event::Log }
|
22
|
+
|
23
|
+
# Common fields
|
24
|
+
const :source, Source::Sidekiq, default: T.let(Source::Sidekiq, Source::Sidekiq)
|
25
|
+
const :event, SidekiqEvent, default: T.let(Event::Log, SidekiqEvent)
|
26
|
+
const :timestamp, Time, factory: -> { Time.now }
|
27
|
+
const :level, Level, default: T.let(Level::Info, Level)
|
28
|
+
|
29
|
+
# Sidekiq-specific fields
|
30
|
+
const :process_id, T.nilable(Integer), default: nil
|
31
|
+
const :thread_id, T.nilable(T.any(Integer, String)), default: nil
|
32
|
+
const :message, T.nilable(String), default: nil
|
33
|
+
const :context, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
|
34
|
+
|
35
|
+
# Convert the log entry to a hash for serialization
|
36
|
+
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
37
|
+
def serialize(strict = true)
|
38
|
+
hash = serialize_common(strict)
|
39
|
+
|
40
|
+
# Add Sidekiq-specific fields if they're present
|
41
|
+
hash[LOG_KEYS.fetch(:message)] = message if message
|
42
|
+
hash[LOG_KEYS.fetch(:context)] = context if context
|
43
|
+
hash[LOG_KEYS.fetch(:process_id)] = process_id if process_id
|
44
|
+
hash[LOG_KEYS.fetch(:thread_id)] = thread_id if thread_id
|
45
|
+
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,126 @@
|
|
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
|
+
|
9
|
+
module LogStruct
|
10
|
+
module Log
|
11
|
+
# SQL Query Log Structure
|
12
|
+
#
|
13
|
+
# Captures detailed information about SQL queries executed through ActiveRecord.
|
14
|
+
# This provides structured logging for database operations, including:
|
15
|
+
# - Query text and operation name
|
16
|
+
# - Execution timing and performance metrics
|
17
|
+
# - Row counts and connection information
|
18
|
+
# - Safely filtered bind parameters
|
19
|
+
#
|
20
|
+
# ## Use Cases:
|
21
|
+
# - Development debugging of N+1 queries
|
22
|
+
# - Production performance monitoring
|
23
|
+
# - Database query analysis and optimization
|
24
|
+
# - Audit trails for data access patterns
|
25
|
+
#
|
26
|
+
# ## Security:
|
27
|
+
# - SQL queries are safe (always parameterized with ?)
|
28
|
+
# - Bind parameters are filtered through LogStruct's param filters
|
29
|
+
# - Sensitive data like passwords, tokens are automatically scrubbed
|
30
|
+
#
|
31
|
+
# ## Example Usage:
|
32
|
+
#
|
33
|
+
# ```ruby
|
34
|
+
# # Automatically captured when SQL query integration is enabled
|
35
|
+
# LogStruct.config.integrations.enable_sql_logging = true
|
36
|
+
#
|
37
|
+
# # Manual logging (rare)
|
38
|
+
# sql_log = LogStruct::Log::SQL.new(
|
39
|
+
# message: "User lookup query",
|
40
|
+
# sql: "SELECT * FROM users WHERE id = ?",
|
41
|
+
# name: "User Load",
|
42
|
+
# duration: 2.3,
|
43
|
+
# row_count: 1,
|
44
|
+
# bind_params: [123]
|
45
|
+
# )
|
46
|
+
# LogStruct.info(sql_log)
|
47
|
+
# ```
|
48
|
+
class SQL < T::Struct
|
49
|
+
extend T::Sig
|
50
|
+
include Interfaces::CommonFields
|
51
|
+
include Interfaces::AdditionalDataField
|
52
|
+
include SerializeCommon
|
53
|
+
include MergeAdditionalDataFields
|
54
|
+
|
55
|
+
SQLEvent = T.type_alias {
|
56
|
+
Event::Database
|
57
|
+
}
|
58
|
+
|
59
|
+
# Common fields
|
60
|
+
const :source, Source, default: T.let(Source::App, Source)
|
61
|
+
const :event, SQLEvent, default: T.let(Event::Database, SQLEvent)
|
62
|
+
const :level, Level, default: T.let(Level::Info, Level)
|
63
|
+
const :timestamp, Time, factory: -> { Time.now }
|
64
|
+
const :message, String
|
65
|
+
|
66
|
+
# The SQL query that was executed (parameterized, safe to log)
|
67
|
+
const :sql, String
|
68
|
+
|
69
|
+
# The name of the database operation (e.g., "User Load", "Post Create")
|
70
|
+
const :name, String
|
71
|
+
|
72
|
+
# Duration of the query execution in milliseconds
|
73
|
+
const :duration, Float
|
74
|
+
|
75
|
+
# Number of rows affected or returned by the query
|
76
|
+
const :row_count, T.nilable(Integer)
|
77
|
+
|
78
|
+
# Database connection information (adapter name)
|
79
|
+
const :connection_adapter, T.nilable(String)
|
80
|
+
|
81
|
+
# Filtered bind parameters (sensitive data removed)
|
82
|
+
const :bind_params, T.nilable(T::Array[T.untyped])
|
83
|
+
|
84
|
+
# Database name (if available)
|
85
|
+
const :database_name, T.nilable(String)
|
86
|
+
|
87
|
+
# Connection pool size information (for monitoring)
|
88
|
+
const :connection_pool_size, T.nilable(Integer)
|
89
|
+
|
90
|
+
# Active connection count (for monitoring)
|
91
|
+
const :active_connections, T.nilable(Integer)
|
92
|
+
|
93
|
+
# SQL operation type (SELECT, INSERT, UPDATE, DELETE, etc.)
|
94
|
+
const :operation_type, T.nilable(String)
|
95
|
+
|
96
|
+
# Table names involved in the query (extracted from SQL)
|
97
|
+
const :table_names, T.nilable(T::Array[String])
|
98
|
+
|
99
|
+
# Allow additional custom data
|
100
|
+
const :additional_data, T::Hash[Symbol, T.untyped], default: {}
|
101
|
+
|
102
|
+
# Convert the log entry to a hash for serialization
|
103
|
+
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
|
104
|
+
def serialize(strict = true)
|
105
|
+
hash = serialize_common(strict)
|
106
|
+
merge_additional_data_fields(hash)
|
107
|
+
|
108
|
+
# Add SQL-specific fields using LOG_KEYS mapping for consistency
|
109
|
+
hash[LOG_KEYS.fetch(:message)] = message
|
110
|
+
hash[LOG_KEYS.fetch(:sql)] = sql
|
111
|
+
hash[LOG_KEYS.fetch(:name)] = name
|
112
|
+
hash[LOG_KEYS.fetch(:duration)] = duration
|
113
|
+
hash[LOG_KEYS.fetch(:row_count)] = row_count
|
114
|
+
hash[LOG_KEYS.fetch(:connection_adapter)] = connection_adapter
|
115
|
+
hash[LOG_KEYS.fetch(:bind_params)] = bind_params
|
116
|
+
hash[LOG_KEYS.fetch(:database_name)] = database_name
|
117
|
+
hash[LOG_KEYS.fetch(:connection_pool_size)] = connection_pool_size
|
118
|
+
hash[LOG_KEYS.fetch(:active_connections)] = active_connections
|
119
|
+
hash[LOG_KEYS.fetch(:operation_type)] = operation_type
|
120
|
+
hash[LOG_KEYS.fetch(:table_names)] = table_names
|
121
|
+
|
122
|
+
hash
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Common Enums
|
5
|
+
require_relative "enums/source"
|
6
|
+
require_relative "enums/event"
|
7
|
+
require_relative "enums/level"
|
8
|
+
|
9
|
+
# Log Structs
|
10
|
+
require_relative "log/carrierwave"
|
11
|
+
require_relative "log/action_mailer"
|
12
|
+
require_relative "log/active_storage"
|
13
|
+
require_relative "log/active_job"
|
14
|
+
require_relative "log/error"
|
15
|
+
require_relative "log/good_job"
|
16
|
+
require_relative "log/plain"
|
17
|
+
require_relative "log/request"
|
18
|
+
require_relative "log/security"
|
19
|
+
require_relative "log/shrine"
|
20
|
+
require_relative "log/sidekiq"
|
21
|
+
require_relative "log/sql"
|
22
|
+
|
23
|
+
module LogStruct
|
24
|
+
# Type aliases for all possible log types
|
25
|
+
# This should be updated whenever a new log type is added
|
26
|
+
# (Can't use sealed! unless we want to put everything in one giant file.)
|
27
|
+
LogClassType = T.type_alias do
|
28
|
+
T.any(
|
29
|
+
T.class_of(LogStruct::Log::CarrierWave),
|
30
|
+
T.class_of(LogStruct::Log::ActionMailer),
|
31
|
+
T.class_of(LogStruct::Log::ActiveStorage),
|
32
|
+
T.class_of(LogStruct::Log::ActiveJob),
|
33
|
+
T.class_of(LogStruct::Log::Error),
|
34
|
+
T.class_of(LogStruct::Log::GoodJob),
|
35
|
+
T.class_of(LogStruct::Log::Plain),
|
36
|
+
T.class_of(LogStruct::Log::Request),
|
37
|
+
T.class_of(LogStruct::Log::Security),
|
38
|
+
T.class_of(LogStruct::Log::Shrine),
|
39
|
+
T.class_of(LogStruct::Log::Sidekiq),
|
40
|
+
T.class_of(LogStruct::Log::SQL)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module LogStruct
|
5
|
+
# Define a mapping of property names to JSON keys
|
6
|
+
LOG_KEYS = T.let({
|
7
|
+
# Ruby struct property name => JSON key name
|
8
|
+
|
9
|
+
# Shared fields
|
10
|
+
source: :src,
|
11
|
+
event: :evt,
|
12
|
+
timestamp: :ts,
|
13
|
+
level: :lvl,
|
14
|
+
|
15
|
+
# Common fields
|
16
|
+
message: :msg,
|
17
|
+
data: :data,
|
18
|
+
|
19
|
+
# Request-related fields
|
20
|
+
path: :path,
|
21
|
+
http_method: :method, # Use `http_method` because `method` is a reserved word
|
22
|
+
source_ip: :source_ip,
|
23
|
+
user_agent: :user_agent,
|
24
|
+
referer: :referer,
|
25
|
+
request_id: :request_id,
|
26
|
+
|
27
|
+
# HTTP-specific fields
|
28
|
+
format: :format,
|
29
|
+
controller: :controller,
|
30
|
+
action: :action,
|
31
|
+
status: :status,
|
32
|
+
duration: :duration,
|
33
|
+
view: :view,
|
34
|
+
db: :db,
|
35
|
+
params: :params,
|
36
|
+
|
37
|
+
# Security-specific fields
|
38
|
+
blocked_host: :blocked_host,
|
39
|
+
blocked_hosts: :blocked_hosts,
|
40
|
+
client_ip: :client_ip,
|
41
|
+
x_forwarded_for: :x_forwarded_for,
|
42
|
+
|
43
|
+
# Email-specific fields
|
44
|
+
to: :to,
|
45
|
+
from: :from,
|
46
|
+
subject: :subject,
|
47
|
+
|
48
|
+
# Error fields
|
49
|
+
err_class: :err_class,
|
50
|
+
backtrace: :backtrace,
|
51
|
+
|
52
|
+
# Job-specific fields
|
53
|
+
job_id: :job_id,
|
54
|
+
job_class: :job_class,
|
55
|
+
queue_name: :queue_name,
|
56
|
+
arguments: :arguments,
|
57
|
+
retry_count: :retry_count,
|
58
|
+
|
59
|
+
# Sidekiq-specific fields
|
60
|
+
process_id: :pid,
|
61
|
+
thread_id: :tid,
|
62
|
+
context: :ctx,
|
63
|
+
|
64
|
+
# Storage-specific fields (ActiveStorage)
|
65
|
+
checksum: :checksum,
|
66
|
+
exist: :exist,
|
67
|
+
url: :url,
|
68
|
+
prefix: :prefix,
|
69
|
+
range: :range,
|
70
|
+
|
71
|
+
# Storage-specific fields (Shrine)
|
72
|
+
storage: :storage,
|
73
|
+
operation: :op,
|
74
|
+
file_id: :file_id,
|
75
|
+
filename: :filename,
|
76
|
+
mime_type: :mime_type,
|
77
|
+
size: :size,
|
78
|
+
metadata: :metadata,
|
79
|
+
location: :location,
|
80
|
+
upload_options: :upload_opts,
|
81
|
+
download_options: :download_opts,
|
82
|
+
options: :opts,
|
83
|
+
uploader: :uploader,
|
84
|
+
|
85
|
+
# CarrierWave-specific fields
|
86
|
+
model: :model,
|
87
|
+
mount_point: :mount_point,
|
88
|
+
|
89
|
+
# SQL-specific fields
|
90
|
+
sql: :sql,
|
91
|
+
name: :name,
|
92
|
+
row_count: :row_count,
|
93
|
+
connection_adapter: :connection_adapter,
|
94
|
+
bind_params: :bind_params,
|
95
|
+
database_name: :database_name,
|
96
|
+
connection_pool_size: :connection_pool_size,
|
97
|
+
active_connections: :active_connections,
|
98
|
+
operation_type: :operation_type,
|
99
|
+
table_names: :table_names
|
100
|
+
}.freeze,
|
101
|
+
T::Hash[Symbol, Symbol])
|
102
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "active_support/tagged_logging"
|
5
|
+
|
6
|
+
# Monkey-patch ActiveSupport::TaggedLogging::Formatter to support hash inputs
|
7
|
+
# This allows us to pass structured data to the logger and have tags incorporated
|
8
|
+
# directly into the hash instead of being prepended as strings
|
9
|
+
module ActiveSupport
|
10
|
+
module TaggedLogging
|
11
|
+
module FormatterExtension
|
12
|
+
extend T::Sig
|
13
|
+
extend T::Helpers
|
14
|
+
requires_ancestor { ::ActiveSupport::TaggedLogging::Formatter }
|
15
|
+
|
16
|
+
# Override the call method to support hash input/output, and wrap
|
17
|
+
# plain strings in a Hash under a `msg` key.
|
18
|
+
# The data is then passed to our custom log formatter that transforms it
|
19
|
+
# into a JSON string before logging.
|
20
|
+
sig { params(severity: T.any(String, Symbol), time: Time, progname: T.untyped, data: T.untyped).returns(String) }
|
21
|
+
def call(severity, time, progname, data)
|
22
|
+
# Convert data to a hash if it's not already one
|
23
|
+
data = {message: data.to_s} unless data.is_a?(Hash)
|
24
|
+
|
25
|
+
# Add current tags to the hash if present
|
26
|
+
tags = current_tags
|
27
|
+
data[:tags] = tags if tags.present?
|
28
|
+
|
29
|
+
# Call the original formatter with our enhanced data
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveSupport::TaggedLogging::Formatter.prepend(ActiveSupport::TaggedLogging::FormatterExtension)
|