logstruct 0.2.2 → 0.3.0
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 +20 -0
- data/lib/log_struct/bounded_input.rb +101 -0
- data/lib/log_struct/concerns/configuration.rb +7 -7
- data/lib/log_struct/concerns/error_handling.rb +10 -0
- data/lib/log_struct/config_struct/filters.rb +23 -9
- data/lib/log_struct/config_struct/limits.rb +1 -1
- data/lib/log_struct/enums/log_field.rb +0 -3
- data/lib/log_struct/formatter.rb +38 -23
- data/lib/log_struct/hash_utils.rb +11 -4
- data/lib/log_struct/integrations/ahoy.rb +3 -2
- data/lib/log_struct/integrations/carrierwave.rb +3 -6
- data/lib/log_struct/integrations/lograge.rb +4 -10
- data/lib/log_struct/log/action_mailer/delivered.rb +10 -0
- data/lib/log_struct/log/action_mailer/delivery.rb +10 -0
- data/lib/log_struct/log/action_mailer/error.rb +10 -0
- data/lib/log_struct/log/carrierwave/delete.rb +0 -4
- data/lib/log_struct/log/carrierwave/download.rb +0 -6
- data/lib/log_struct/log/carrierwave/upload.rb +0 -6
- data/lib/log_struct/log/carrierwave.rb +2 -8
- data/lib/log_struct/log/error.rb +10 -0
- data/lib/log_struct/log/plain.rb +10 -0
- data/lib/log_struct/log/puma/error.rb +10 -0
- data/lib/log_struct/log/puma/shutdown.rb +10 -0
- data/lib/log_struct/log/puma/start.rb +10 -0
- data/lib/log_struct/log/request.rb +10 -0
- data/lib/log_struct/log/security/blocked_host.rb +10 -0
- data/lib/log_struct/log/security/csrf_violation.rb +10 -0
- data/lib/log_struct/log/security/ip_spoof.rb +10 -0
- data/lib/log_struct/log/sidekiq/error.rb +10 -0
- data/lib/log_struct/log/sidekiq/log.rb +10 -0
- data/lib/log_struct/log/sql.rb +10 -0
- data/lib/log_struct/param_filters.rb +7 -26
- data/lib/log_struct/shared/merge_additional_data_fields.rb +21 -5
- data/lib/log_struct/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 647c97ba4368b67d39333204ca35a5161b0905dda2c8ef72d01e15d58665d367
|
|
4
|
+
data.tar.gz: e86e688d516ef584c5f8994ecb0739599c09595a021ebc949f494e6d631f349d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6709f7868f43c348891761749400bb93c762e3df6f2d7093dab12f51c50a2ce33e26bfd94dab009093a1c4207f746735764c56ce20768394a9b8e81cce5d346d
|
|
7
|
+
data.tar.gz: 7ab05d71a64c08cd4ad747c7684d3b75553e2740c0539cefa2cab2f906f2c5023dd8696bb78129790472896f83008addde117f9e41229d563ad9003a76911fd9
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Breaking**: CarrierWave lines log `file_id` as an HMAC of the file's identifier (keyed like email hashes, see `hash_salt`), and no longer log `filename`, `store_path` or `ext`. CarrierWave names stored files after the uploaded file by default, and an uploaded file's name often names a person. `LogField::Filename`, `LogField::StorePath` and `LogField::Extension` are removed.
|
|
15
|
+
- **Breaking**: A filtered Hash's summary has its `_keys_count` but not its first 10 keys (`_keys`): a Hash's keys can be secrets themselves, like tokens or email addresses used as keys.
|
|
16
|
+
- **Breaking**: `additional_data` can't set any field of its line's own type, whether or not the line has it (an error line's `msg`, `backtrace` or `error_message`), or `req_id`. Like `src`, `evt`, `lvl` and `ts`, such a key is left out and handled as a LogStruct error (raised in `local_environments`). It used to replace the field. Other types' field names, like `name` on a plain line, can still be used.
|
|
17
|
+
- **Filtering**: Without a `hash_salt`, values hashed before the Rails app has its `secret_key_base` use a random key for each process, instead of a fixed key that was public in LogStruct's source. Set `hash_salt` to keep these hashes the same across processes.
|
|
18
|
+
- **Development**: `task ci` checks the locked Ruby and docs dependencies for known advisories, with `bundler-audit` and `pnpm audit`.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Formatting**: Hash keys longer than `max_string_bytes` are left out with their values, and counted in `_oversized_keys`. A request's parameter names could make a line any size.
|
|
23
|
+
- **Formatting**: `ActionController::Parameters` are written like a Hash, within the size limits, instead of being converted whole first. Lograge's `params` and Ahoy's `properties` are converted at their first level only, and only as many keys as the line keeps; `_more_keys` and `_oversized_keys` count the rest.
|
|
24
|
+
- **Filtering**: A Rails filter block with three arguments gets a copy of the parameters that stops at 10,000 values, 20 levels of nesting or a value that contains itself. Past those, every key the block would check is filtered. The parameters used to be copied whole.
|
|
25
|
+
- **Errors**: An error raised while formatting, serializing or filtering a value is reported with its class and backtrace but not its message, which can quote the value. Errors in Rails filter blocks and LogStruct's filter matchers aren't reported with the key, which can be the secret. An object whose `as_json` raises is logged as `[SERIALIZATION_ERROR]` instead of its `to_s`, and its `inspect` isn't reported.
|
|
26
|
+
- **Formatting**: A Hash key that isn't a String or Symbol is written as its string, scrubbed, so an email address in its `to_s` is scrubbed.
|
|
27
|
+
- **Filtering**: A value longer than `max_string_bytes` is hashed by its first `max_string_bytes` bytes and its length.
|
|
28
|
+
- **Development**: Update Next.js to 16.4.1 for the docs site, which fixes three advisories in features the static site doesn't use, and the test-only Dalli to 3.2.13.
|
|
29
|
+
|
|
10
30
|
## [0.2.2] - 2026-10-21
|
|
11
31
|
|
|
12
32
|
### Changed
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "action_controller"
|
|
5
|
+
|
|
6
|
+
module LogStruct
|
|
7
|
+
# Copies of logged Hashes and Arrays that stop before an input too large to copy in full
|
|
8
|
+
module BoundedInput
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# A Rails filter block with three arguments gets every parameter, so a copy for it keeps what the formatter
|
|
12
|
+
# leaves out of the line. These limits only stop values no request sends: nesting deeper than the formatter
|
|
13
|
+
# writes, and more values than a form or JSON body has. A copy that reaches one is truncated, and the
|
|
14
|
+
# filters that read it fail closed.
|
|
15
|
+
MAX_DEPTH = 20
|
|
16
|
+
MAX_NODES = 10_000
|
|
17
|
+
|
|
18
|
+
# The keys that count what a Hash's copy left out. The formatter writes them as it writes its own counts:
|
|
19
|
+
# Rails' default filter_parameters include :_key, which would filter them.
|
|
20
|
+
COUNT_KEYS = T.let(Set[:_more_keys, :_oversized_keys].freeze, T::Set[Symbol])
|
|
21
|
+
|
|
22
|
+
class State < T::Struct
|
|
23
|
+
prop :remaining_nodes, Integer, default: MAX_NODES
|
|
24
|
+
prop :truncated, T::Boolean, default: false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig do
|
|
28
|
+
params(value: T.untyped, state: State, depth: Integer, ancestors: T::Hash[T.untyped, T::Boolean]).returns(T.untyped)
|
|
29
|
+
end
|
|
30
|
+
def self.copy(value, state:, depth: 0, ancestors: {}.compare_by_identity)
|
|
31
|
+
if depth > MAX_DEPTH || ancestors.key?(value) || state.remaining_nodes <= 0
|
|
32
|
+
state.truncated = true
|
|
33
|
+
return nil
|
|
34
|
+
end
|
|
35
|
+
state.remaining_nodes -= 1
|
|
36
|
+
|
|
37
|
+
case value
|
|
38
|
+
when Hash, ::ActionController::Parameters
|
|
39
|
+
ancestors[value] = true
|
|
40
|
+
begin
|
|
41
|
+
result = {}
|
|
42
|
+
value.each_pair do |key, child|
|
|
43
|
+
break if state.truncated
|
|
44
|
+
|
|
45
|
+
result[key] = copy(child, state: state, depth: depth + 1, ancestors: ancestors)
|
|
46
|
+
end
|
|
47
|
+
result
|
|
48
|
+
ensure
|
|
49
|
+
ancestors.delete(value)
|
|
50
|
+
end
|
|
51
|
+
when Array
|
|
52
|
+
ancestors[value] = true
|
|
53
|
+
begin
|
|
54
|
+
result = []
|
|
55
|
+
value.each do |child|
|
|
56
|
+
break if state.truncated
|
|
57
|
+
|
|
58
|
+
result << copy(child, state: state, depth: depth + 1, ancestors: ancestors)
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
ensure
|
|
62
|
+
ancestors.delete(value)
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# How many keys a Hash or ActionController::Parameters has, without converting its values
|
|
70
|
+
sig { params(hash: T.untyped).returns(Integer) }
|
|
71
|
+
def self.key_count(hash)
|
|
72
|
+
hash.is_a?(Hash) ? hash.size : hash.keys.size
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The first keys of a Hash or ActionController::Parameters as Symbols, for a log struct's Hash field, without
|
|
76
|
+
# converting the keys the formatter would leave out. The formatter writes max_hash_keys entries of a Hash,
|
|
77
|
+
# and the counts of the keys left out here are entries too: the last key's slot goes to _more_keys, and an
|
|
78
|
+
# omitted oversized key's slot to _oversized_keys, so the formatter writes both counts as they are.
|
|
79
|
+
sig { params(hash: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
|
80
|
+
def self.symbolize_keys(hash)
|
|
81
|
+
limits = LogStruct.config.limits
|
|
82
|
+
total = key_count(hash)
|
|
83
|
+
examined = (total > limits.max_hash_keys) ? [limits.max_hash_keys - 1, 0].max : total
|
|
84
|
+
result = T.let({}, T::Hash[Symbol, T.untyped])
|
|
85
|
+
oversized_keys = 0
|
|
86
|
+
hash.each_pair.with_index do |(key, value), index|
|
|
87
|
+
break if index >= examined
|
|
88
|
+
|
|
89
|
+
if key.to_s.bytesize > limits.max_string_bytes
|
|
90
|
+
oversized_keys += 1
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result[key.to_s.to_sym] = value
|
|
95
|
+
end
|
|
96
|
+
result[:_more_keys] = total - examined if examined < total
|
|
97
|
+
result[:_oversized_keys] = oversized_keys if oversized_keys.positive?
|
|
98
|
+
result
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -259,7 +259,7 @@ module LogStruct
|
|
|
259
259
|
result = filter.call(key, value, root)
|
|
260
260
|
!!result || (value.is_a?(String) && value != original_value)
|
|
261
261
|
rescue => e
|
|
262
|
-
handle_filter_error(e, filter
|
|
262
|
+
handle_filter_error(e, filter)
|
|
263
263
|
true
|
|
264
264
|
end
|
|
265
265
|
ConfigStruct::PathFilterMatcher.new(callable: callable, label: filter.inspect)
|
|
@@ -290,11 +290,11 @@ module LogStruct
|
|
|
290
290
|
begin
|
|
291
291
|
!!filter.call(key)
|
|
292
292
|
rescue => e
|
|
293
|
-
handle_filter_error(e, filter
|
|
293
|
+
handle_filter_error(e, filter)
|
|
294
294
|
true
|
|
295
295
|
end
|
|
296
296
|
rescue => e
|
|
297
|
-
handle_filter_error(e, filter
|
|
297
|
+
handle_filter_error(e, filter)
|
|
298
298
|
true
|
|
299
299
|
end
|
|
300
300
|
ConfigStruct::FilterMatcher.new(callable: callable, label: filter.inspect)
|
|
@@ -305,11 +305,11 @@ module LogStruct
|
|
|
305
305
|
filter.respond_to?(:arity) ? filter.arity : 2
|
|
306
306
|
end
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
# The key isn't reported: a key can be the secret (an email address or a token used as a key)
|
|
309
|
+
sig { params(error: StandardError, filter: T.untyped).void }
|
|
310
|
+
def handle_filter_error(error, filter)
|
|
310
311
|
context = {
|
|
311
312
|
filter: filter.class.name,
|
|
312
|
-
key: key,
|
|
313
313
|
filter_label: begin
|
|
314
314
|
filter.inspect
|
|
315
315
|
rescue
|
|
@@ -317,7 +317,7 @@ module LogStruct
|
|
|
317
317
|
end
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
LogStruct.
|
|
320
|
+
LogStruct.handle_value_error(error, "filter_parameters entry failed", context: context)
|
|
321
321
|
end
|
|
322
322
|
end
|
|
323
323
|
end
|
|
@@ -89,6 +89,16 @@ module LogStruct
|
|
|
89
89
|
Thread.current[:logstruct_formatting_internal_error] = previous_internal_error
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# Handle an error raised while LogStruct read a value it was logging, without the error's message: the
|
|
93
|
+
# message can quote that value, as a NoMethodError quotes its receiver's inspect before Ruby 3.4.
|
|
94
|
+
# failure says what failed, and the reported error keeps the original's class and backtrace.
|
|
95
|
+
sig { params(error: Exception, failure: String, context: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
|
96
|
+
def handle_value_error(error, failure, context: nil)
|
|
97
|
+
reported = RuntimeError.new("#{error.class}: #{failure}")
|
|
98
|
+
reported.set_backtrace(error.backtrace)
|
|
99
|
+
handle_exception(reported, source: Source::Internal, context: context)
|
|
100
|
+
end
|
|
101
|
+
|
|
92
102
|
# Handle an error according to the configured error handling mode (log, report, raise, etc).
|
|
93
103
|
# Pass `log: false` when the caller has already logged the error in its own structured line.
|
|
94
104
|
sig { params(error: StandardError, source: Source, context: T.nilable(T::Hash[Symbol, T.untyped]), log: T::Boolean).void }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "active_support/hash_with_indifferent_access"
|
|
5
|
+
require_relative "../bounded_input"
|
|
5
6
|
|
|
6
7
|
module LogStruct
|
|
7
8
|
# A key's words, lowercase and joined by underscores, so that keys match however they're written:
|
|
@@ -38,8 +39,8 @@ module LogStruct
|
|
|
38
39
|
# A filter that needs more than the key: Rails' entries with a dot (`card.number`) match a key by its
|
|
39
40
|
# path, and Rails gives filter blocks with three arguments the parameters around the key. The path
|
|
40
41
|
# starts at the line's top-level field (`["params", "card", "number"]`); `root` is that field's value,
|
|
41
|
-
# or the whole line for a top-level key, with indifferent access as Rails gives it.
|
|
42
|
-
#
|
|
42
|
+
# or the whole line for a top-level key, with indifferent access as Rails gives it. A matcher
|
|
43
|
+
# that doesn't read the root sets uses_root to false and gets nil.
|
|
43
44
|
class PathFilterMatcher < T::Struct
|
|
44
45
|
extend T::Sig
|
|
45
46
|
|
|
@@ -50,7 +51,10 @@ module LogStruct
|
|
|
50
51
|
sig { params(path: T::Array[String], value: T.untyped, root: T.untyped).returns(T::Boolean) }
|
|
51
52
|
def matches?(path, value, root)
|
|
52
53
|
root = nil unless uses_root
|
|
53
|
-
|
|
54
|
+
root = root.value if root.is_a?(LazyRoot)
|
|
55
|
+
return true if root.equal?(LazyRoot::UNAVAILABLE)
|
|
56
|
+
|
|
57
|
+
callable.call(path, value, root)
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
@@ -58,6 +62,8 @@ module LogStruct
|
|
|
58
62
|
class LazyRoot
|
|
59
63
|
extend T::Sig
|
|
60
64
|
|
|
65
|
+
UNAVAILABLE = T.let(Object.new.freeze, Object)
|
|
66
|
+
|
|
61
67
|
sig { params(value: T.untyped).void }
|
|
62
68
|
def initialize(value)
|
|
63
69
|
@source = value
|
|
@@ -70,12 +76,20 @@ module LogStruct
|
|
|
70
76
|
def value
|
|
71
77
|
return @value if @built
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
state = BoundedInput::State.new
|
|
80
|
+
bounded = BoundedInput.copy(@source, state: state)
|
|
81
|
+
@value = if state.truncated
|
|
82
|
+
UNAVAILABLE
|
|
83
|
+
elsif bounded.is_a?(Hash)
|
|
84
|
+
ActiveSupport::HashWithIndifferentAccess.new(bounded)
|
|
85
|
+
else
|
|
86
|
+
bounded
|
|
78
87
|
end
|
|
88
|
+
@built = true
|
|
89
|
+
@value
|
|
90
|
+
rescue SystemStackError, StandardError
|
|
91
|
+
@built = true
|
|
92
|
+
@value = UNAVAILABLE
|
|
79
93
|
end
|
|
80
94
|
end
|
|
81
95
|
|
|
@@ -86,7 +100,7 @@ module LogStruct
|
|
|
86
100
|
# A filtered value is replaced by a summary of it, with no length (which would say how long a
|
|
87
101
|
# secret is):
|
|
88
102
|
#
|
|
89
|
-
# { _filtered: { _class: "Hash", _keys_count: 3
|
|
103
|
+
# { _filtered: { _class: "Hash", _keys_count: 3 } }
|
|
90
104
|
# { _filtered: { _class: "Array", _count: 2 } }
|
|
91
105
|
# { _filtered: { _class: "String" } } # plus its _hash for filter_keys_with_hashes
|
|
92
106
|
#
|
|
@@ -21,7 +21,7 @@ module LogStruct
|
|
|
21
21
|
# Default: 20
|
|
22
22
|
prop :max_backtrace_lines, Integer, default: 20
|
|
23
23
|
|
|
24
|
-
# Strings longer than this many bytes are cut
|
|
24
|
+
# Strings longer than this many bytes are cut; oversized Hash keys and their values are omitted
|
|
25
25
|
# Default: 32768 (32 KB)
|
|
26
26
|
prop :max_string_bytes, Integer, default: 32_768
|
|
27
27
|
end
|
|
@@ -120,7 +120,6 @@ module LogStruct
|
|
|
120
120
|
Storage = new(:storage)
|
|
121
121
|
Operation = new(:op)
|
|
122
122
|
FileId = new(:file_id)
|
|
123
|
-
Filename = new(:filename)
|
|
124
123
|
MimeType = new(:mime_type)
|
|
125
124
|
Size = new(:size)
|
|
126
125
|
Metadata = new(:metadata)
|
|
@@ -134,8 +133,6 @@ module LogStruct
|
|
|
134
133
|
Model = new(:model)
|
|
135
134
|
MountPoint = new(:mount_point)
|
|
136
135
|
Version = new(:version)
|
|
137
|
-
StorePath = new(:store_path)
|
|
138
|
-
Extension = new(:ext)
|
|
139
136
|
|
|
140
137
|
# SQL-specific fields
|
|
141
138
|
Sql = new(:sql)
|
data/lib/log_struct/formatter.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative "enums/source"
|
|
|
10
10
|
require_relative "enums/event"
|
|
11
11
|
require_relative "enums/log_field"
|
|
12
12
|
require_relative "string_scrubber"
|
|
13
|
+
require_relative "bounded_input"
|
|
13
14
|
require_relative "log"
|
|
14
15
|
require_relative "param_filters"
|
|
15
16
|
require_relative "multi_error_reporter"
|
|
@@ -80,13 +81,13 @@ module LogStruct
|
|
|
80
81
|
return summarize_unprocessed_value(arg) if recursion_depth > MAX_RECURSION_DEPTH || ancestors.key?(arg)
|
|
81
82
|
|
|
82
83
|
case arg
|
|
83
|
-
when Hash, Array
|
|
84
|
+
when Hash, Array, ::ActionController::Parameters
|
|
84
85
|
ancestors[arg] = true
|
|
85
86
|
begin
|
|
86
|
-
if arg.is_a?(
|
|
87
|
-
process_hash(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
|
|
88
|
-
else
|
|
87
|
+
if arg.is_a?(Array)
|
|
89
88
|
process_array(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
|
|
89
|
+
else
|
|
90
|
+
process_hash(arg, recursion_depth: recursion_depth, ancestors: ancestors, path: path, root: root)
|
|
90
91
|
end
|
|
91
92
|
ensure
|
|
92
93
|
ancestors.delete(arg)
|
|
@@ -107,7 +108,7 @@ module LogStruct
|
|
|
107
108
|
scrub_string(String(object))
|
|
108
109
|
end
|
|
109
110
|
rescue => e
|
|
110
|
-
LogStruct.
|
|
111
|
+
LogStruct.handle_value_error(e, "GlobalID formatting failed")
|
|
111
112
|
"[GLOBALID_ERROR]"
|
|
112
113
|
end
|
|
113
114
|
end
|
|
@@ -125,26 +126,24 @@ module LogStruct
|
|
|
125
126
|
when Numeric, TrueClass, FalseClass, NilClass
|
|
126
127
|
arg
|
|
127
128
|
else
|
|
128
|
-
# Structs
|
|
129
|
+
# Structs and other objects: filter and scrub what
|
|
129
130
|
# to_json would write
|
|
130
131
|
process_values(arg.as_json, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: path, root: root)
|
|
131
132
|
end
|
|
132
133
|
rescue SystemStackError, StandardError => e
|
|
133
134
|
# A SystemStackError comes from an object whose as_json refers back to itself
|
|
134
|
-
e = RuntimeError.new("#{e.class}: #{e.message}") unless e.is_a?(StandardError)
|
|
135
|
-
# Report error through LogStruct's framework
|
|
136
135
|
context = {
|
|
137
136
|
processor_method: "process_values",
|
|
138
137
|
value_type: arg.class.name,
|
|
139
138
|
recursion_depth: recursion_depth
|
|
140
139
|
}
|
|
141
|
-
LogStruct.
|
|
140
|
+
LogStruct.handle_value_error(e, "value formatting failed", context: context)
|
|
142
141
|
summarize_unprocessed_value(arg)
|
|
143
142
|
end
|
|
144
143
|
|
|
145
144
|
sig do
|
|
146
145
|
params(
|
|
147
|
-
hash: T
|
|
146
|
+
hash: T.untyped,
|
|
148
147
|
recursion_depth: Integer,
|
|
149
148
|
ancestors: T::Hash[T.untyped, T::Boolean],
|
|
150
149
|
path: T.nilable(T::Array[String]),
|
|
@@ -158,13 +157,30 @@ module LogStruct
|
|
|
158
157
|
line_root = (path && recursion_depth == 0) ? ConfigStruct::LazyRoot.new(hash) : nil
|
|
159
158
|
|
|
160
159
|
max_keys = LogStruct.config.limits.max_hash_keys
|
|
161
|
-
|
|
160
|
+
max_key_bytes = LogStruct.config.limits.max_string_bytes
|
|
161
|
+
oversized_keys = 0
|
|
162
|
+
hash.each_pair.with_index do |(key, value), index|
|
|
162
163
|
if index >= max_keys
|
|
163
|
-
result[:_more_keys] = hash
|
|
164
|
+
result[:_more_keys] = BoundedInput.key_count(hash) - max_keys
|
|
164
165
|
break
|
|
165
166
|
end
|
|
166
167
|
|
|
168
|
+
if BoundedInput::COUNT_KEYS.include?(key) && value.is_a?(Integer)
|
|
169
|
+
result[key] = value
|
|
170
|
+
next
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if key.to_s.bytesize > max_key_bytes
|
|
174
|
+
oversized_keys += 1
|
|
175
|
+
next
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Checked again: each byte that isn't valid UTF-8 becomes a 3-byte U+FFFD
|
|
167
179
|
key = valid_utf8_key(key)
|
|
180
|
+
if key.to_s.bytesize > max_key_bytes
|
|
181
|
+
oversized_keys += 1
|
|
182
|
+
next
|
|
183
|
+
end
|
|
168
184
|
logstruct_field = recursion_depth == 0 && LOG_FIELD_KEYS.include?(key)
|
|
169
185
|
key_path = path && [*path, key.to_s]
|
|
170
186
|
# A top-level key's root is the line; the keys below it get the top-level field's value
|
|
@@ -173,7 +189,9 @@ module LogStruct
|
|
|
173
189
|
# Check if this key should be filtered at any depth
|
|
174
190
|
filtered = ParamFilters.should_filter_key?(key, value, logstruct_field: logstruct_field, path: key_path, root: key_root)
|
|
175
191
|
# Keys are scrubbed too: an email address can be a key
|
|
176
|
-
|
|
192
|
+
scrubbed_key = StringScrubber.scrub_name(key)
|
|
193
|
+
scrubbed_key = scrub_limited_string(scrubbed_key) if scrubbed_key.is_a?(String) && scrubbed_key.bytesize > max_key_bytes
|
|
194
|
+
result[scrubbed_key] = if !filtered
|
|
177
195
|
process_values(value, recursion_depth: recursion_depth + 1, ancestors: ancestors, path: key_path, root: children_root)
|
|
178
196
|
elsif value == RAILS_FILTERED_MASK
|
|
179
197
|
# Rails' parameter filter has already replaced the value
|
|
@@ -183,6 +201,8 @@ module LogStruct
|
|
|
183
201
|
end
|
|
184
202
|
end
|
|
185
203
|
|
|
204
|
+
result[:_oversized_keys] = oversized_keys if oversized_keys.positive?
|
|
205
|
+
|
|
186
206
|
result
|
|
187
207
|
end
|
|
188
208
|
|
|
@@ -205,7 +225,7 @@ module LogStruct
|
|
|
205
225
|
fixed = valid_utf8(name)
|
|
206
226
|
fixed.equal?(name) ? key : fixed.to_sym
|
|
207
227
|
else
|
|
208
|
-
key
|
|
228
|
+
valid_utf8(key.to_s)
|
|
209
229
|
end
|
|
210
230
|
end
|
|
211
231
|
|
|
@@ -295,15 +315,10 @@ module LogStruct
|
|
|
295
315
|
log_value.to_s
|
|
296
316
|
end
|
|
297
317
|
rescue => e
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
LogStruct.handle_exception(e, source: Source::Internal, context: context)
|
|
304
|
-
|
|
305
|
-
# Fall back to the string representation to ensure we continue processing
|
|
306
|
-
log_value.to_s
|
|
318
|
+
LogStruct.handle_value_error(e, "value serialization failed", context: {object_class: log_value.class.name})
|
|
319
|
+
|
|
320
|
+
# An object's string representation may contain the same secret that broke serialization.
|
|
321
|
+
"[SERIALIZATION_ERROR]"
|
|
307
322
|
end
|
|
308
323
|
end
|
|
309
324
|
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "openssl"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
|
|
6
7
|
module LogStruct
|
|
7
8
|
# Utility module for hashing sensitive data
|
|
8
9
|
module HashUtils
|
|
9
|
-
# The key
|
|
10
|
-
#
|
|
11
|
-
FALLBACK_KEY =
|
|
10
|
+
# The key without a hash_salt, before the Rails app has its secret_key_base. A fixed key would be public,
|
|
11
|
+
# and anyone could match its hashes to a list of candidate emails; these hashes differ between processes.
|
|
12
|
+
FALLBACK_KEY = T.let(::SecureRandom.random_bytes(32).freeze, String)
|
|
12
13
|
|
|
13
14
|
class << self
|
|
14
15
|
extend T::Sig
|
|
@@ -17,7 +18,13 @@ module LogStruct
|
|
|
17
18
|
sig { params(value: String).returns(String) }
|
|
18
19
|
def hash_value(value)
|
|
19
20
|
length = LogStruct.config.filters.hash_length
|
|
20
|
-
|
|
21
|
+
max_bytes = LogStruct.config.limits.max_string_bytes
|
|
22
|
+
input = if value.bytesize > max_bytes
|
|
23
|
+
value.byteslice(0, max_bytes).to_s.b + "\0".b + value.bytesize.to_s.b
|
|
24
|
+
else
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
OpenSSL::HMAC.hexdigest("SHA256", hash_key, input)[0...length] || "error"
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
# The configured hash_salt, or a key derived from the app's secret_key_base
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative "../bounded_input"
|
|
5
|
+
|
|
4
6
|
module LogStruct
|
|
5
7
|
module Integrations
|
|
6
8
|
# Ahoy analytics integration. If Ahoy is present, prepend a small hook to
|
|
@@ -45,10 +47,9 @@ module LogStruct
|
|
|
45
47
|
# Event properties can come from the browser, so they can be anything; a non-Hash is logged as its class
|
|
46
48
|
sig { params(properties: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
47
49
|
def self.properties_for_log(properties)
|
|
48
|
-
properties = properties.to_unsafe_h if properties.respond_to?(:to_unsafe_h)
|
|
49
50
|
case properties
|
|
50
51
|
when nil then nil
|
|
51
|
-
when Hash then properties
|
|
52
|
+
when Hash, ::ActionController::Parameters then BoundedInput.symbolize_keys(properties)
|
|
52
53
|
else {_class: properties.class.name}
|
|
53
54
|
end
|
|
54
55
|
end
|
|
@@ -6,6 +6,7 @@ begin
|
|
|
6
6
|
rescue LoadError
|
|
7
7
|
# CarrierWave gem is not available, integration will be skipped
|
|
8
8
|
end
|
|
9
|
+
require_relative "../hash_utils"
|
|
9
10
|
|
|
10
11
|
module LogStruct
|
|
11
12
|
module Integrations
|
|
@@ -49,7 +50,6 @@ module LogStruct
|
|
|
49
50
|
# file_fields is a Hash Sorbet can't check against the struct's keyword arguments
|
|
50
51
|
Log::CarrierWave::Upload.new(
|
|
51
52
|
**T.unsafe(file_fields),
|
|
52
|
-
filename: file&.filename,
|
|
53
53
|
mime_type: file&.content_type,
|
|
54
54
|
size: file&.size,
|
|
55
55
|
duration_ms: duration_ms
|
|
@@ -72,7 +72,6 @@ module LogStruct
|
|
|
72
72
|
log_file_operation do
|
|
73
73
|
Log::CarrierWave::Download.new(
|
|
74
74
|
**T.unsafe(file_fields),
|
|
75
|
-
filename: file&.filename,
|
|
76
75
|
mime_type: file&.content_type,
|
|
77
76
|
size: file&.size
|
|
78
77
|
)
|
|
@@ -87,13 +86,11 @@ module LogStruct
|
|
|
87
86
|
def file_fields
|
|
88
87
|
{
|
|
89
88
|
storage: storage.class.name.to_s.split("::").last.to_s.downcase.to_sym,
|
|
90
|
-
file_id: identifier.to_s,
|
|
89
|
+
file_id: HashUtils.hash_value(identifier.to_s),
|
|
91
90
|
uploader: self.class.name,
|
|
92
91
|
model: model&.class&.name,
|
|
93
92
|
mount_point: mounted_as&.to_s,
|
|
94
|
-
version: version_name&.to_s
|
|
95
|
-
store_path: store_path,
|
|
96
|
-
extension: file&.extension
|
|
93
|
+
version: version_name&.to_s
|
|
97
94
|
}
|
|
98
95
|
end
|
|
99
96
|
|
|
@@ -6,6 +6,7 @@ begin
|
|
|
6
6
|
rescue LoadError
|
|
7
7
|
# Lograge gem is not available, integration will be skipped
|
|
8
8
|
end
|
|
9
|
+
require_relative "../bounded_input"
|
|
9
10
|
|
|
10
11
|
module LogStruct
|
|
11
12
|
module Integrations
|
|
@@ -196,12 +197,12 @@ module LogStruct
|
|
|
196
197
|
result = custom_options_proc.call(event, options)
|
|
197
198
|
return if !result.is_a?(Hash) || result.equal?(options)
|
|
198
199
|
|
|
199
|
-
options.merge!(result
|
|
200
|
+
options.merge!(BoundedInput.symbolize_keys(result))
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(Log::Request) }
|
|
203
204
|
def build_request_log(data)
|
|
204
|
-
normalized_data =
|
|
205
|
+
normalized_data = BoundedInput.symbolize_keys(data)
|
|
205
206
|
|
|
206
207
|
# Coerce common fields to expected types
|
|
207
208
|
status = ((s = normalized_data[:status]) && s.respond_to?(:to_i)) ? s.to_i : s
|
|
@@ -210,7 +211,7 @@ module LogStruct
|
|
|
210
211
|
db = ((b = normalized_data[:db]) && b.respond_to?(:to_f)) ? b.to_f : b
|
|
211
212
|
|
|
212
213
|
params = normalized_data[:params]
|
|
213
|
-
params = params
|
|
214
|
+
params = BoundedInput.symbolize_keys(params) if params.is_a?(Hash) || params.is_a?(::ActionController::Parameters)
|
|
214
215
|
|
|
215
216
|
additional_data = extract_additional_data(normalized_data)
|
|
216
217
|
|
|
@@ -237,13 +238,6 @@ module LogStruct
|
|
|
237
238
|
)
|
|
238
239
|
end
|
|
239
240
|
|
|
240
|
-
sig { params(data: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
241
|
-
def normalize_lograge_data(data)
|
|
242
|
-
data.each_with_object({}) do |(key, value), normalized|
|
|
243
|
-
normalized[key.to_s.to_sym] = value
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
241
|
sig { params(data: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
|
248
242
|
def extract_additional_data(data)
|
|
249
243
|
extras = T.let({}, T::Hash[Symbol, T.untyped])
|
|
@@ -42,6 +42,16 @@ module LogStruct
|
|
|
42
42
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
43
43
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
44
44
|
|
|
45
|
+
FIELD_KEYS = T.let(
|
|
46
|
+
[LogField::To, LogField::From, LogField::Subject, LogField::MessageId, LogField::MailerClass, LogField::MailerAction, LogField::AttachmentCount].to_set(&:serialize).freeze,
|
|
47
|
+
T::Set[Symbol]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
51
|
+
def field_keys
|
|
52
|
+
FIELD_KEYS
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
# Serialize shared fields
|
|
46
56
|
include LogStruct::Log::Interfaces::CommonFields
|
|
47
57
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -42,6 +42,16 @@ module LogStruct
|
|
|
42
42
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
43
43
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
44
44
|
|
|
45
|
+
FIELD_KEYS = T.let(
|
|
46
|
+
[LogField::To, LogField::From, LogField::Subject, LogField::MessageId, LogField::MailerClass, LogField::MailerAction, LogField::AttachmentCount].to_set(&:serialize).freeze,
|
|
47
|
+
T::Set[Symbol]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
51
|
+
def field_keys
|
|
52
|
+
FIELD_KEYS
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
# Serialize shared fields
|
|
46
56
|
include LogStruct::Log::Interfaces::CommonFields
|
|
47
57
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -47,6 +47,16 @@ module LogStruct
|
|
|
47
47
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
48
48
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
49
49
|
|
|
50
|
+
FIELD_KEYS = T.let(
|
|
51
|
+
[LogField::To, LogField::From, LogField::Subject, LogField::MessageId, LogField::MailerClass, LogField::MailerAction, LogField::AttachmentCount, LogField::ErrorClass, LogField::Message, LogField::Backtrace].to_set(&:serialize).freeze,
|
|
52
|
+
T::Set[Symbol]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
56
|
+
def field_keys
|
|
57
|
+
FIELD_KEYS
|
|
58
|
+
end
|
|
59
|
+
|
|
50
60
|
# Serialize shared fields
|
|
51
61
|
include LogStruct::Log::Interfaces::CommonFields
|
|
52
62
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -35,8 +35,6 @@ module LogStruct
|
|
|
35
35
|
const :model, T.nilable(String), default: nil
|
|
36
36
|
const :mount_point, T.nilable(String), default: nil
|
|
37
37
|
const :version, T.nilable(String), default: nil
|
|
38
|
-
const :store_path, T.nilable(String), default: nil
|
|
39
|
-
const :extension, T.nilable(String), default: nil
|
|
40
38
|
|
|
41
39
|
# Serialize shared fields
|
|
42
40
|
include LogStruct::Log::Interfaces::CommonFields
|
|
@@ -51,8 +49,6 @@ module LogStruct
|
|
|
51
49
|
h[LogField::Model] = model unless model.nil?
|
|
52
50
|
h[LogField::MountPoint] = mount_point unless mount_point.nil?
|
|
53
51
|
h[LogField::Version] = version unless version.nil?
|
|
54
|
-
h[LogField::StorePath] = store_path unless store_path.nil?
|
|
55
|
-
h[LogField::Extension] = extension unless extension.nil?
|
|
56
52
|
h
|
|
57
53
|
end
|
|
58
54
|
end
|
|
@@ -35,11 +35,8 @@ module LogStruct
|
|
|
35
35
|
const :model, T.nilable(String), default: nil
|
|
36
36
|
const :mount_point, T.nilable(String), default: nil
|
|
37
37
|
const :version, T.nilable(String), default: nil
|
|
38
|
-
const :store_path, T.nilable(String), default: nil
|
|
39
|
-
const :extension, T.nilable(String), default: nil
|
|
40
38
|
|
|
41
39
|
# Event-specific fields
|
|
42
|
-
const :filename, T.nilable(String), default: nil
|
|
43
40
|
const :mime_type, T.nilable(String), default: nil
|
|
44
41
|
const :size, T.nilable(Integer), default: nil
|
|
45
42
|
|
|
@@ -56,9 +53,6 @@ module LogStruct
|
|
|
56
53
|
h[LogField::Model] = model unless model.nil?
|
|
57
54
|
h[LogField::MountPoint] = mount_point unless mount_point.nil?
|
|
58
55
|
h[LogField::Version] = version unless version.nil?
|
|
59
|
-
h[LogField::StorePath] = store_path unless store_path.nil?
|
|
60
|
-
h[LogField::Extension] = extension unless extension.nil?
|
|
61
|
-
h[LogField::Filename] = filename unless filename.nil?
|
|
62
56
|
h[LogField::MimeType] = mime_type unless mime_type.nil?
|
|
63
57
|
h[LogField::Size] = size unless size.nil?
|
|
64
58
|
h
|
|
@@ -35,11 +35,8 @@ module LogStruct
|
|
|
35
35
|
const :model, T.nilable(String), default: nil
|
|
36
36
|
const :mount_point, T.nilable(String), default: nil
|
|
37
37
|
const :version, T.nilable(String), default: nil
|
|
38
|
-
const :store_path, T.nilable(String), default: nil
|
|
39
|
-
const :extension, T.nilable(String), default: nil
|
|
40
38
|
|
|
41
39
|
# Event-specific fields
|
|
42
|
-
const :filename, T.nilable(String), default: nil
|
|
43
40
|
const :mime_type, T.nilable(String), default: nil
|
|
44
41
|
const :size, T.nilable(Integer), default: nil
|
|
45
42
|
const :metadata, T.nilable(T::Hash[String, T.untyped]), default: nil
|
|
@@ -58,9 +55,6 @@ module LogStruct
|
|
|
58
55
|
h[LogField::Model] = model unless model.nil?
|
|
59
56
|
h[LogField::MountPoint] = mount_point unless mount_point.nil?
|
|
60
57
|
h[LogField::Version] = version unless version.nil?
|
|
61
|
-
h[LogField::StorePath] = store_path unless store_path.nil?
|
|
62
|
-
h[LogField::Extension] = extension unless extension.nil?
|
|
63
|
-
h[LogField::Filename] = filename unless filename.nil?
|
|
64
58
|
h[LogField::MimeType] = mime_type unless mime_type.nil?
|
|
65
59
|
h[LogField::Size] = size unless size.nil?
|
|
66
60
|
h[LogField::Metadata] = metadata unless metadata.nil?
|
|
@@ -21,8 +21,6 @@ module LogStruct
|
|
|
21
21
|
const :model, T.nilable(String), default: nil
|
|
22
22
|
const :mount_point, T.nilable(String), default: nil
|
|
23
23
|
const :version, T.nilable(String), default: nil
|
|
24
|
-
const :store_path, T.nilable(String), default: nil
|
|
25
|
-
const :extension, T.nilable(String), default: nil
|
|
26
24
|
|
|
27
25
|
Kwargs = T.type_alias do
|
|
28
26
|
{
|
|
@@ -31,9 +29,7 @@ module LogStruct
|
|
|
31
29
|
uploader: T.nilable(String),
|
|
32
30
|
model: T.nilable(String),
|
|
33
31
|
mount_point: T.nilable(String),
|
|
34
|
-
version: T.nilable(String)
|
|
35
|
-
store_path: T.nilable(String),
|
|
36
|
-
extension: T.nilable(String)
|
|
32
|
+
version: T.nilable(String)
|
|
37
33
|
}
|
|
38
34
|
end
|
|
39
35
|
|
|
@@ -45,9 +41,7 @@ module LogStruct
|
|
|
45
41
|
uploader: uploader,
|
|
46
42
|
model: model,
|
|
47
43
|
mount_point: mount_point,
|
|
48
|
-
version: version
|
|
49
|
-
store_path: store_path,
|
|
50
|
-
extension: extension
|
|
44
|
+
version: version
|
|
51
45
|
}
|
|
52
46
|
end
|
|
53
47
|
end
|
data/lib/log_struct/log/error.rb
CHANGED
|
@@ -48,6 +48,16 @@ module LogStruct
|
|
|
48
48
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
49
49
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
50
50
|
|
|
51
|
+
FIELD_KEYS = T.let(
|
|
52
|
+
[LogField::Path, LogField::HttpMethod, LogField::SourceIp, LogField::UserAgent, LogField::Referer, LogField::ErrorClass, LogField::Message, LogField::ErrorMessage, LogField::Backtrace, LogField::Status, LogField::CauseClass, LogField::CauseMessage].to_set(&:serialize).freeze,
|
|
53
|
+
T::Set[Symbol]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
57
|
+
def field_keys
|
|
58
|
+
FIELD_KEYS
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
# Request fields (optional)
|
|
52
62
|
include LogStruct::Log::Interfaces::RequestFields
|
|
53
63
|
|
data/lib/log_struct/log/plain.rb
CHANGED
|
@@ -36,6 +36,16 @@ module LogStruct
|
|
|
36
36
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
37
37
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
38
38
|
|
|
39
|
+
FIELD_KEYS = T.let(
|
|
40
|
+
[LogField::Message].to_set(&:serialize).freeze,
|
|
41
|
+
T::Set[Symbol]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
45
|
+
def field_keys
|
|
46
|
+
FIELD_KEYS
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
# Serialize shared fields
|
|
40
50
|
include LogStruct::Log::Interfaces::CommonFields
|
|
41
51
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -42,6 +42,16 @@ module LogStruct
|
|
|
42
42
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
43
43
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
44
44
|
|
|
45
|
+
FIELD_KEYS = T.let(
|
|
46
|
+
[LogField::ErrorClass, LogField::Message, LogField::Backtrace, LogField::ErrorContext, LogField::HttpMethod, LogField::Path].to_set(&:serialize).freeze,
|
|
47
|
+
T::Set[Symbol]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
51
|
+
def field_keys
|
|
52
|
+
FIELD_KEYS
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
# Serialize shared fields
|
|
46
56
|
include LogStruct::Log::Interfaces::CommonFields
|
|
47
57
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -37,6 +37,16 @@ module LogStruct
|
|
|
37
37
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
38
38
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
39
39
|
|
|
40
|
+
FIELD_KEYS = T.let(
|
|
41
|
+
[LogField::ProcessId].to_set(&:serialize).freeze,
|
|
42
|
+
T::Set[Symbol]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
46
|
+
def field_keys
|
|
47
|
+
FIELD_KEYS
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
# Serialize shared fields
|
|
41
51
|
include LogStruct::Log::Interfaces::CommonFields
|
|
42
52
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -45,6 +45,16 @@ module LogStruct
|
|
|
45
45
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
46
46
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
47
47
|
|
|
48
|
+
FIELD_KEYS = T.let(
|
|
49
|
+
[LogField::Mode, LogField::PumaVersion, LogField::PumaCodename, LogField::RubyVersion, LogField::MinThreads, LogField::MaxThreads, LogField::Environment, LogField::ProcessId, LogField::ListeningAddresses].to_set(&:serialize).freeze,
|
|
50
|
+
T::Set[Symbol]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
54
|
+
def field_keys
|
|
55
|
+
FIELD_KEYS
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
# Serialize shared fields
|
|
49
59
|
include LogStruct::Log::Interfaces::CommonFields
|
|
50
60
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -52,6 +52,16 @@ module LogStruct
|
|
|
52
52
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
53
53
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
54
54
|
|
|
55
|
+
FIELD_KEYS = T.let(
|
|
56
|
+
[LogField::Path, LogField::HttpMethod, LogField::SourceIp, LogField::UserAgent, LogField::Referer, LogField::Format, LogField::Controller, LogField::Action, LogField::Status, LogField::DurationMs, LogField::View, LogField::Database, LogField::Params, LogField::Host, LogField::ContentType, LogField::Accept].to_set(&:serialize).freeze,
|
|
57
|
+
T::Set[Symbol]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
61
|
+
def field_keys
|
|
62
|
+
FIELD_KEYS
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
# Request fields (optional)
|
|
56
66
|
include LogStruct::Log::Interfaces::RequestFields
|
|
57
67
|
|
|
@@ -46,6 +46,16 @@ module LogStruct
|
|
|
46
46
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
47
47
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
48
48
|
|
|
49
|
+
FIELD_KEYS = T.let(
|
|
50
|
+
[LogField::Path, LogField::HttpMethod, LogField::SourceIp, LogField::UserAgent, LogField::Referer, LogField::Message, LogField::BlockedHost, LogField::BlockedHosts, LogField::XForwardedFor].to_set(&:serialize).freeze,
|
|
51
|
+
T::Set[Symbol]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
55
|
+
def field_keys
|
|
56
|
+
FIELD_KEYS
|
|
57
|
+
end
|
|
58
|
+
|
|
49
59
|
# Request fields (optional)
|
|
50
60
|
include LogStruct::Log::Interfaces::RequestFields
|
|
51
61
|
|
|
@@ -43,6 +43,16 @@ module LogStruct
|
|
|
43
43
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
44
44
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
45
45
|
|
|
46
|
+
FIELD_KEYS = T.let(
|
|
47
|
+
[LogField::Path, LogField::HttpMethod, LogField::SourceIp, LogField::UserAgent, LogField::Referer, LogField::Message].to_set(&:serialize).freeze,
|
|
48
|
+
T::Set[Symbol]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
52
|
+
def field_keys
|
|
53
|
+
FIELD_KEYS
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
# Request fields (optional)
|
|
47
57
|
include LogStruct::Log::Interfaces::RequestFields
|
|
48
58
|
|
|
@@ -45,6 +45,16 @@ module LogStruct
|
|
|
45
45
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
46
46
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
47
47
|
|
|
48
|
+
FIELD_KEYS = T.let(
|
|
49
|
+
[LogField::Path, LogField::HttpMethod, LogField::SourceIp, LogField::UserAgent, LogField::Referer, LogField::Message, LogField::ClientIp, LogField::XForwardedFor].to_set(&:serialize).freeze,
|
|
50
|
+
T::Set[Symbol]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
54
|
+
def field_keys
|
|
55
|
+
FIELD_KEYS
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
# Request fields (optional)
|
|
49
59
|
include LogStruct::Log::Interfaces::RequestFields
|
|
50
60
|
|
|
@@ -42,6 +42,16 @@ module LogStruct
|
|
|
42
42
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
43
43
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
44
44
|
|
|
45
|
+
FIELD_KEYS = T.let(
|
|
46
|
+
[LogField::ErrorClass, LogField::Message, LogField::Backtrace, LogField::ErrorContext, LogField::QueueName, LogField::RetryCount].to_set(&:serialize).freeze,
|
|
47
|
+
T::Set[Symbol]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
51
|
+
def field_keys
|
|
52
|
+
FIELD_KEYS
|
|
53
|
+
end
|
|
54
|
+
|
|
45
55
|
# Serialize shared fields
|
|
46
56
|
include LogStruct::Log::Interfaces::CommonFields
|
|
47
57
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -40,6 +40,16 @@ module LogStruct
|
|
|
40
40
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
41
41
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
42
42
|
|
|
43
|
+
FIELD_KEYS = T.let(
|
|
44
|
+
[LogField::Message, LogField::Context, LogField::ProcessId, LogField::ThreadId].to_set(&:serialize).freeze,
|
|
45
|
+
T::Set[Symbol]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
49
|
+
def field_keys
|
|
50
|
+
FIELD_KEYS
|
|
51
|
+
end
|
|
52
|
+
|
|
43
53
|
# Serialize shared fields
|
|
44
54
|
include LogStruct::Log::Interfaces::CommonFields
|
|
45
55
|
include LogStruct::Log::Shared::SerializeCommon
|
data/lib/log_struct/log/sql.rb
CHANGED
|
@@ -47,6 +47,16 @@ module LogStruct
|
|
|
47
47
|
const :additional_data, T.nilable(T::Hash[T.any(String, Symbol), T.untyped]), default: nil
|
|
48
48
|
include LogStruct::Log::Shared::MergeAdditionalDataFields
|
|
49
49
|
|
|
50
|
+
FIELD_KEYS = T.let(
|
|
51
|
+
[LogField::Message, LogField::Sql, LogField::Name, LogField::DurationMs, LogField::RowCount, LogField::Adapter, LogField::BindParams, LogField::DatabaseName, LogField::ConnectionPoolSize, LogField::ActiveConnections, LogField::OperationType, LogField::TableNames].to_set(&:serialize).freeze,
|
|
52
|
+
T::Set[Symbol]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
sig { override.returns(T::Set[Symbol]) }
|
|
56
|
+
def field_keys
|
|
57
|
+
FIELD_KEYS
|
|
58
|
+
end
|
|
59
|
+
|
|
50
60
|
# Serialize shared fields
|
|
51
61
|
include LogStruct::Log::Interfaces::CommonFields
|
|
52
62
|
include LogStruct::Log::Shared::SerializeCommon
|
|
@@ -43,7 +43,7 @@ module LogStruct
|
|
|
43
43
|
|
|
44
44
|
matcher.matches?(normalized_key, value)
|
|
45
45
|
rescue => e
|
|
46
|
-
handle_filter_matcher_error(e, matcher.label
|
|
46
|
+
handle_filter_matcher_error(e, matcher.label)
|
|
47
47
|
# An unsuccessful filter must never expose the value it was meant to protect.
|
|
48
48
|
true
|
|
49
49
|
end
|
|
@@ -55,7 +55,7 @@ module LogStruct
|
|
|
55
55
|
filters.path_matchers.any? do |matcher|
|
|
56
56
|
matcher.matches?(path, value, root)
|
|
57
57
|
rescue => e
|
|
58
|
-
handle_filter_matcher_error(e, matcher.label
|
|
58
|
+
handle_filter_matcher_error(e, matcher.label)
|
|
59
59
|
true
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -94,16 +94,14 @@ module LogStruct
|
|
|
94
94
|
filtered_string
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
#
|
|
98
|
-
# how long the secrets in it are.
|
|
97
|
+
# A filtered Hash's child keys can themselves contain secrets.
|
|
99
98
|
sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
|
|
100
99
|
def summarize_hash(hash)
|
|
101
100
|
return {_class: "Hash", _empty: true} if hash.empty?
|
|
102
101
|
|
|
103
102
|
{
|
|
104
103
|
_class: Hash,
|
|
105
|
-
_keys_count: hash.size
|
|
106
|
-
_keys: hash.each_key.take(10).map { |key| normalize_summary_key(StringScrubber.scrub_name(key)) }
|
|
104
|
+
_keys_count: hash.size
|
|
107
105
|
}
|
|
108
106
|
end
|
|
109
107
|
|
|
@@ -120,26 +118,9 @@ module LogStruct
|
|
|
120
118
|
|
|
121
119
|
private
|
|
122
120
|
|
|
123
|
-
sig { params(
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
key
|
|
127
|
-
elsif key.respond_to?(:to_sym)
|
|
128
|
-
key.to_sym
|
|
129
|
-
else
|
|
130
|
-
key.to_s
|
|
131
|
-
end
|
|
132
|
-
rescue
|
|
133
|
-
key.to_s
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
sig { params(error: StandardError, label: String, key: String).void }
|
|
137
|
-
def handle_filter_matcher_error(error, label, key)
|
|
138
|
-
context = {
|
|
139
|
-
matcher: label,
|
|
140
|
-
key: key
|
|
141
|
-
}
|
|
142
|
-
LogStruct.handle_exception(error, source: Source::Internal, context: context)
|
|
121
|
+
sig { params(error: StandardError, label: String).void }
|
|
122
|
+
def handle_filter_matcher_error(error, label)
|
|
123
|
+
LogStruct.handle_value_error(error, "filter matcher failed", context: {matcher: label})
|
|
143
124
|
end
|
|
144
125
|
end
|
|
145
126
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require_relative "interfaces/additional_data_field"
|
|
5
5
|
require_relative "../enums/source"
|
|
6
|
+
require_relative "../enums/log_field"
|
|
6
7
|
|
|
7
8
|
module LogStruct
|
|
8
9
|
module Log
|
|
@@ -14,17 +15,32 @@ module LogStruct
|
|
|
14
15
|
requires_ancestor { T::Struct }
|
|
15
16
|
requires_ancestor { Interfaces::AdditionalDataField }
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# Every line's own fields, and the request ID the formatter adds unless the line has one
|
|
19
|
+
RESERVED_KEYS = T.let(
|
|
20
|
+
[LogField::Source, LogField::Event, LogField::Level, LogField::Timestamp, LogField::RequestId].to_set(&:serialize).freeze,
|
|
21
|
+
T::Set[Symbol]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
NO_FIELD_KEYS = T.let(Set.new.freeze, T::Set[Symbol])
|
|
25
|
+
|
|
26
|
+
# The keys of every field of the struct's type, set or not on this line, which additional_data can't
|
|
27
|
+
# set: it would replace the field, or stand in for one the type didn't write. LogStruct's log types list
|
|
28
|
+
# theirs. A custom type's serialize writes its own fields after merging additional_data, so they win.
|
|
29
|
+
sig { overridable.returns(T::Set[Symbol]) }
|
|
30
|
+
def field_keys
|
|
31
|
+
NO_FIELD_KEYS
|
|
32
|
+
end
|
|
20
33
|
|
|
21
34
|
sig { params(hash: T::Hash[Symbol, T.untyped]).void }
|
|
22
35
|
def merge_additional_data_fields(hash)
|
|
23
36
|
ad = additional_data
|
|
24
37
|
return unless ad
|
|
25
|
-
ad.
|
|
38
|
+
ad.each_pair.with_index do |(key, value), index|
|
|
39
|
+
break if index >= LogStruct.config.limits.max_hash_keys
|
|
40
|
+
next if key.to_s.bytesize > LogStruct.config.limits.max_string_bytes
|
|
41
|
+
|
|
26
42
|
sym_key = key.to_sym
|
|
27
|
-
if RESERVED_KEYS.include?(sym_key)
|
|
43
|
+
if RESERVED_KEYS.include?(sym_key) || field_keys.include?(sym_key)
|
|
28
44
|
LogStruct.handle_exception(
|
|
29
45
|
ArgumentError.new("additional_data attempted to overwrite reserved key: #{sym_key}"),
|
|
30
46
|
source: Source::Internal
|
data/lib/log_struct/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: logstruct
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- DocSpring
|
|
@@ -283,6 +283,7 @@ files:
|
|
|
283
283
|
- README.md
|
|
284
284
|
- lib/log_struct.rb
|
|
285
285
|
- lib/log_struct/boot_buffer.rb
|
|
286
|
+
- lib/log_struct/bounded_input.rb
|
|
286
287
|
- lib/log_struct/concerns/configuration.rb
|
|
287
288
|
- lib/log_struct/concerns/error_handling.rb
|
|
288
289
|
- lib/log_struct/concerns/logging.rb
|