logstruct 0.1.7 → 0.1.8
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 +6 -0
- data/README.md +1 -0
- data/lib/log_struct/concerns/configuration.rb +6 -2
- data/lib/log_struct/enums/log_field.rb +3 -0
- data/lib/log_struct/integrations/lograge.rb +86 -23
- data/lib/log_struct/integrations/shrine.rb +71 -8
- data/lib/log_struct/log/request.rb +11 -0
- data/lib/log_struct/monkey_patches/active_support/tagged_logging/formatter.rb +29 -22
- data/lib/log_struct/semantic_logger/formatter.rb +48 -10
- data/lib/log_struct/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3634f56776f895f97747c8165065651aca55098520469624f73f2d75b090f33c
|
|
4
|
+
data.tar.gz: c29661ad9e8044bad356a5a699f9aaed053c55ec124226784c60dd78655e794f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c491c3087d5341e5ab88bc607c2596be06fb90c482b74498b5aebd5a0eb5ef92f917b889b3ea26f56cb5a69be756bbc0afabfb957bff557cbbd7615f45b3934b
|
|
7
|
+
data.tar.gz: '090f85673e396225d96254c14fdfcdc941904354d3c11700be60b93e83e7c2e230bcbc15c8436ba2e0491223c77e0498f33d866beddb1e34379b7e4863916d1b'
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
### Changed
|
|
9
9
|
|
|
10
|
+
## [0.1.8] - 2026-01-22
|
|
11
|
+
|
|
12
|
+
- **Fix**: Lograge custom options now appear in request logs
|
|
13
|
+
- **Fix**: Request logs include request metadata fields (request_id, source_ip, user_agent, referer, host, content_type, accept)
|
|
14
|
+
- **Docs**: Documented Lograge custom options and request metadata fields
|
|
15
|
+
|
|
10
16
|
## [0.1.7] - 2025-12-06
|
|
11
17
|
|
|
12
18
|
- **Fix**: Puma server detection now uses `$PROGRAM_NAME` instead of checking `defined?(::Puma::Server)` which was unreliable
|
data/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Once initialized (and enabled), the gem automatically includes its modules into
|
|
|
52
52
|
- `ActiveSupport::TaggedLogging` is patched to support both Hashes and Strings (only when LogStruct is enabled)
|
|
53
53
|
- `ActionMailer::Base` includes error handling and event logging modules
|
|
54
54
|
- We configure `Lograge` for request logging
|
|
55
|
+
- Lograge request logs include request metadata (request_id, source_ip, user_agent, referer, host, content_type, accept) and custom fields from `lograge_custom_options`
|
|
55
56
|
- A Rack middleware is inserted to catch and log errors, including security violations (IP spoofing, CSRF, blocked hosts, etc.)
|
|
56
57
|
- Structured logging is set up for ActiveJob, Sidekiq, Shrine, etc.
|
|
57
58
|
- Rails `config.filter_parameters` are merged into LogStruct's filters and then cleared (to avoid double filtering). Configure sensitive keys via `LogStruct.config.filters`.
|
|
@@ -145,8 +145,12 @@ module LogStruct
|
|
|
145
145
|
sig { returns(T::Boolean) }
|
|
146
146
|
def puma_server?
|
|
147
147
|
# Just checking defined?(::Puma::Server) is not reliable - Puma might be installed
|
|
148
|
-
# but not running. Check $PROGRAM_NAME to verify we're actually running puma.
|
|
149
|
-
|
|
148
|
+
# but not running. Check $PROGRAM_NAME and ARGV to verify we're actually running puma.
|
|
149
|
+
# ARGV check is needed when running through wrapper scripts like gosu.
|
|
150
|
+
return true if $PROGRAM_NAME.include?("puma")
|
|
151
|
+
return true if current_argv.any? { |arg| arg.include?("puma") }
|
|
152
|
+
|
|
153
|
+
false
|
|
150
154
|
end
|
|
151
155
|
|
|
152
156
|
sig { returns(T::Boolean) }
|
|
@@ -13,6 +13,29 @@ module LogStruct
|
|
|
13
13
|
module Lograge
|
|
14
14
|
extend IntegrationInterface
|
|
15
15
|
|
|
16
|
+
LOGRAGE_KNOWN_KEYS = T.let(
|
|
17
|
+
[
|
|
18
|
+
:method,
|
|
19
|
+
:path,
|
|
20
|
+
:format,
|
|
21
|
+
:controller,
|
|
22
|
+
:action,
|
|
23
|
+
:status,
|
|
24
|
+
:duration,
|
|
25
|
+
:view,
|
|
26
|
+
:db,
|
|
27
|
+
:params,
|
|
28
|
+
:request_id,
|
|
29
|
+
:source_ip,
|
|
30
|
+
:user_agent,
|
|
31
|
+
:referer,
|
|
32
|
+
:host,
|
|
33
|
+
:content_type,
|
|
34
|
+
:accept
|
|
35
|
+
].freeze,
|
|
36
|
+
T::Array[Symbol]
|
|
37
|
+
)
|
|
38
|
+
|
|
16
39
|
class << self
|
|
17
40
|
extend T::Sig
|
|
18
41
|
|
|
@@ -38,30 +61,9 @@ module LogStruct
|
|
|
38
61
|
# The struct is converted to JSON by our Formatter (after filtering, etc.)
|
|
39
62
|
config.lograge.formatter = T.let(
|
|
40
63
|
lambda do |data|
|
|
41
|
-
|
|
42
|
-
status = ((s = data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
|
|
43
|
-
duration_ms = ((d = data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
|
|
44
|
-
view = ((v = data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
|
|
45
|
-
db = ((b = data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
|
|
46
|
-
|
|
47
|
-
params = data[:params]
|
|
48
|
-
params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
|
|
49
|
-
|
|
50
|
-
Log::Request.new(
|
|
51
|
-
http_method: data[:method]&.to_s,
|
|
52
|
-
path: data[:path]&.to_s,
|
|
53
|
-
format: data[:format]&.to_sym,
|
|
54
|
-
controller: data[:controller]&.to_s,
|
|
55
|
-
action: data[:action]&.to_s,
|
|
56
|
-
status: status,
|
|
57
|
-
duration_ms: duration_ms,
|
|
58
|
-
view: view,
|
|
59
|
-
database: db,
|
|
60
|
-
params: params,
|
|
61
|
-
timestamp: Time.now
|
|
62
|
-
)
|
|
64
|
+
LogStruct::Integrations::Lograge.build_request_log(data)
|
|
63
65
|
end,
|
|
64
|
-
T.proc.params(hash: T::Hash[Symbol, T.untyped]).returns(Log::Request)
|
|
66
|
+
T.proc.params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request)
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
# Add custom options to lograge
|
|
@@ -100,6 +102,7 @@ module LogStruct
|
|
|
100
102
|
return if headers.blank?
|
|
101
103
|
|
|
102
104
|
options[:user_agent] = headers["HTTP_USER_AGENT"]
|
|
105
|
+
options[:referer] = headers["HTTP_REFERER"]
|
|
103
106
|
options[:content_type] = headers["CONTENT_TYPE"]
|
|
104
107
|
options[:accept] = headers["HTTP_ACCEPT"]
|
|
105
108
|
end
|
|
@@ -114,6 +117,66 @@ module LogStruct
|
|
|
114
117
|
# The proc can modify the options hash directly
|
|
115
118
|
custom_options_proc.call(event, options)
|
|
116
119
|
end
|
|
120
|
+
|
|
121
|
+
sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request) }
|
|
122
|
+
def build_request_log(data)
|
|
123
|
+
normalized_data = normalize_lograge_data(data)
|
|
124
|
+
|
|
125
|
+
# Coerce common fields to expected types
|
|
126
|
+
status = ((s = normalized_data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
|
|
127
|
+
duration_ms = ((d = normalized_data[:duration]) && d.respond_to?(:to_f)) ? d.to_f : d
|
|
128
|
+
view = ((v = normalized_data[:view]) && v.respond_to?(:to_f)) ? v.to_f : v
|
|
129
|
+
db = ((b = normalized_data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
|
|
130
|
+
|
|
131
|
+
params = normalized_data[:params]
|
|
132
|
+
params = params.deep_symbolize_keys if params&.respond_to?(:deep_symbolize_keys)
|
|
133
|
+
|
|
134
|
+
additional_data = extract_additional_data(normalized_data)
|
|
135
|
+
|
|
136
|
+
Log::Request.new(
|
|
137
|
+
http_method: normalized_data[:method]&.to_s,
|
|
138
|
+
path: normalized_data[:path]&.to_s,
|
|
139
|
+
format: normalized_data[:format]&.to_sym,
|
|
140
|
+
controller: normalized_data[:controller]&.to_s,
|
|
141
|
+
action: normalized_data[:action]&.to_s,
|
|
142
|
+
status: status,
|
|
143
|
+
duration_ms: duration_ms,
|
|
144
|
+
view: view,
|
|
145
|
+
database: db,
|
|
146
|
+
params: params,
|
|
147
|
+
request_id: normalized_data[:request_id]&.to_s,
|
|
148
|
+
source_ip: normalized_data[:source_ip]&.to_s,
|
|
149
|
+
user_agent: normalized_data[:user_agent]&.to_s,
|
|
150
|
+
referer: normalized_data[:referer]&.to_s,
|
|
151
|
+
host: normalized_data[:host]&.to_s,
|
|
152
|
+
content_type: normalized_data[:content_type]&.to_s,
|
|
153
|
+
accept: normalized_data[:accept]&.to_s,
|
|
154
|
+
additional_data: additional_data,
|
|
155
|
+
timestamp: Time.now
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
160
|
+
def normalize_lograge_data(data)
|
|
161
|
+
data.each_with_object({}) do |(key, value), normalized|
|
|
162
|
+
normalized[key.to_s.to_sym] = value
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
167
|
+
def extract_additional_data(data)
|
|
168
|
+
extras = T.let({}, T::Hash[Symbol, T.untyped])
|
|
169
|
+
data.each do |key, value|
|
|
170
|
+
next if LOGRAGE_KNOWN_KEYS.include?(key)
|
|
171
|
+
next if value.nil?
|
|
172
|
+
|
|
173
|
+
extras[key] = value
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
return nil if extras.empty?
|
|
177
|
+
|
|
178
|
+
extras
|
|
179
|
+
end
|
|
117
180
|
end
|
|
118
181
|
end
|
|
119
182
|
end
|
|
@@ -14,6 +14,8 @@ module LogStruct
|
|
|
14
14
|
extend T::Sig
|
|
15
15
|
extend IntegrationInterface
|
|
16
16
|
|
|
17
|
+
SHRINE_EVENTS = T.let(%i[upload exists download delete metadata open].freeze, T::Array[Symbol])
|
|
18
|
+
|
|
17
19
|
# Set up Shrine structured logging
|
|
18
20
|
sig { override.params(config: LogStruct::Configuration).returns(T.nilable(T::Boolean)) }
|
|
19
21
|
def self.setup(config)
|
|
@@ -30,9 +32,10 @@ module LogStruct
|
|
|
30
32
|
event_type = case event.name
|
|
31
33
|
when :upload then Event::Upload
|
|
32
34
|
when :download then Event::Download
|
|
35
|
+
when :open then Event::Download
|
|
33
36
|
when :delete then Event::Delete
|
|
34
37
|
when :metadata then Event::Metadata
|
|
35
|
-
when :exists then Event::Exist
|
|
38
|
+
when :exists then Event::Exist
|
|
36
39
|
else Event::Unknown
|
|
37
40
|
end
|
|
38
41
|
|
|
@@ -80,18 +83,78 @@ module LogStruct
|
|
|
80
83
|
Log::Shrine::Metadata.new(**unknown_params)
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
::Shrine.
|
|
86
|
+
# Log directly through SemanticLogger, NOT through Shrine.logger
|
|
87
|
+
# Shrine.logger is a basic Logger that would just call .to_s on the struct
|
|
88
|
+
::SemanticLogger[::Shrine].info(log_data)
|
|
86
89
|
end)
|
|
87
90
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
# Check if instrumentation plugin is already loaded
|
|
92
|
+
# If so, we need to replace the existing subscribers, not add duplicates
|
|
93
|
+
if instrumentation_already_configured?
|
|
94
|
+
replace_existing_subscribers(shrine_log_subscriber)
|
|
95
|
+
else
|
|
96
|
+
# First time setup - configure the instrumentation plugin
|
|
97
|
+
::Shrine.plugin :instrumentation,
|
|
98
|
+
events: SHRINE_EVENTS,
|
|
99
|
+
log_subscriber: shrine_log_subscriber
|
|
100
|
+
end
|
|
92
101
|
|
|
93
102
|
true
|
|
94
103
|
end
|
|
104
|
+
|
|
105
|
+
sig { returns(T::Boolean) }
|
|
106
|
+
def self.instrumentation_already_configured?
|
|
107
|
+
return false unless defined?(::Shrine)
|
|
108
|
+
|
|
109
|
+
opts = T.unsafe(::Shrine).opts
|
|
110
|
+
return false unless opts.is_a?(Hash)
|
|
111
|
+
|
|
112
|
+
instrumentation_opts = opts[:instrumentation]
|
|
113
|
+
return false unless instrumentation_opts.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
subscribers = instrumentation_opts[:subscribers]
|
|
116
|
+
return false unless subscribers.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
!subscribers.empty?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig { params(new_subscriber: T.untyped).void }
|
|
122
|
+
def self.replace_existing_subscribers(new_subscriber)
|
|
123
|
+
opts = T.unsafe(::Shrine).opts
|
|
124
|
+
instrumentation_opts = opts[:instrumentation]
|
|
125
|
+
subscribers = instrumentation_opts[:subscribers]
|
|
126
|
+
|
|
127
|
+
# Clear all existing subscribers and add our new one
|
|
128
|
+
SHRINE_EVENTS.each do |event_name|
|
|
129
|
+
# Clear existing subscribers for this event
|
|
130
|
+
subscribers[event_name] = [] if subscribers[event_name]
|
|
131
|
+
|
|
132
|
+
# Add our subscriber
|
|
133
|
+
subscribers[event_name] ||= []
|
|
134
|
+
subscribers[event_name] << new_subscriber
|
|
135
|
+
|
|
136
|
+
# Also re-subscribe via ActiveSupport::Notifications
|
|
137
|
+
# Shrine uses "shrine.#{event_name}" as the notification name
|
|
138
|
+
notification_name = "shrine.#{event_name}"
|
|
139
|
+
|
|
140
|
+
# Unsubscribe existing listeners for this event
|
|
141
|
+
# ActiveSupport::Notifications stores subscriptions, we need to find and remove them
|
|
142
|
+
notifier = ::ActiveSupport::Notifications.notifier
|
|
143
|
+
if notifier.respond_to?(:listeners_for)
|
|
144
|
+
# Rails 7.0+ uses listeners_for
|
|
145
|
+
listeners = notifier.listeners_for(notification_name)
|
|
146
|
+
listeners.each do |listener|
|
|
147
|
+
::ActiveSupport::Notifications.unsubscribe(listener)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Subscribe our new subscriber
|
|
152
|
+
::ActiveSupport::Notifications.subscribe(notification_name) do |*args|
|
|
153
|
+
event = ::ActiveSupport::Notifications::Event.new(*args)
|
|
154
|
+
new_subscriber.call(event)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
95
158
|
end
|
|
96
159
|
end
|
|
97
160
|
end
|
|
@@ -44,6 +44,14 @@ module LogStruct
|
|
|
44
44
|
const :view, T.nilable(Float), default: nil
|
|
45
45
|
const :database, T.nilable(Float), default: nil
|
|
46
46
|
const :params, T.nilable(T::Hash[Symbol, T.untyped]), default: nil
|
|
47
|
+
const :host, T.nilable(String), default: nil
|
|
48
|
+
const :content_type, T.nilable(String), default: nil
|
|
49
|
+
const :accept, T.nilable(String), default: nil
|
|
50
|
+
|
|
51
|
+
# Additional data
|
|
52
|
+
include LogStruct::Log::Interfaces::AdditionalDataField
|
|
53
|
+
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
54
|
+
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
47
55
|
|
|
48
56
|
# Request fields (optional)
|
|
49
57
|
include LogStruct::Log::Interfaces::RequestFields
|
|
@@ -70,6 +78,9 @@ module LogStruct
|
|
|
70
78
|
h[LogField::View] = view unless view.nil?
|
|
71
79
|
h[LogField::Database] = database unless database.nil?
|
|
72
80
|
h[LogField::Params] = params unless params.nil?
|
|
81
|
+
h[LogField::Host] = host unless host.nil?
|
|
82
|
+
h[LogField::ContentType] = content_type unless content_type.nil?
|
|
83
|
+
h[LogField::Accept] = accept unless accept.nil?
|
|
73
84
|
h
|
|
74
85
|
end
|
|
75
86
|
end
|
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
|
|
4
4
|
require "active_support/tagged_logging"
|
|
5
5
|
|
|
6
|
-
# Monkey-patch ActiveSupport::TaggedLogging::Formatter to
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# Monkey-patch ActiveSupport::TaggedLogging::Formatter to work with structured logging.
|
|
7
|
+
#
|
|
8
|
+
# Problem: Rails' TaggedLogging prepends tags as text and converts messages to strings.
|
|
9
|
+
# When we pass a hash to super(), Rails does "#{tags_text}#{msg}" which calls .to_s
|
|
10
|
+
# on the hash, producing Ruby inspect format {message: "..."} instead of valid JSON.
|
|
11
|
+
#
|
|
12
|
+
# Solution: When LogStruct is enabled, we add tags as a hash entry and delegate to
|
|
13
|
+
# LogStruct::Formatter for proper JSON serialization with filtering and standard fields.
|
|
9
14
|
module ActiveSupport
|
|
10
15
|
module TaggedLogging
|
|
11
16
|
extend T::Sig
|
|
12
17
|
|
|
13
18
|
# 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
19
|
sig { returns(T::Array[T.any(String, Symbol)]) }
|
|
17
20
|
def self.current_tags
|
|
18
21
|
Thread.current[:activesupport_tagged_logging_tags] || []
|
|
@@ -23,29 +26,33 @@ module ActiveSupport
|
|
|
23
26
|
extend T::Helpers
|
|
24
27
|
requires_ancestor { ::ActiveSupport::TaggedLogging::Formatter }
|
|
25
28
|
|
|
26
|
-
# Override the call method to support hash input/output, and wrap
|
|
27
|
-
# plain strings in a Hash under a `msg` key.
|
|
28
|
-
# The data is then passed to our custom log formatter that transforms it
|
|
29
|
-
# into a JSON string before logging.
|
|
30
|
-
#
|
|
31
|
-
# IMPORTANT: This only applies when LogStruct is enabled. When disabled,
|
|
32
|
-
# we preserve the original Rails logging behavior to avoid wrapping
|
|
33
|
-
# messages in hashes (which would break default Rails log formatting).
|
|
34
29
|
sig { params(severity: T.any(String, Symbol), time: Time, progname: T.untyped, data: T.untyped).returns(String) }
|
|
35
30
|
def call(severity, time, progname, data)
|
|
36
|
-
#
|
|
31
|
+
# Preserve original Rails behavior when LogStruct is disabled
|
|
37
32
|
return super unless ::LogStruct.enabled?
|
|
38
33
|
|
|
39
|
-
#
|
|
40
|
-
data = {message: data.to_s} unless data.is_a?(Hash)
|
|
41
|
-
|
|
42
|
-
# Add current tags to the hash if present
|
|
43
|
-
# Use thread-local storage directly as fallback if current_tags method doesn't exist
|
|
34
|
+
# Get current tags
|
|
44
35
|
tags = T.unsafe(self).respond_to?(:current_tags) ? current_tags : (Thread.current[:activesupport_tagged_logging_tags] || [])
|
|
45
|
-
data[:tags] = tags if tags.present?
|
|
46
36
|
|
|
47
|
-
#
|
|
48
|
-
|
|
37
|
+
# Add tags to data as hash entry (not text prefix)
|
|
38
|
+
data_with_tags = if data.is_a?(Hash)
|
|
39
|
+
tags.present? ? data.merge(tags: tags) : data
|
|
40
|
+
elsif data.is_a?(::LogStruct::Log::Interfaces::CommonFields) || (data.is_a?(T::Struct) && data.respond_to?(:serialize))
|
|
41
|
+
hash = T.unsafe(data).serialize
|
|
42
|
+
tags.present? ? hash.merge(tags: tags) : hash
|
|
43
|
+
else
|
|
44
|
+
tags.present? ? {message: data.to_s, tags: tags} : {message: data.to_s}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Delegate to LogStruct::Formatter for JSON serialization with filtering
|
|
48
|
+
logstruct_formatter.call(severity, time, progname, data_with_tags)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
sig { returns(::LogStruct::Formatter) }
|
|
54
|
+
def logstruct_formatter
|
|
55
|
+
@logstruct_formatter ||= T.let(::LogStruct::Formatter.new, T.nilable(::LogStruct::Formatter))
|
|
49
56
|
end
|
|
50
57
|
end
|
|
51
58
|
end
|
|
@@ -61,16 +61,12 @@ module LogStruct
|
|
|
61
61
|
|
|
62
62
|
sig { params(log: ::SemanticLogger::Log, logger: T.untyped).returns(String) }
|
|
63
63
|
def call(log, logger)
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@logstruct_formatter.call(log.level, log.time, log.name, log.payload)
|
|
71
|
-
elsif log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
|
|
72
|
-
# T::Struct wrapped in payload hash
|
|
73
|
-
@logstruct_formatter.call(log.level, log.time, log.name, log.payload[:payload])
|
|
64
|
+
# Extract LogStruct from various locations where it might be stored
|
|
65
|
+
logstruct = extract_logstruct(log)
|
|
66
|
+
|
|
67
|
+
json = if logstruct
|
|
68
|
+
# Use our formatter to process LogStruct types directly
|
|
69
|
+
@logstruct_formatter.call(log.level, log.time, log.name, logstruct)
|
|
74
70
|
elsif log.payload.is_a?(Hash) || log.payload.is_a?(T::Struct)
|
|
75
71
|
# Process hashes and T::Structs through our formatter
|
|
76
72
|
@logstruct_formatter.call(log.level, log.time, log.name, log.payload)
|
|
@@ -89,6 +85,48 @@ module LogStruct
|
|
|
89
85
|
|
|
90
86
|
private
|
|
91
87
|
|
|
88
|
+
# Extract a LogStruct from the various places it might be stored in a SemanticLogger::Log
|
|
89
|
+
sig do
|
|
90
|
+
params(log: ::SemanticLogger::Log).returns(
|
|
91
|
+
T.nilable(
|
|
92
|
+
T.any(
|
|
93
|
+
LogStruct::Log::Interfaces::CommonFields,
|
|
94
|
+
LogStruct::Log::Interfaces::PublicCommonFields,
|
|
95
|
+
T::Struct
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
def extract_logstruct(log)
|
|
101
|
+
# Check payload first (most common path for structured logging)
|
|
102
|
+
if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(LogStruct::Log::Interfaces::CommonFields)
|
|
103
|
+
return T.cast(log.payload[:payload], LogStruct::Log::Interfaces::CommonFields)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if log.payload.is_a?(LogStruct::Log::Interfaces::CommonFields)
|
|
107
|
+
return log.payload
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if log.payload.is_a?(LogStruct::Log::Interfaces::PublicCommonFields)
|
|
111
|
+
return log.payload
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check message - this is where structs end up when passed directly to logger.info(struct)
|
|
115
|
+
if log.message.is_a?(LogStruct::Log::Interfaces::CommonFields)
|
|
116
|
+
return T.cast(log.message, LogStruct::Log::Interfaces::CommonFields)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Check for T::Struct in payload hash (might be a LogStruct struct not implementing CommonFields directly)
|
|
120
|
+
if log.payload.is_a?(Hash) && log.payload[:payload].is_a?(T::Struct)
|
|
121
|
+
struct = log.payload[:payload]
|
|
122
|
+
if struct.respond_to?(:source) && struct.respond_to?(:event)
|
|
123
|
+
return struct
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
92
130
|
sig { returns(LogStruct::Formatter) }
|
|
93
131
|
attr_reader :logstruct_formatter
|
|
94
132
|
end
|
data/lib/log_struct/version.rb
CHANGED