philiprehberger-log_filter 0.1.7 → 0.2.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 +8 -0
- data/README.md +67 -0
- data/lib/philiprehberger/log_filter/filter.rb +134 -4
- data/lib/philiprehberger/log_filter/version.rb +1 -1
- data/lib/philiprehberger/log_filter.rb +4 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 683ed9a496a285367a229889b099e3fe5dcaacce4c4f659d98f51c1f151f77f6
|
|
4
|
+
data.tar.gz: 97ad3d5ecb23b237128cbe1f416c444bf531e428d6f5611aeb1ca3f606fa7d16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8fc6d49639fc0b38e7915953f19d41d8aec7f0e6eeae4eff394cbc55ebed7331ef88a3f0da856d8eddeac762a20d1e24c0de84ac29003bdb621a49ad893de38
|
|
7
|
+
data.tar.gz: df60ffb387bbb13b15fe9cb903d46494bcc33800822a5e42dc73e5b01275943ca0840a0b2d5bf93355e66d411995b129a9f17ff3ff79874e4e346fa0920bb248
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-03-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Sampling support via `Filter#sample(pattern, rate:)` — pass through only a fraction of matching messages
|
|
15
|
+
- Filter statistics via `Filter#stats` and `Filter#reset_stats!` — thread-safe atomic counters for dropped, passed, replaced, and sampled messages
|
|
16
|
+
- Structured log support via `Filter#drop_field(key)` and `Filter#mask_field(key, with:)` — parse JSON messages, remove or mask fields, re-serialize
|
|
17
|
+
|
|
10
18
|
## [0.1.7] - 2026-03-26
|
|
11
19
|
|
|
12
20
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/philiprehberger/rb-log-filter/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-log_filter)
|
|
5
|
+
[](https://github.com/philiprehberger/rb-log-filter/releases)
|
|
6
|
+
[](https://github.com/philiprehberger/rb-log-filter/commits/main)
|
|
5
7
|
[](LICENSE)
|
|
8
|
+
[](https://github.com/philiprehberger/rb-log-filter/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
|
9
|
+
[](https://github.com/philiprehberger/rb-log-filter/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
|
6
10
|
[](https://github.com/sponsors/philiprehberger)
|
|
7
11
|
|
|
8
12
|
Pattern-based log filtering with drop, replace, and preset rules
|
|
@@ -63,6 +67,8 @@ filtered_logger.info("GET /api/users 200") # logs normally
|
|
|
63
67
|
### Using Presets
|
|
64
68
|
|
|
65
69
|
```ruby
|
|
70
|
+
require "philiprehberger/log_filter"
|
|
71
|
+
|
|
66
72
|
# Drop health-check noise
|
|
67
73
|
filter = Philiprehberger::LogFilter.health_check_filter
|
|
68
74
|
filtered_logger = Philiprehberger::LogFilter.wrap(logger, filter)
|
|
@@ -77,11 +83,60 @@ filter = Philiprehberger::LogFilter.bot_filter
|
|
|
77
83
|
### Block-Based Drop Rules
|
|
78
84
|
|
|
79
85
|
```ruby
|
|
86
|
+
require "philiprehberger/log_filter"
|
|
87
|
+
|
|
80
88
|
filter = Philiprehberger::LogFilter::Filter.new
|
|
81
89
|
.drop_if { |msg| msg.length > 1000 } # drop excessively long messages
|
|
82
90
|
.drop_if { |msg| msg.count("\n") > 10 } # drop multi-line spam
|
|
83
91
|
```
|
|
84
92
|
|
|
93
|
+
### Sampling
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
require "philiprehberger/log_filter"
|
|
97
|
+
|
|
98
|
+
# Only pass through 10% of debug messages
|
|
99
|
+
filter = Philiprehberger::LogFilter::Filter.new
|
|
100
|
+
.sample(/DEBUG/, rate: 0.1)
|
|
101
|
+
|
|
102
|
+
filter.apply("DEBUG verbose output") # => nil (90% of the time)
|
|
103
|
+
filter.apply("INFO normal message") # => "INFO normal message" (always passes)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Structured Log Support
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require "philiprehberger/log_filter"
|
|
110
|
+
|
|
111
|
+
filter = Philiprehberger::LogFilter::Filter.new
|
|
112
|
+
.drop_field("password")
|
|
113
|
+
.mask_field("ssn", with: "***")
|
|
114
|
+
|
|
115
|
+
filter.apply('{"user":"alice","password":"secret","ssn":"123-45-6789"}')
|
|
116
|
+
# => '{"user":"alice","ssn":"***"}'
|
|
117
|
+
|
|
118
|
+
# Non-JSON messages pass through unmodified
|
|
119
|
+
filter.apply("plain text log line") # => "plain text log line"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Filter Statistics
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
require "philiprehberger/log_filter"
|
|
126
|
+
|
|
127
|
+
filter = Philiprehberger::LogFilter::Filter.new
|
|
128
|
+
.drop(/DEBUG/)
|
|
129
|
+
.replace(/secret/, "[REDACTED]")
|
|
130
|
+
|
|
131
|
+
filter.apply("DEBUG noise")
|
|
132
|
+
filter.apply("has secret data")
|
|
133
|
+
filter.apply("normal message")
|
|
134
|
+
|
|
135
|
+
filter.stats # => { dropped: 1, passed: 2, replaced: 1, sampled: 0 }
|
|
136
|
+
filter.reset_stats!
|
|
137
|
+
filter.stats # => { dropped: 0, passed: 0, replaced: 0, sampled: 0 }
|
|
138
|
+
```
|
|
139
|
+
|
|
85
140
|
## API
|
|
86
141
|
|
|
87
142
|
| Class / Method | Description |
|
|
@@ -90,7 +145,12 @@ filter = Philiprehberger::LogFilter::Filter.new
|
|
|
90
145
|
| `Filter#drop(pattern)` | Add a regex drop rule; returns self |
|
|
91
146
|
| `Filter#drop_if(&block)` | Add a block-based drop rule; returns self |
|
|
92
147
|
| `Filter#replace(pattern, replacement)` | Add a replacement rule; returns self |
|
|
148
|
+
| `Filter#sample(pattern, rate:)` | Add a sampling rule; only pass rate fraction of matches |
|
|
149
|
+
| `Filter#drop_field(key)` | Remove a field from JSON log messages; returns self |
|
|
150
|
+
| `Filter#mask_field(key, with:)` | Mask a field value in JSON log messages; returns self |
|
|
93
151
|
| `Filter#apply(message)` | Run all rules; returns transformed string or nil |
|
|
152
|
+
| `Filter#stats` | Return counters: dropped, passed, replaced, sampled |
|
|
153
|
+
| `Filter#reset_stats!` | Zero all statistics counters |
|
|
94
154
|
| `Wrapper.new(logger, filter)` | Wrap a Logger with a filter |
|
|
95
155
|
| `Presets.health_check` | Filter dropping health-check paths |
|
|
96
156
|
| `Presets.assets` | Filter dropping static-asset requests |
|
|
@@ -108,6 +168,13 @@ bundle exec rspec
|
|
|
108
168
|
bundle exec rubocop
|
|
109
169
|
```
|
|
110
170
|
|
|
171
|
+
## Support
|
|
172
|
+
|
|
173
|
+
If you find this package useful, consider giving it a star on GitHub — it helps motivate continued maintenance and development.
|
|
174
|
+
|
|
175
|
+
[](https://www.linkedin.com/in/philiprehberger)
|
|
176
|
+
[](https://philiprehberger.com/open-source-packages)
|
|
177
|
+
|
|
111
178
|
## License
|
|
112
179
|
|
|
113
180
|
[MIT](LICENSE)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
3
6
|
module Philiprehberger
|
|
4
7
|
module LogFilter
|
|
5
8
|
# Chain of rules that can drop or transform log messages.
|
|
@@ -13,6 +16,8 @@ module Philiprehberger
|
|
|
13
16
|
|
|
14
17
|
def initialize
|
|
15
18
|
@rules = []
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@stats = { dropped: 0, passed: 0, replaced: 0, sampled: 0 }
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
# Add a pattern-based drop rule. Messages matching +pattern+ are suppressed.
|
|
@@ -47,6 +52,56 @@ module Philiprehberger
|
|
|
47
52
|
self
|
|
48
53
|
end
|
|
49
54
|
|
|
55
|
+
# Add a sampling rule. Only pass through +rate+ fraction of messages
|
|
56
|
+
# matching +pattern+. Non-matching messages pass through unaffected.
|
|
57
|
+
#
|
|
58
|
+
# @param pattern [Regexp] the pattern to match
|
|
59
|
+
# @param rate [Float] sampling rate between 0.0 and 1.0
|
|
60
|
+
# @return [self] for chaining
|
|
61
|
+
def sample(pattern, rate:)
|
|
62
|
+
raise ArgumentError, 'rate must be between 0.0 and 1.0' unless rate.is_a?(Numeric) && rate >= 0.0 && rate <= 1.0
|
|
63
|
+
|
|
64
|
+
@rules << { type: :sample, pattern: pattern, rate: rate.to_f }
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Add a rule to remove a field from JSON log messages.
|
|
69
|
+
# Non-JSON messages pass through unmodified.
|
|
70
|
+
#
|
|
71
|
+
# @param key [String] the JSON field key to remove
|
|
72
|
+
# @return [self] for chaining
|
|
73
|
+
def drop_field(key)
|
|
74
|
+
@rules << { type: :drop_field, key: key.to_s }
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Add a rule to mask a field value in JSON log messages.
|
|
79
|
+
# Non-JSON messages pass through unmodified.
|
|
80
|
+
#
|
|
81
|
+
# @param key [String] the JSON field key to mask
|
|
82
|
+
# @param with [String] the mask replacement value
|
|
83
|
+
# @return [self] for chaining
|
|
84
|
+
def mask_field(key, with: '***')
|
|
85
|
+
@rules << { type: :mask_field, key: key.to_s, mask: with }
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Return current filter statistics.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] counters for :dropped, :passed, :replaced, :sampled
|
|
92
|
+
def stats
|
|
93
|
+
@mutex.synchronize { @stats.dup }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Reset all statistics counters to zero.
|
|
97
|
+
#
|
|
98
|
+
# @return [void]
|
|
99
|
+
def reset_stats!
|
|
100
|
+
@mutex.synchronize do
|
|
101
|
+
@stats = { dropped: 0, passed: 0, replaced: 0, sampled: 0 }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
50
105
|
# Run all rules against +message+ in order.
|
|
51
106
|
#
|
|
52
107
|
# @param message [String] the log message to filter
|
|
@@ -56,23 +111,98 @@ module Philiprehberger
|
|
|
56
111
|
|
|
57
112
|
@rules.each do |rule|
|
|
58
113
|
result = apply_rule(rule, result)
|
|
59
|
-
|
|
114
|
+
if result.nil?
|
|
115
|
+
increment_stat(:dropped)
|
|
116
|
+
return nil
|
|
117
|
+
end
|
|
60
118
|
end
|
|
61
119
|
|
|
120
|
+
increment_stat(:passed)
|
|
62
121
|
result
|
|
63
122
|
end
|
|
64
123
|
|
|
65
124
|
private
|
|
66
125
|
|
|
126
|
+
# @param stat [Symbol] the stat key to increment
|
|
127
|
+
# @return [void]
|
|
128
|
+
def increment_stat(stat)
|
|
129
|
+
@mutex.synchronize { @stats[stat] += 1 }
|
|
130
|
+
end
|
|
131
|
+
|
|
67
132
|
# @param rule [Hash] a single rule hash
|
|
68
133
|
# @param message [String] the current message
|
|
69
134
|
# @return [String, nil]
|
|
70
135
|
def apply_rule(rule, message)
|
|
71
136
|
case rule[:type]
|
|
72
|
-
when :drop_pattern
|
|
73
|
-
|
|
74
|
-
when :
|
|
137
|
+
when :drop_pattern
|
|
138
|
+
message.match?(rule[:pattern]) ? nil : message
|
|
139
|
+
when :drop_block
|
|
140
|
+
rule[:block].call(message) ? nil : message
|
|
141
|
+
when :replace
|
|
142
|
+
replaced = message.gsub(rule[:pattern], rule[:replacement])
|
|
143
|
+
if replaced != message
|
|
144
|
+
increment_stat(:replaced)
|
|
145
|
+
end
|
|
146
|
+
replaced
|
|
147
|
+
when :sample
|
|
148
|
+
apply_sample_rule(rule, message)
|
|
149
|
+
when :drop_field
|
|
150
|
+
apply_drop_field_rule(rule, message)
|
|
151
|
+
when :mask_field
|
|
152
|
+
apply_mask_field_rule(rule, message)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @param rule [Hash] a sample rule
|
|
157
|
+
# @param message [String] the current message
|
|
158
|
+
# @return [String, nil]
|
|
159
|
+
def apply_sample_rule(rule, message)
|
|
160
|
+
return message unless message.match?(rule[:pattern])
|
|
161
|
+
|
|
162
|
+
return unless SecureRandom.rand < rule[:rate]
|
|
163
|
+
|
|
164
|
+
increment_stat(:sampled)
|
|
165
|
+
message
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @param rule [Hash] a drop_field rule
|
|
169
|
+
# @param message [String] the current message
|
|
170
|
+
# @return [String]
|
|
171
|
+
def apply_drop_field_rule(rule, message)
|
|
172
|
+
parsed = parse_json(message)
|
|
173
|
+
return message unless parsed
|
|
174
|
+
|
|
175
|
+
parsed.delete(rule[:key])
|
|
176
|
+
JSON.generate(parsed)
|
|
177
|
+
rescue StandardError
|
|
178
|
+
message
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @param rule [Hash] a mask_field rule
|
|
182
|
+
# @param message [String] the current message
|
|
183
|
+
# @return [String]
|
|
184
|
+
def apply_mask_field_rule(rule, message)
|
|
185
|
+
parsed = parse_json(message)
|
|
186
|
+
return message unless parsed
|
|
187
|
+
|
|
188
|
+
if parsed.key?(rule[:key])
|
|
189
|
+
parsed[rule[:key]] = rule[:mask]
|
|
190
|
+
increment_stat(:replaced)
|
|
75
191
|
end
|
|
192
|
+
JSON.generate(parsed)
|
|
193
|
+
rescue StandardError
|
|
194
|
+
message
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Attempt to parse a string as JSON.
|
|
198
|
+
#
|
|
199
|
+
# @param str [String] the string to parse
|
|
200
|
+
# @return [Hash, nil] parsed hash or nil if not valid JSON object
|
|
201
|
+
def parse_json(str)
|
|
202
|
+
result = JSON.parse(str)
|
|
203
|
+
result.is_a?(Hash) ? result : nil
|
|
204
|
+
rescue JSON::ParserError
|
|
205
|
+
nil
|
|
76
206
|
end
|
|
77
207
|
end
|
|
78
208
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
3
|
+
require_relative 'log_filter/version'
|
|
4
|
+
require_relative 'log_filter/filter'
|
|
5
|
+
require_relative 'log_filter/wrapper'
|
|
6
|
+
require_relative 'log_filter/presets'
|
|
7
7
|
|
|
8
8
|
module Philiprehberger
|
|
9
9
|
module LogFilter
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-log_filter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Pattern-based log filtering — drop or transform log lines matching rules.
|
|
14
14
|
Includes preset filters for health checks, static assets, and bot traffic.
|