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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/LICENSE +21 -0
  4. data/README.md +67 -0
  5. data/lib/log_struct/concerns/configuration.rb +93 -0
  6. data/lib/log_struct/concerns/error_handling.rb +94 -0
  7. data/lib/log_struct/concerns/logging.rb +45 -0
  8. data/lib/log_struct/config_struct/error_handling_modes.rb +25 -0
  9. data/lib/log_struct/config_struct/filters.rb +80 -0
  10. data/lib/log_struct/config_struct/integrations.rb +89 -0
  11. data/lib/log_struct/configuration.rb +59 -0
  12. data/lib/log_struct/enums/error_handling_mode.rb +22 -0
  13. data/lib/log_struct/enums/error_reporter.rb +14 -0
  14. data/lib/log_struct/enums/event.rb +48 -0
  15. data/lib/log_struct/enums/level.rb +66 -0
  16. data/lib/log_struct/enums/source.rb +26 -0
  17. data/lib/log_struct/enums.rb +9 -0
  18. data/lib/log_struct/formatter.rb +224 -0
  19. data/lib/log_struct/handlers.rb +27 -0
  20. data/lib/log_struct/hash_utils.rb +21 -0
  21. data/lib/log_struct/integrations/action_mailer/callbacks.rb +100 -0
  22. data/lib/log_struct/integrations/action_mailer/error_handling.rb +173 -0
  23. data/lib/log_struct/integrations/action_mailer/event_logging.rb +90 -0
  24. data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +78 -0
  25. data/lib/log_struct/integrations/action_mailer.rb +50 -0
  26. data/lib/log_struct/integrations/active_job/log_subscriber.rb +104 -0
  27. data/lib/log_struct/integrations/active_job.rb +38 -0
  28. data/lib/log_struct/integrations/active_record.rb +258 -0
  29. data/lib/log_struct/integrations/active_storage.rb +94 -0
  30. data/lib/log_struct/integrations/carrierwave.rb +111 -0
  31. data/lib/log_struct/integrations/good_job/log_subscriber.rb +228 -0
  32. data/lib/log_struct/integrations/good_job/logger.rb +73 -0
  33. data/lib/log_struct/integrations/good_job.rb +111 -0
  34. data/lib/log_struct/integrations/host_authorization.rb +81 -0
  35. data/lib/log_struct/integrations/integration_interface.rb +21 -0
  36. data/lib/log_struct/integrations/lograge.rb +114 -0
  37. data/lib/log_struct/integrations/rack.rb +31 -0
  38. data/lib/log_struct/integrations/rack_error_handler/middleware.rb +146 -0
  39. data/lib/log_struct/integrations/rack_error_handler.rb +32 -0
  40. data/lib/log_struct/integrations/shrine.rb +75 -0
  41. data/lib/log_struct/integrations/sidekiq/logger.rb +43 -0
  42. data/lib/log_struct/integrations/sidekiq.rb +39 -0
  43. data/lib/log_struct/integrations/sorbet.rb +49 -0
  44. data/lib/log_struct/integrations.rb +41 -0
  45. data/lib/log_struct/log/action_mailer.rb +55 -0
  46. data/lib/log_struct/log/active_job.rb +64 -0
  47. data/lib/log_struct/log/active_storage.rb +78 -0
  48. data/lib/log_struct/log/carrierwave.rb +82 -0
  49. data/lib/log_struct/log/error.rb +76 -0
  50. data/lib/log_struct/log/good_job.rb +151 -0
  51. data/lib/log_struct/log/interfaces/additional_data_field.rb +20 -0
  52. data/lib/log_struct/log/interfaces/common_fields.rb +42 -0
  53. data/lib/log_struct/log/interfaces/message_field.rb +20 -0
  54. data/lib/log_struct/log/interfaces/request_fields.rb +36 -0
  55. data/lib/log_struct/log/plain.rb +53 -0
  56. data/lib/log_struct/log/request.rb +76 -0
  57. data/lib/log_struct/log/security.rb +80 -0
  58. data/lib/log_struct/log/shared/add_request_fields.rb +29 -0
  59. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +28 -0
  60. data/lib/log_struct/log/shared/serialize_common.rb +36 -0
  61. data/lib/log_struct/log/shrine.rb +70 -0
  62. data/lib/log_struct/log/sidekiq.rb +50 -0
  63. data/lib/log_struct/log/sql.rb +126 -0
  64. data/lib/log_struct/log.rb +43 -0
  65. data/lib/log_struct/log_keys.rb +102 -0
  66. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +36 -0
  67. data/lib/log_struct/multi_error_reporter.rb +149 -0
  68. data/lib/log_struct/param_filters.rb +89 -0
  69. data/lib/log_struct/railtie.rb +31 -0
  70. data/lib/log_struct/semantic_logger/color_formatter.rb +209 -0
  71. data/lib/log_struct/semantic_logger/formatter.rb +94 -0
  72. data/lib/log_struct/semantic_logger/logger.rb +129 -0
  73. data/lib/log_struct/semantic_logger/setup.rb +219 -0
  74. data/lib/log_struct/sorbet/serialize_symbol_keys.rb +23 -0
  75. data/lib/log_struct/sorbet.rb +13 -0
  76. data/lib/log_struct/string_scrubber.rb +84 -0
  77. data/lib/log_struct/version.rb +6 -0
  78. data/lib/log_struct.rb +37 -0
  79. data/lib/logstruct.rb +2 -6
  80. data/logstruct.gemspec +52 -0
  81. metadata +221 -5
  82. 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)