semantic_logger 5.0.0 → 5.1.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/README.md +9 -0
- data/Rakefile +29 -0
- data/docs/api.md +480 -0
- data/docs/appenders.md +990 -0
- data/docs/config.md +605 -0
- data/docs/index.md +197 -0
- data/docs/log.md +108 -0
- data/docs/metrics.md +168 -0
- data/docs/operations.md +400 -0
- data/docs/rails.md +816 -0
- data/docs/security.md +119 -0
- data/docs/testing.md +285 -0
- data/docs/upgrading.md +409 -0
- data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -2
- data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
- data/lib/semantic_logger/appender/elasticsearch_base.rb +17 -17
- data/lib/semantic_logger/appender/elasticsearch_http.rb +14 -7
- data/lib/semantic_logger/appender/file.rb +5 -0
- data/lib/semantic_logger/appender/new_relic.rb +6 -0
- data/lib/semantic_logger/appender/new_relic_logs.rb +2 -1
- data/lib/semantic_logger/appender/open_telemetry.rb +2 -1
- data/lib/semantic_logger/appender/opensearch.rb +2 -1
- data/lib/semantic_logger/appender/rabbitmq.rb +4 -3
- data/lib/semantic_logger/appender/sentry.rb +5 -0
- data/lib/semantic_logger/appender/syslog.rb +6 -3
- data/lib/semantic_logger/appender/tcp.rb +4 -3
- data/lib/semantic_logger/appender/wrapper.rb +2 -1
- data/lib/semantic_logger/appender.rb +6 -3
- data/lib/semantic_logger/appenders.rb +2 -1
- data/lib/semantic_logger/base.rb +0 -2
- data/lib/semantic_logger/formatters/color.rb +5 -2
- data/lib/semantic_logger/formatters/default.rb +7 -2
- data/lib/semantic_logger/formatters/logfmt.rb +4 -2
- data/lib/semantic_logger/formatters/pattern.rb +3 -1
- data/lib/semantic_logger/formatters/syslog.rb +2 -1
- data/lib/semantic_logger/formatters/syslog_cee.rb +2 -1
- data/lib/semantic_logger/metric/signalfx.rb +4 -2
- data/lib/semantic_logger/queue_processor.rb +5 -2
- data/lib/semantic_logger/reporters/minitest.rb +4 -4
- data/lib/semantic_logger/test/capture_log_events.rb +1 -1
- data/lib/semantic_logger/version.rb +1 -1
- metadata +14 -3
data/docs/security.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Security
|
|
6
|
+
{:.no_toc}
|
|
7
|
+
|
|
8
|
+
**Contents**
|
|
9
|
+
|
|
10
|
+
* TOC
|
|
11
|
+
{:toc}
|
|
12
|
+
|
|
13
|
+
Logging frameworks sit on a sensitive boundary: they take data from all over an
|
|
14
|
+
application, including data that originated from untrusted users, and write it to
|
|
15
|
+
files, terminals, and centralized logging systems. This page describes the security
|
|
16
|
+
properties of Semantic Logger and how to configure it when logging untrusted data.
|
|
17
|
+
|
|
18
|
+
### Log injection and forging
|
|
19
|
+
|
|
20
|
+
When a log message, tag, or exception message contains untrusted, attacker-controlled
|
|
21
|
+
data (for example a user name, request parameter, or `User-Agent` header), control
|
|
22
|
+
characters in that data can be abused:
|
|
23
|
+
|
|
24
|
+
* A newline can forge an additional, fake log entry ("log forging"), or split one
|
|
25
|
+
record into two at a collector that frames records with a separator.
|
|
26
|
+
* An ANSI escape sequence can spoof or hide terminal output when the log is viewed in
|
|
27
|
+
a terminal.
|
|
28
|
+
|
|
29
|
+
The structured formatters, such as `:json`, are **not** affected, because JSON encoding
|
|
30
|
+
always escapes control characters. They are the recommended choice when forwarding logs
|
|
31
|
+
that may contain untrusted data to a centralized logging system.
|
|
32
|
+
|
|
33
|
+
By design the human readable text formatters (`:default` and `:color`) preserve
|
|
34
|
+
newlines and ANSI color codes, since multi-line and colorized output is useful when
|
|
35
|
+
reading logs locally. When the text output may contain untrusted data, enable the
|
|
36
|
+
`escape_control_chars` option to replace control characters with a printable, escaped
|
|
37
|
+
form (for example a newline becomes `\n`):
|
|
38
|
+
|
|
39
|
+
~~~ruby
|
|
40
|
+
SemanticLogger.add_appender(file_name: "production.log", formatter: {default: {escape_control_chars: true}})
|
|
41
|
+
~~~
|
|
42
|
+
|
|
43
|
+
See [Escaping Control Characters](config.html#escaping-control-characters) for
|
|
44
|
+
details.
|
|
45
|
+
|
|
46
|
+
The record-framed network appenders are already safe by default:
|
|
47
|
+
|
|
48
|
+
* The **TCP** and **UDP** appenders default to the JSON formatter.
|
|
49
|
+
* The **Syslog** formatters escape control characters by default, since syslog frames
|
|
50
|
+
each record.
|
|
51
|
+
|
|
52
|
+
If you replace the formatter on any of these appenders with a text formatter, enable
|
|
53
|
+
`escape_control_chars` as shown above.
|
|
54
|
+
|
|
55
|
+
### Redacting sensitive data
|
|
56
|
+
|
|
57
|
+
Semantic Logger does not automatically redact secrets or personal information; it logs
|
|
58
|
+
the message and payload it is given. Avoid logging passwords, tokens, full credit card
|
|
59
|
+
numbers, and similar values in the first place.
|
|
60
|
+
|
|
61
|
+
When sensitive values can reach the logs indirectly, redact them. A global `on_log`
|
|
62
|
+
subscriber runs inline, once per log event, before the event is handed to any appender,
|
|
63
|
+
so it can scrub the message or payload for every destination:
|
|
64
|
+
|
|
65
|
+
~~~ruby
|
|
66
|
+
SECRET_KEYS = %i[password token authorization secret].freeze
|
|
67
|
+
|
|
68
|
+
SemanticLogger.on_log do |log|
|
|
69
|
+
log.payload&.each_key do |key|
|
|
70
|
+
log.payload[key] = "[REDACTED]" if SECRET_KEYS.include?(key)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
~~~
|
|
74
|
+
|
|
75
|
+
Redaction can also be applied to a single logger or appender with a `filter` that
|
|
76
|
+
mutates the log and returns `true`. See [Filtering](config.html#filtering) for examples.
|
|
77
|
+
|
|
78
|
+
Rails applications should use the sister gem
|
|
79
|
+
[rails_semantic_logger](https://github.com/reidmorrison/rails_semantic_logger), which
|
|
80
|
+
honours `config.filter_parameters` so that parameters Rails already considers sensitive
|
|
81
|
+
are filtered out of the logs.
|
|
82
|
+
|
|
83
|
+
### Transport encryption
|
|
84
|
+
|
|
85
|
+
Appenders that send logs over the network support TLS. The appenders built on the
|
|
86
|
+
`:http` appender (`:http`, `:splunk_http`, and `:elasticsearch_http`) default to
|
|
87
|
+
verifying the server certificate (`OpenSSL::SSL::VERIFY_PEER`) when an `https` URL is
|
|
88
|
+
used.
|
|
89
|
+
|
|
90
|
+
Do not disable certificate verification in production. Settings such as
|
|
91
|
+
`ssl: {verify_mode: OpenSSL::SSL::VERIFY_NONE}` expose the connection to
|
|
92
|
+
man-in-the-middle attacks and should be limited to local development against
|
|
93
|
+
self-signed certificates.
|
|
94
|
+
|
|
95
|
+
### Log file permissions
|
|
96
|
+
|
|
97
|
+
Log files frequently contain sensitive information. By default the file appender
|
|
98
|
+
creates files using the process umask, the standard Ruby behavior. To restrict access,
|
|
99
|
+
supply the `permissions` option, which is applied both when the file is created and to
|
|
100
|
+
an existing log file:
|
|
101
|
+
|
|
102
|
+
~~~ruby
|
|
103
|
+
# Owner read/write, group read, no access for others:
|
|
104
|
+
SemanticLogger.add_appender(file_name: "production.log", permissions: 0o640)
|
|
105
|
+
~~~
|
|
106
|
+
|
|
107
|
+
### Dependencies and supply chain
|
|
108
|
+
|
|
109
|
+
Semantic Logger has a single runtime dependency, `concurrent-ruby`. Appenders for
|
|
110
|
+
third-party services (Kafka, MongoDB, Sentry, etc.) keep their backing gem optional: it
|
|
111
|
+
is required lazily inside the appender only when that appender is used, and is never
|
|
112
|
+
added to the gemspec. This keeps the dependency surface of an application that only uses
|
|
113
|
+
the built-in file and IO appenders very small.
|
|
114
|
+
|
|
115
|
+
Auditing the resolved dependency versions for known advisories is the responsibility of
|
|
116
|
+
the consuming application, which should run a tool such as
|
|
117
|
+
[bundler-audit](https://github.com/rubysec/bundler-audit) against its own
|
|
118
|
+
`Gemfile.lock`. Pinning dependencies with a committed lockfile, and verifying it in CI
|
|
119
|
+
with `bundle install --frozen`, prevents an unexpected dependency from being pulled in.
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Testing
|
|
6
|
+
{:.no_toc}
|
|
7
|
+
|
|
8
|
+
**Contents**
|
|
9
|
+
|
|
10
|
+
* TOC
|
|
11
|
+
{:toc}
|
|
12
|
+
|
|
13
|
+
When testing application code, you often want to assert that it logged the right thing: the expected
|
|
14
|
+
message, level, payload, or metric. Semantic Logger logs through a global, asynchronous pipeline, so
|
|
15
|
+
you cannot simply read it back from a normal appender. Instead it ships a test helper that **captures
|
|
16
|
+
log events in memory** during a block, so you can make assertions on them.
|
|
17
|
+
|
|
18
|
+
The events are captured as raw `SemanticLogger::Log` objects, before any appender or formatter runs,
|
|
19
|
+
so your assertions are not affected by how logging happens to be configured.
|
|
20
|
+
|
|
21
|
+
Helpers are provided for both Minitest and RSpec. Other frameworks are covered further down.
|
|
22
|
+
|
|
23
|
+
## Minitest
|
|
24
|
+
|
|
25
|
+
### Step 1: install the helpers
|
|
26
|
+
|
|
27
|
+
Add the helper methods to your tests, once, in `test_helper.rb`:
|
|
28
|
+
|
|
29
|
+
~~~ruby
|
|
30
|
+
Minitest::Test.include SemanticLogger::Test::Minitest
|
|
31
|
+
~~~
|
|
32
|
+
|
|
33
|
+
### Step 2: capture events
|
|
34
|
+
|
|
35
|
+
Wrap the code under test in `semantic_logger_events`. It returns every log event created during the
|
|
36
|
+
block:
|
|
37
|
+
|
|
38
|
+
~~~ruby
|
|
39
|
+
messages = semantic_logger_events do
|
|
40
|
+
User.new.enable!
|
|
41
|
+
end
|
|
42
|
+
~~~
|
|
43
|
+
|
|
44
|
+
By default it captures events from every class. To capture only the events from one class, pass that
|
|
45
|
+
class:
|
|
46
|
+
|
|
47
|
+
~~~ruby
|
|
48
|
+
messages = semantic_logger_events(ApiClient) do
|
|
49
|
+
# Only ApiClient log events created during this block are captured.
|
|
50
|
+
end
|
|
51
|
+
~~~
|
|
52
|
+
|
|
53
|
+
### Step 3: assert on the events
|
|
54
|
+
|
|
55
|
+
Check how many events were produced, then assert on each one with `assert_semantic_logger_event`:
|
|
56
|
+
|
|
57
|
+
~~~ruby
|
|
58
|
+
require_relative "test_helper"
|
|
59
|
+
|
|
60
|
+
class UserTest < ActiveSupport::TestCase
|
|
61
|
+
describe User do
|
|
62
|
+
it "logs message" do
|
|
63
|
+
messages = semantic_logger_events do
|
|
64
|
+
User.new.enable!
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# How many events were logged
|
|
68
|
+
assert_equal 2, messages.count, messages
|
|
69
|
+
|
|
70
|
+
# The first event
|
|
71
|
+
assert_semantic_logger_event(
|
|
72
|
+
messages[0],
|
|
73
|
+
level: :info,
|
|
74
|
+
message: "User enabled"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# The second event
|
|
78
|
+
assert_semantic_logger_event(
|
|
79
|
+
messages[1],
|
|
80
|
+
level: :debug,
|
|
81
|
+
message: "Completed"
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
~~~
|
|
87
|
+
|
|
88
|
+
Every argument other than the event itself is optional, so you assert only on what matters to the
|
|
89
|
+
test. The available checks are:
|
|
90
|
+
|
|
91
|
+
| Argument | Checks |
|
|
92
|
+
|----------|--------|
|
|
93
|
+
| `message` | Exact match of the text message. |
|
|
94
|
+
| `message_includes` | Partial (substring) match of the message. |
|
|
95
|
+
| `level` | Log level: `:trace`, `:debug`, `:info`, `:warn`, `:error`, `:fatal`. |
|
|
96
|
+
| `payload` | Exact match of the payload Hash. |
|
|
97
|
+
| `payload_includes` | Partial match: the given keys/values, ignoring any others. |
|
|
98
|
+
| `tags` | Tags active on the thread when logged. |
|
|
99
|
+
| `named_tags` | Named tags active on the thread when logged. |
|
|
100
|
+
| `name` | Class name of the logger. |
|
|
101
|
+
| `exception` | The Ruby exception that was logged. |
|
|
102
|
+
| `metric` | The metric name. |
|
|
103
|
+
| `metric_amount` | The metric amount. |
|
|
104
|
+
| `dimensions` | The metric dimensions. |
|
|
105
|
+
| `context` | Named contexts captured with the entry. |
|
|
106
|
+
| `time` | When the entry was created. |
|
|
107
|
+
| `duration` | Duration of a measure call, in milliseconds. |
|
|
108
|
+
| `backtrace` | The captured backtrace, if any. |
|
|
109
|
+
| `thread_name` | Name of the thread that logged the entry. |
|
|
110
|
+
|
|
111
|
+
### Match part of a message or payload
|
|
112
|
+
|
|
113
|
+
For assertions that should not break on incidental detail, match partially.
|
|
114
|
+
|
|
115
|
+
Use `message_includes` for a substring of the message:
|
|
116
|
+
|
|
117
|
+
~~~ruby
|
|
118
|
+
assert_semantic_logger_event(
|
|
119
|
+
messages[0],
|
|
120
|
+
level: :info,
|
|
121
|
+
message_includes: "enabled"
|
|
122
|
+
)
|
|
123
|
+
~~~
|
|
124
|
+
|
|
125
|
+
Use `payload_includes` to assert specific payload keys while ignoring any extras. Compare with
|
|
126
|
+
`payload`, which must match the whole Hash exactly:
|
|
127
|
+
|
|
128
|
+
~~~ruby
|
|
129
|
+
# Exact: the payload must be exactly these keys and values
|
|
130
|
+
assert_semantic_logger_event(
|
|
131
|
+
messages[0],
|
|
132
|
+
level: :info,
|
|
133
|
+
message: "User enabled",
|
|
134
|
+
payload: {first_name: "Jack", last_name: "Jones"}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Partial: first_name must be present, other keys are ignored
|
|
138
|
+
assert_semantic_logger_event(
|
|
139
|
+
messages[0],
|
|
140
|
+
level: :info,
|
|
141
|
+
message: "User enabled",
|
|
142
|
+
payload_includes: {first_name: "Jack"}
|
|
143
|
+
)
|
|
144
|
+
~~~
|
|
145
|
+
|
|
146
|
+
For more examples, see the
|
|
147
|
+
[Rails Semantic Logger tests](https://github.com/reidmorrison/rails_semantic_logger/blob/main/test/active_record_test.rb).
|
|
148
|
+
|
|
149
|
+
## RSpec
|
|
150
|
+
|
|
151
|
+
The RSpec helpers mirror the Minitest ones: a capture helper plus matchers for asserting on the
|
|
152
|
+
captured events.
|
|
153
|
+
|
|
154
|
+
### Step 1: install the helpers
|
|
155
|
+
|
|
156
|
+
Require the helpers and include them, once, in `spec_helper.rb`:
|
|
157
|
+
|
|
158
|
+
~~~ruby
|
|
159
|
+
require "semantic_logger/test/rspec"
|
|
160
|
+
|
|
161
|
+
RSpec.configure do |config|
|
|
162
|
+
config.include SemanticLogger::Test::RSpec
|
|
163
|
+
end
|
|
164
|
+
~~~
|
|
165
|
+
|
|
166
|
+
### Step 2: capture events
|
|
167
|
+
|
|
168
|
+
Wrap the code under test in `capture_semantic_logger_events`. It returns every log event created
|
|
169
|
+
during the block, regardless of the global default log level:
|
|
170
|
+
|
|
171
|
+
~~~ruby
|
|
172
|
+
events = capture_semantic_logger_events do
|
|
173
|
+
User.new.enable!
|
|
174
|
+
end
|
|
175
|
+
~~~
|
|
176
|
+
|
|
177
|
+
By default it captures events from every class. To capture only the events from one class, pass that
|
|
178
|
+
class:
|
|
179
|
+
|
|
180
|
+
~~~ruby
|
|
181
|
+
events = capture_semantic_logger_events(ApiClient) do
|
|
182
|
+
# Only ApiClient log events created during this block are captured.
|
|
183
|
+
end
|
|
184
|
+
~~~
|
|
185
|
+
|
|
186
|
+
### Step 3: assert on the events
|
|
187
|
+
|
|
188
|
+
Use `be_a_semantic_logger_event` to assert on a single captured event:
|
|
189
|
+
|
|
190
|
+
~~~ruby
|
|
191
|
+
RSpec.describe User do
|
|
192
|
+
it "logs message" do
|
|
193
|
+
events = capture_semantic_logger_events do
|
|
194
|
+
User.new.enable!
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
expect(events.count).to eq(2)
|
|
198
|
+
|
|
199
|
+
expect(events[0]).to be_a_semantic_logger_event(
|
|
200
|
+
level: :info,
|
|
201
|
+
message: "User enabled"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
expect(events[1]).to be_a_semantic_logger_event(
|
|
205
|
+
level: :debug,
|
|
206
|
+
message: "Completed"
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
~~~
|
|
211
|
+
|
|
212
|
+
Every argument is optional, so you assert only on what matters to the test. The available checks are
|
|
213
|
+
the same as the [Minitest table above](#step-3-assert-on-the-events), plus `message_includes`,
|
|
214
|
+
`payload_includes`, and `exception_includes` for partial matches:
|
|
215
|
+
|
|
216
|
+
~~~ruby
|
|
217
|
+
expect(events[0]).to be_a_semantic_logger_event(
|
|
218
|
+
level: :info,
|
|
219
|
+
message_includes: "enabled",
|
|
220
|
+
payload_includes: {first_name: "Jack"}
|
|
221
|
+
)
|
|
222
|
+
~~~
|
|
223
|
+
|
|
224
|
+
An expected value that is a Class matches by type, and `:nil` asserts the attribute is `nil`:
|
|
225
|
+
|
|
226
|
+
~~~ruby
|
|
227
|
+
expect(events[0]).to be_a_semantic_logger_event(time: Time, exception: :nil)
|
|
228
|
+
~~~
|
|
229
|
+
|
|
230
|
+
### Match against a list of events
|
|
231
|
+
|
|
232
|
+
`a_semantic_logger_event` is a composable alias, so it works inside `include` and other matchers:
|
|
233
|
+
|
|
234
|
+
~~~ruby
|
|
235
|
+
expect(events).to include(
|
|
236
|
+
a_semantic_logger_event(message: "User enabled")
|
|
237
|
+
)
|
|
238
|
+
~~~
|
|
239
|
+
|
|
240
|
+
### Assert directly on a block
|
|
241
|
+
|
|
242
|
+
`log_semantic_logger_event` captures the events for you and passes if any of them match. Pass `on:`
|
|
243
|
+
to capture only one class's events:
|
|
244
|
+
|
|
245
|
+
~~~ruby
|
|
246
|
+
expect { User.new.enable! }.to(
|
|
247
|
+
log_semantic_logger_event(level: :info, message: "User enabled")
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect { ApiClient.new.call }.to(
|
|
251
|
+
log_semantic_logger_event(on: ApiClient, metric: "ApiClient/call")
|
|
252
|
+
)
|
|
253
|
+
~~~
|
|
254
|
+
|
|
255
|
+
## Other test frameworks
|
|
256
|
+
|
|
257
|
+
The same approach works anywhere: replace the logger with a
|
|
258
|
+
`SemanticLogger::Test::CaptureLogEvents` instance. It looks and behaves like a normal logger, but
|
|
259
|
+
keeps the raw log events in memory instead of writing them, so your assertions are unaffected by the
|
|
260
|
+
configured appenders or their formats.
|
|
261
|
+
|
|
262
|
+
~~~ruby
|
|
263
|
+
logger = SemanticLogger::Test::CaptureLogEvents.new
|
|
264
|
+
~~~
|
|
265
|
+
|
|
266
|
+
Stub it onto a single class to capture just that class's logging:
|
|
267
|
+
|
|
268
|
+
~~~ruby
|
|
269
|
+
User.stub(:logger, logger) do
|
|
270
|
+
# Capture all logging calls to the User logger.
|
|
271
|
+
end
|
|
272
|
+
~~~
|
|
273
|
+
|
|
274
|
+
Or stub the global processor to capture logging from every class:
|
|
275
|
+
|
|
276
|
+
~~~ruby
|
|
277
|
+
SemanticLogger::Logger.stub(:processor, logger) do
|
|
278
|
+
# Capture all log events during the block.
|
|
279
|
+
end
|
|
280
|
+
~~~
|
|
281
|
+
|
|
282
|
+
Either way, the captured events are available as `logger.events`.
|
|
283
|
+
|
|
284
|
+
If you add helper methods for another framework like the Minitest ones, a pull request would be
|
|
285
|
+
welcome.
|