logstruct 0.1.2 → 0.1.3

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -1
  3. data/README.md +4 -6
  4. data/lib/log_struct/concerns/configuration.rb +2 -2
  5. data/lib/log_struct/config_struct/integrations.rb +5 -0
  6. data/lib/log_struct/enums/log_field.rb +12 -1
  7. data/lib/log_struct/integrations/action_mailer/error_handling.rb +121 -27
  8. data/lib/log_struct/integrations/action_mailer/event_logging.rb +30 -14
  9. data/lib/log_struct/integrations/action_mailer/metadata_collection.rb +18 -24
  10. data/lib/log_struct/integrations/action_mailer.rb +13 -6
  11. data/lib/log_struct/integrations/active_job/log_subscriber.rb +2 -2
  12. data/lib/log_struct/integrations/active_storage.rb +8 -8
  13. data/lib/log_struct/integrations/ahoy.rb +2 -3
  14. data/lib/log_struct/integrations/carrierwave.rb +8 -10
  15. data/lib/log_struct/integrations/good_job/log_subscriber.rb +5 -5
  16. data/lib/log_struct/integrations/good_job/logger.rb +2 -6
  17. data/lib/log_struct/integrations/good_job.rb +1 -1
  18. data/lib/log_struct/integrations/host_authorization.rb +27 -36
  19. data/lib/log_struct/integrations/lograge.rb +1 -1
  20. data/lib/log_struct/integrations/shrine.rb +21 -24
  21. data/lib/log_struct/integrations/sidekiq/logger.rb +8 -1
  22. data/lib/log_struct/log/action_mailer/delivered.rb +14 -49
  23. data/lib/log_struct/log/action_mailer/delivery.rb +14 -49
  24. data/lib/log_struct/log/action_mailer/error.rb +72 -0
  25. data/lib/log_struct/log/action_mailer.rb +15 -2
  26. data/lib/log_struct/log/active_job/enqueue.rb +9 -73
  27. data/lib/log_struct/log/active_job/finish.rb +9 -76
  28. data/lib/log_struct/log/active_job/schedule.rb +9 -73
  29. data/lib/log_struct/log/active_job/start.rb +9 -76
  30. data/lib/log_struct/log/active_job.rb +2 -2
  31. data/lib/log_struct/log/active_model_serializers.rb +5 -45
  32. data/lib/log_struct/log/active_storage/delete.rb +8 -46
  33. data/lib/log_struct/log/active_storage/download.rb +9 -55
  34. data/lib/log_struct/log/active_storage/exist.rb +9 -49
  35. data/lib/log_struct/log/active_storage/metadata.rb +9 -49
  36. data/lib/log_struct/log/active_storage/stream.rb +9 -49
  37. data/lib/log_struct/log/active_storage/upload.rb +9 -64
  38. data/lib/log_struct/log/active_storage/url.rb +9 -49
  39. data/lib/log_struct/log/active_storage.rb +2 -2
  40. data/lib/log_struct/log/ahoy.rb +5 -43
  41. data/lib/log_struct/log/carrierwave/delete.rb +15 -69
  42. data/lib/log_struct/log/carrierwave/download.rb +15 -77
  43. data/lib/log_struct/log/carrierwave/upload.rb +15 -83
  44. data/lib/log_struct/log/carrierwave.rb +13 -4
  45. data/lib/log_struct/log/dotenv/load.rb +5 -33
  46. data/lib/log_struct/log/dotenv/restore.rb +5 -33
  47. data/lib/log_struct/log/dotenv/save.rb +5 -33
  48. data/lib/log_struct/log/dotenv/update.rb +5 -33
  49. data/lib/log_struct/log/error.rb +7 -40
  50. data/lib/log_struct/log/good_job/enqueue.rb +9 -72
  51. data/lib/log_struct/log/good_job/error.rb +9 -89
  52. data/lib/log_struct/log/good_job/finish.rb +9 -78
  53. data/lib/log_struct/log/good_job/log.rb +11 -75
  54. data/lib/log_struct/log/good_job/schedule.rb +7 -78
  55. data/lib/log_struct/log/good_job/start.rb +7 -78
  56. data/lib/log_struct/log/good_job.rb +2 -2
  57. data/lib/log_struct/log/plain.rb +5 -32
  58. data/lib/log_struct/log/puma/shutdown.rb +5 -32
  59. data/lib/log_struct/log/puma/start.rb +5 -56
  60. data/lib/log_struct/log/request.rb +7 -90
  61. data/lib/log_struct/log/security/blocked_host.rb +12 -73
  62. data/lib/log_struct/log/security/csrf_violation.rb +6 -67
  63. data/lib/log_struct/log/security/ip_spoof.rb +6 -73
  64. data/lib/log_struct/log/shrine/delete.rb +6 -41
  65. data/lib/log_struct/log/shrine/download.rb +6 -44
  66. data/lib/log_struct/log/shrine/exist.rb +6 -44
  67. data/lib/log_struct/log/shrine/metadata.rb +8 -46
  68. data/lib/log_struct/log/shrine/upload.rb +6 -53
  69. data/lib/log_struct/log/sidekiq.rb +5 -42
  70. data/lib/log_struct/log/sql.rb +5 -65
  71. data/lib/log_struct/log.rb +2 -2
  72. data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +12 -1
  73. data/lib/log_struct/railtie.rb +0 -22
  74. data/lib/log_struct/semantic_logger/concerns/log_methods.rb +100 -0
  75. data/lib/log_struct/semantic_logger/logger.rb +46 -15
  76. data/lib/log_struct/semantic_logger/setup.rb +11 -7
  77. data/lib/log_struct/shared/{shared/add_request_fields.rb → add_request_fields.rb} +2 -2
  78. data/lib/log_struct/shared/{shared/merge_additional_data_fields.rb → merge_additional_data_fields.rb} +1 -1
  79. data/lib/log_struct/shared/{shared/serialize_common.rb → serialize_common.rb} +9 -3
  80. data/lib/log_struct/{log/shared → shared}/serialize_common_public.rb +2 -2
  81. data/lib/log_struct/version.rb +1 -1
  82. data/lib/log_struct.rb +4 -1
  83. data/logstruct.gemspec +1 -1
  84. metadata +9 -11
  85. data/lib/log_struct/integrations/action_mailer/callbacks.rb +0 -100
  86. data/lib/log_struct/log/shared/add_request_fields.rb +0 -4
  87. data/lib/log_struct/log/shared/merge_additional_data_fields.rb +0 -4
  88. data/lib/log_struct/log/shared/serialize_common.rb +0 -4
@@ -9,9 +9,9 @@
9
9
  require "log_struct/shared/interfaces/common_fields"
10
10
  require "log_struct/shared/interfaces/additional_data_field"
11
11
  require "log_struct/shared/interfaces/request_fields"
12
- require "log_struct/shared/shared/serialize_common"
13
- require "log_struct/shared/shared/merge_additional_data_fields"
14
- require "log_struct/shared/shared/add_request_fields"
12
+ require "log_struct/shared/serialize_common"
13
+ require "log_struct/shared/merge_additional_data_fields"
14
+ require "log_struct/shared/add_request_fields"
15
15
  require_relative "../../enums/source"
16
16
  require_relative "../../enums/event"
17
17
  require_relative "../../enums/level"
@@ -21,11 +21,6 @@ module LogStruct
21
21
  module Log
22
22
  class Shrine
23
23
  class Upload < T::Struct
24
- # typed: strict
25
- # frozen_string_literal: true
26
-
27
- extend T::Sig
28
-
29
24
  extend T::Sig
30
25
 
31
26
  # Shared/common fields
@@ -35,48 +30,20 @@ module LogStruct
35
30
  const :level, Level, default: Level::Info
36
31
 
37
32
  # Event-specific fields
38
- const :storage, String
33
+ const :storage, Symbol
39
34
  const :location, String
40
35
  const :upload_options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
41
36
  const :options, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
42
37
  const :uploader, T.nilable(String), default: nil
43
38
  const :duration_ms, T.nilable(Float), default: nil
44
39
 
45
- # Additional data
46
- include LogStruct::Log::Interfaces::AdditionalDataField
47
- const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
48
- include LogStruct::Log::Shared::MergeAdditionalDataFields
49
-
50
- # Request fields (optional)
51
-
52
40
  # Serialize shared fields
53
41
  include LogStruct::Log::Interfaces::CommonFields
54
42
  include LogStruct::Log::Shared::SerializeCommon
55
43
 
56
44
  sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
57
- def self.base_hash
58
- {}
59
- end
60
-
61
- sig {
62
- params(storage: T.untyped,
63
- location: T.untyped,
64
- upload_options: T.untyped,
65
- options: T.untyped,
66
- uploader: T.untyped,
67
- duration_ms: T.untyped,
68
- additional_data: T.untyped,
69
- timestamp: T.untyped).returns(T::Hash[LogStruct::LogField, T.untyped])
70
- }
71
- def self.build(storage:,
72
- location:,
73
- upload_options: nil,
74
- options: nil,
75
- uploader: nil,
76
- duration_ms: nil,
77
- additional_data: nil,
78
- timestamp: Time.now)
79
- h = base_hash
45
+ def to_h
46
+ h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
80
47
  h[LogField::Storage] = storage
81
48
  h[LogField::Location] = location
82
49
  h[LogField::UploadOptions] = upload_options unless upload_options.nil?
@@ -85,20 +52,6 @@ module LogStruct
85
52
  h[LogField::DurationMs] = duration_ms unless duration_ms.nil?
86
53
  h
87
54
  end
88
-
89
- sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
90
- def to_h
91
- self.class.build(
92
- storage: storage,
93
- location: location,
94
- upload_options: upload_options,
95
- options: options,
96
- uploader: uploader,
97
- duration_ms: duration_ms,
98
- additional_data: additional_data,
99
- timestamp: timestamp
100
- )
101
- end
102
55
  end
103
56
  end
104
57
  end
@@ -9,9 +9,9 @@
9
9
  require "log_struct/shared/interfaces/common_fields"
10
10
  require "log_struct/shared/interfaces/additional_data_field"
11
11
  require "log_struct/shared/interfaces/request_fields"
12
- require "log_struct/shared/shared/serialize_common"
13
- require "log_struct/shared/shared/merge_additional_data_fields"
14
- require "log_struct/shared/shared/add_request_fields"
12
+ require "log_struct/shared/serialize_common"
13
+ require "log_struct/shared/merge_additional_data_fields"
14
+ require "log_struct/shared/add_request_fields"
15
15
  require_relative "../enums/source"
16
16
  require_relative "../enums/event"
17
17
  require_relative "../enums/level"
@@ -20,11 +20,6 @@ require_relative "../enums/log_field"
20
20
  module LogStruct
21
21
  module Log
22
22
  class Sidekiq < T::Struct
23
- # typed: strict
24
- # frozen_string_literal: true
25
-
26
- extend T::Sig
27
-
28
23
  extend T::Sig
29
24
 
30
25
  # Shared/common fields
@@ -39,51 +34,19 @@ module LogStruct
39
34
  const :process_id, T.nilable(Integer), default: nil
40
35
  const :thread_id, T.nilable(T.any(Integer, String)), default: nil
41
36
 
42
- # Additional data
43
-
44
- # Request fields (optional)
45
-
46
37
  # Serialize shared fields
47
38
  include LogStruct::Log::Interfaces::CommonFields
48
39
  include LogStruct::Log::Shared::SerializeCommon
49
40
 
50
41
  sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
51
- def self.base_hash
52
- {}
53
- end
54
-
55
- sig {
56
- params(message: T.untyped,
57
- context: T.untyped,
58
- process_id: T.untyped,
59
- thread_id: T.untyped,
60
- additional_data: T.untyped,
61
- timestamp: T.untyped).returns(T::Hash[LogStruct::LogField, T.untyped])
62
- }
63
- def self.build(message: nil,
64
- context: nil,
65
- process_id: nil,
66
- thread_id: nil,
67
- additional_data: nil,
68
- timestamp: Time.now)
69
- h = base_hash
42
+ def to_h
43
+ h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
70
44
  h[LogField::Message] = message unless message.nil?
71
45
  h[LogField::Context] = context unless context.nil?
72
46
  h[LogField::ProcessId] = process_id unless process_id.nil?
73
47
  h[LogField::ThreadId] = thread_id unless thread_id.nil?
74
48
  h
75
49
  end
76
-
77
- sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
78
- def to_h
79
- self.class.build(
80
- message: message,
81
- context: context,
82
- process_id: process_id,
83
- thread_id: thread_id,
84
- timestamp: timestamp
85
- )
86
- end
87
50
  end
88
51
  end
89
52
  end
@@ -9,9 +9,9 @@
9
9
  require "log_struct/shared/interfaces/common_fields"
10
10
  require "log_struct/shared/interfaces/additional_data_field"
11
11
  require "log_struct/shared/interfaces/request_fields"
12
- require "log_struct/shared/shared/serialize_common"
13
- require "log_struct/shared/shared/merge_additional_data_fields"
14
- require "log_struct/shared/shared/add_request_fields"
12
+ require "log_struct/shared/serialize_common"
13
+ require "log_struct/shared/merge_additional_data_fields"
14
+ require "log_struct/shared/add_request_fields"
15
15
  require_relative "../enums/source"
16
16
  require_relative "../enums/event"
17
17
  require_relative "../enums/level"
@@ -20,11 +20,6 @@ require_relative "../enums/log_field"
20
20
  module LogStruct
21
21
  module Log
22
22
  class SQL < T::Struct
23
- # typed: strict
24
- # frozen_string_literal: true
25
-
26
- extend T::Sig
27
-
28
23
  extend T::Sig
29
24
 
30
25
  # Shared/common fields
@@ -52,48 +47,13 @@ module LogStruct
52
47
  const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
53
48
  include LogStruct::Log::Shared::MergeAdditionalDataFields
54
49
 
55
- # Request fields (optional)
56
-
57
50
  # Serialize shared fields
58
51
  include LogStruct::Log::Interfaces::CommonFields
59
52
  include LogStruct::Log::Shared::SerializeCommon
60
53
 
61
54
  sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
62
- def self.base_hash
63
- {}
64
- end
65
-
66
- sig {
67
- params(message: T.untyped,
68
- sql: T.untyped,
69
- name: T.untyped,
70
- duration_ms: T.untyped,
71
- row_count: T.untyped,
72
- adapter: T.untyped,
73
- bind_params: T.untyped,
74
- database_name: T.untyped,
75
- connection_pool_size: T.untyped,
76
- active_connections: T.untyped,
77
- operation_type: T.untyped,
78
- table_names: T.untyped,
79
- additional_data: T.untyped,
80
- timestamp: T.untyped).returns(T::Hash[LogStruct::LogField, T.untyped])
81
- }
82
- def self.build(message:,
83
- sql:,
84
- name:,
85
- duration_ms:,
86
- row_count: nil,
87
- adapter: nil,
88
- bind_params: nil,
89
- database_name: nil,
90
- connection_pool_size: nil,
91
- active_connections: nil,
92
- operation_type: nil,
93
- table_names: nil,
94
- additional_data: nil,
95
- timestamp: Time.now)
96
- h = base_hash
55
+ def to_h
56
+ h = T.let({}, T::Hash[LogStruct::LogField, T.untyped])
97
57
  h[LogField::Message] = message
98
58
  h[LogField::Sql] = sql
99
59
  h[LogField::Name] = name
@@ -108,26 +68,6 @@ module LogStruct
108
68
  h[LogField::TableNames] = table_names unless table_names.nil?
109
69
  h
110
70
  end
111
-
112
- sig { returns(T::Hash[LogStruct::LogField, T.untyped]) }
113
- def to_h
114
- self.class.build(
115
- message: message,
116
- sql: sql,
117
- name: name,
118
- duration_ms: duration_ms,
119
- row_count: row_count,
120
- adapter: adapter,
121
- bind_params: bind_params,
122
- database_name: database_name,
123
- connection_pool_size: connection_pool_size,
124
- active_connections: active_connections,
125
- operation_type: operation_type,
126
- table_names: table_names,
127
- additional_data: additional_data,
128
- timestamp: timestamp
129
- )
130
- end
131
71
  end
132
72
  end
133
73
  end
@@ -7,7 +7,7 @@ require_relative "enums/event"
7
7
  require_relative "enums/level"
8
8
  require_relative "enums/log_field"
9
9
  require_relative "log/interfaces/public_common_fields"
10
- require_relative "log/shared/serialize_common_public"
10
+ require_relative "shared/serialize_common_public"
11
11
 
12
12
  # Dynamically require all top-level log structs under log/*
13
13
  # Nested per-event files are required by their parent files.
@@ -31,7 +31,7 @@ module LogStruct
31
31
  def self.from_exception(source, ex, additional_data = {}, timestamp = Time.now)
32
32
  LogStruct::Log::Error.new(
33
33
  source: source,
34
- err_class: ex.class,
34
+ error_class: ex.class,
35
35
  message: ex.message,
36
36
  backtrace: ex.backtrace,
37
37
  additional_data: additional_data,
@@ -8,6 +8,16 @@ require "active_support/tagged_logging"
8
8
  # directly into the hash instead of being prepended as strings
9
9
  module ActiveSupport
10
10
  module TaggedLogging
11
+ extend T::Sig
12
+
13
+ # Add class-level current_tags method for compatibility with Rails code
14
+ # that expects to call ActiveSupport::TaggedLogging.current_tags
15
+ # Use thread-local storage directly like Rails does internally
16
+ sig { returns(T::Array[T.any(String, Symbol)]) }
17
+ def self.current_tags
18
+ Thread.current[:activesupport_tagged_logging_tags] || []
19
+ end
20
+
11
21
  module FormatterExtension
12
22
  extend T::Sig
13
23
  extend T::Helpers
@@ -23,7 +33,8 @@ module ActiveSupport
23
33
  data = {message: data.to_s} unless data.is_a?(Hash)
24
34
 
25
35
  # Add current tags to the hash if present
26
- tags = current_tags
36
+ # Use thread-local storage directly as fallback if current_tags method doesn't exist
37
+ tags = T.unsafe(self).respond_to?(:current_tags) ? current_tags : (Thread.current[:activesupport_tagged_logging_tags] || [])
27
38
  data[:tags] = tags if tags.present?
28
39
 
29
40
  # Call the original formatter with our enhanced data
@@ -10,32 +10,10 @@ require_relative "integrations"
10
10
  module LogStruct
11
11
  # Railtie to integrate with Rails
12
12
  class Railtie < ::Rails::Railtie
13
- # Ensure test hosts are allowed early enough for middleware build
14
- initializer "logstruct.allow_test_hosts", before: :build_middleware_stack do |app|
15
- if ::Rails.env.test? && app.config.respond_to?(:hosts)
16
- begin
17
- app.config.hosts << /.*\z/
18
- rescue
19
- # best-effort
20
- end
21
- begin
22
- app.config.middleware.delete(::ActionDispatch::HostAuthorization)
23
- rescue
24
- # best-effort
25
- end
26
- end
27
- end
28
-
29
- # After ActionDispatch is configured, remove HostAuthorization in test to prevent 403s
30
- # (No late deletion needed; handled above before middleware stack is built)
31
-
32
13
  # Configure early, right after logger initialization
33
14
  initializer "logstruct.configure_logger", after: :initialize_logger do |app|
34
15
  next unless LogStruct.enabled?
35
16
 
36
- # Apply TaggedLogging monkey patch only when enabled
37
- require_relative "monkey_patches/active_support/tagged_logging/formatter"
38
-
39
17
  # Use SemanticLogger for powerful logging features
40
18
  LogStruct::SemanticLogger::Setup.configure_semantic_logger(app)
41
19
  end
@@ -0,0 +1,100 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module LogStruct
5
+ module SemanticLogger
6
+ module Concerns
7
+ module LogMethods
8
+ extend T::Sig
9
+ extend T::Helpers
10
+ requires_ancestor { LogStruct::SemanticLogger::Logger }
11
+
12
+ # Override log methods to handle LogStruct types and broadcast
13
+ sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
14
+ def debug(message = nil, payload = nil, &block)
15
+ instrument_log(message, :debug)
16
+ result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
17
+ super(nil, payload: message, &block)
18
+ else
19
+ super
20
+ end
21
+ broadcasts.each do |logger|
22
+ next unless logger.respond_to?(:debug)
23
+ message.is_a?(String) ? logger.debug(message) : (logger.debug(&block) if block)
24
+ end
25
+ result
26
+ end
27
+
28
+ sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
29
+ def info(message = nil, payload = nil, &block)
30
+ instrument_log(message, :info)
31
+ result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
32
+ super(nil, payload: message, &block)
33
+ else
34
+ super
35
+ end
36
+ broadcasts.each do |logger|
37
+ next unless logger.respond_to?(:info)
38
+ message.is_a?(String) ? logger.info(message) : (logger.info(&block) if block)
39
+ end
40
+ result
41
+ end
42
+
43
+ sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
44
+ def warn(message = nil, payload = nil, &block)
45
+ instrument_log(message, :warn)
46
+ result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
47
+ super(nil, payload: message, &block)
48
+ else
49
+ super
50
+ end
51
+ broadcasts.each do |logger|
52
+ next unless logger.respond_to?(:warn)
53
+ message.is_a?(String) ? logger.warn(message) : (logger.warn(&block) if block)
54
+ end
55
+ result
56
+ end
57
+
58
+ sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
59
+ def error(message = nil, payload = nil, &block)
60
+ instrument_log(message, :error)
61
+ result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
62
+ super(nil, payload: message, &block)
63
+ else
64
+ super
65
+ end
66
+ broadcasts.each do |logger|
67
+ next unless logger.respond_to?(:error)
68
+ message.is_a?(String) ? logger.error(message) : (logger.error(&block) if block)
69
+ end
70
+ result
71
+ end
72
+
73
+ sig { params(message: T.untyped, payload: T.untyped, block: T.nilable(T.proc.returns(String))).returns(T::Boolean) }
74
+ def fatal(message = nil, payload = nil, &block)
75
+ instrument_log(message, :fatal)
76
+ result = if message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct) || message.is_a?(Hash)
77
+ super(nil, payload: message, &block)
78
+ else
79
+ super
80
+ end
81
+ broadcasts.each do |logger|
82
+ next unless logger.respond_to?(:fatal)
83
+ message.is_a?(String) ? logger.fatal(message) : (logger.fatal(&block) if block)
84
+ end
85
+ result
86
+ end
87
+
88
+ private
89
+
90
+ # Instrument log events for subscribers
91
+ sig { params(message: T.untyped, level: Symbol).void }
92
+ def instrument_log(message, level)
93
+ return unless message.is_a?(LogStruct::Log::Interfaces::CommonFields) || message.is_a?(T::Struct)
94
+
95
+ ::ActiveSupport::Notifications.instrument("log.logstruct", log: message, level: level)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "semantic_logger"
5
+ require_relative "concerns/log_methods"
5
6
 
6
7
  module LogStruct
7
8
  module SemanticLogger
@@ -69,25 +70,37 @@ module LogStruct
69
70
  def initialize(name = "Application", level: nil, filter: nil)
70
71
  # SemanticLogger::Logger expects positional arguments, not named arguments
71
72
  super(name, level, filter)
73
+ # T.untyped because users can pass any logger: ::Logger, ActiveSupport::Logger,
74
+ # custom loggers (FakeLogger in tests), or third-party loggers
75
+ @broadcasts = T.let([], T::Array[T.untyped])
76
+ # ActiveJob expects logger.formatter to exist and respond to current_tags
77
+ @formatter = T.let(FormatterProxy.new, FormatterProxy)
72
78
  end
73
79
 
74
- # Override log methods to handle LogStruct types
75
- %i[debug info warn error fatal].each do |level|
76
- define_method(level) do |message = nil, payload = nil, &block|
77
- # If message is a LogStruct type, use it as payload
78
- if message.is_a?(LogStruct::Log::Interfaces::CommonFields) ||
79
- message.is_a?(T::Struct) ||
80
- message.is_a?(Hash)
81
- payload = message
82
- message = nil
83
- super(message, payload: payload, &block)
84
- else
85
- # For plain string messages, pass them through normally
86
- super(message, payload, &block)
87
- end
88
- end
80
+ # ActiveSupport::BroadcastLogger compatibility
81
+ # These methods allow Rails.logger to broadcast to multiple loggers
82
+ sig { returns(T::Array[T.untyped]) }
83
+ attr_reader :broadcasts
84
+
85
+ # ActiveJob compatibility - expects logger.formatter.current_tags
86
+ sig { returns(FormatterProxy) }
87
+ attr_reader :formatter
88
+
89
+ # T.untyped for logger param because we accept any logger-like object:
90
+ # ::Logger, ActiveSupport::Logger, test doubles, etc.
91
+ sig { params(logger: T.untyped).returns(T.untyped) }
92
+ def broadcast_to(logger)
93
+ @broadcasts << logger
94
+ logger
95
+ end
96
+
97
+ sig { params(logger: T.untyped).void }
98
+ def stop_broadcasting_to(logger)
99
+ @broadcasts.delete(logger)
89
100
  end
90
101
 
102
+ include Concerns::LogMethods
103
+
91
104
  # Support for tagged logging
92
105
  sig { params(tags: T.untyped, block: T.proc.returns(T.untyped)).returns(T.untyped) }
93
106
  def tagged(*tags, &block)
@@ -124,6 +137,24 @@ module LogStruct
124
137
  def pop_tags(count = 1)
125
138
  ::SemanticLogger.pop_tags(count)
126
139
  end
140
+
141
+ # Support for << operator (used by RailsLogSplitter)
142
+ sig { params(msg: String).returns(T.self_type) }
143
+ def <<(msg)
144
+ info(msg)
145
+ @broadcasts.each { |logger| logger << msg if logger.respond_to?(:<<) }
146
+ self
147
+ end
148
+ end
149
+
150
+ # Proxy object to provide ActiveJob-compatible formatter interface
151
+ class FormatterProxy
152
+ extend T::Sig
153
+
154
+ sig { returns(T::Array[T.any(String, Symbol)]) }
155
+ def current_tags
156
+ Thread.current[:activesupport_tagged_logging_tags] || []
157
+ end
127
158
  end
128
159
  end
129
160
  end
@@ -152,8 +152,8 @@ module LogStruct
152
152
  )
153
153
  end
154
154
 
155
- # Add file appender if configured and not already logging to STDOUT/StringIO
156
- if app.config.paths["log"].first && io != $stdout && !io.is_a?(StringIO)
155
+ # Add file appender if Rails has a log path configured (normal Rails behavior)
156
+ if app.config.paths["log"].first
157
157
  ::SemanticLogger.add_appender(
158
158
  file_name: app.config.paths["log"].first,
159
159
  formatter: LogStruct::SemanticLogger::Formatter.new,
@@ -164,14 +164,14 @@ module LogStruct
164
164
 
165
165
  sig { params(app: T.untyped).returns(T.untyped) }
166
166
  def self.determine_output(app)
167
- # Always honor explicit STDOUT directive, even in test, or when LogStruct is enabled via env
168
- return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present? || ENV["LOGSTRUCT_ENABLED"].to_s.strip.downcase == "true"
167
+ # Always honor explicit STDOUT directive
168
+ return $stdout if ENV["RAILS_LOG_TO_STDOUT"].present?
169
169
 
170
170
  if Rails.env.test?
171
- # Default to StringIO to keep test output clean unless STDOUT is requested
171
+ # Use StringIO in test to keep stdout clean
172
172
  StringIO.new
173
173
  else
174
- # Default to STDOUT for app logs
174
+ # Use STDOUT for app logs in dev/production
175
175
  $stdout
176
176
  end
177
177
  end
@@ -222,7 +222,11 @@ module LogStruct
222
222
  # Also replace various component loggers
223
223
  ActiveRecord::Base.logger = logger if defined?(ActiveRecord::Base)
224
224
  ActionController::Base.logger = logger if defined?(ActionController::Base)
225
- ActionMailer::Base.logger = logger if defined?(ActionMailer::Base)
225
+ if defined?(ActionMailer::Base)
226
+ ActionMailer::Base.logger = logger
227
+ # Ensure ActionMailer.logger is also set (it might be accessed directly)
228
+ T.unsafe(::ActionMailer).logger = logger if T.unsafe(::ActionMailer).respond_to?(:logger=)
229
+ end
226
230
  ActiveJob::Base.logger = logger if defined?(ActiveJob::Base)
227
231
  ActionView::Base.logger = logger if defined?(ActionView::Base)
228
232
  ActionCable.server.config.logger = logger if defined?(ActionCable)
@@ -1,8 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../../enums/log_field"
5
- require_relative "../interfaces/request_fields"
4
+ require_relative "../enums/log_field"
5
+ require_relative "interfaces/request_fields"
6
6
 
7
7
  module LogStruct
8
8
  module Log
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../interfaces/additional_data_field"
4
+ require_relative "interfaces/additional_data_field"
5
5
 
6
6
  module LogStruct
7
7
  module Log
@@ -1,9 +1,9 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../../enums/log_field"
5
- require_relative "../interfaces/common_fields"
6
- require_relative "../../log/shared/merge_additional_data_fields"
4
+ require_relative "../enums/log_field"
5
+ require_relative "interfaces/common_fields"
6
+ require_relative "merge_additional_data_fields"
7
7
 
8
8
  module LogStruct
9
9
  module Log
@@ -25,6 +25,12 @@ module LogStruct
25
25
  field_hash.each do |log_field, value|
26
26
  next if value.nil?
27
27
  key = log_field.serialize
28
+
29
+ # Limit backtrace to first 5 lines
30
+ if key == :backtrace && value.is_a?(Array)
31
+ value = value.first(5)
32
+ end
33
+
28
34
  out[key] = value.is_a?(::Time) ? value.iso8601 : value
29
35
  end
30
36
 
@@ -1,8 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../../enums/log_field"
5
- require_relative "../interfaces/public_common_fields"
4
+ require_relative "../enums/log_field"
5
+ require_relative "interfaces/public_common_fields"
6
6
 
7
7
  module LogStruct
8
8
  module Log
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module LogStruct
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.3"
6
6
  end
data/lib/log_struct.rb CHANGED
@@ -22,6 +22,9 @@ require "log_struct/semantic_logger/logger"
22
22
  require "log_struct/semantic_logger/setup"
23
23
  require "log_struct/rails_boot_banner_silencer"
24
24
 
25
+ # Monkey patches for Rails compatibility
26
+ require "log_struct/monkey_patches/active_support/tagged_logging/formatter"
27
+
25
28
  module LogStruct
26
29
  extend T::Sig
27
30
 
@@ -44,7 +47,7 @@ module LogStruct
44
47
  end
45
48
 
46
49
  # Set enabled at require time based on current Rails environment.
47
- # (Users can disable or enable LogStruct later in an initializer.)
50
+ # (Users can override this in their initializer which runs before the Railtie checks enabled)
48
51
  set_enabled_from_rails_env!
49
52
 
50
53
  # Silence Rails boot banners for cleaner server output