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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -0
  3. data/Rakefile +29 -0
  4. data/docs/api.md +480 -0
  5. data/docs/appenders.md +990 -0
  6. data/docs/config.md +605 -0
  7. data/docs/index.md +197 -0
  8. data/docs/log.md +108 -0
  9. data/docs/metrics.md +168 -0
  10. data/docs/operations.md +400 -0
  11. data/docs/rails.md +816 -0
  12. data/docs/security.md +119 -0
  13. data/docs/testing.md +285 -0
  14. data/docs/upgrading.md +409 -0
  15. data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -2
  16. data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
  17. data/lib/semantic_logger/appender/elasticsearch_base.rb +17 -17
  18. data/lib/semantic_logger/appender/elasticsearch_http.rb +14 -7
  19. data/lib/semantic_logger/appender/file.rb +5 -0
  20. data/lib/semantic_logger/appender/new_relic.rb +6 -0
  21. data/lib/semantic_logger/appender/new_relic_logs.rb +2 -1
  22. data/lib/semantic_logger/appender/open_telemetry.rb +2 -1
  23. data/lib/semantic_logger/appender/opensearch.rb +2 -1
  24. data/lib/semantic_logger/appender/rabbitmq.rb +4 -3
  25. data/lib/semantic_logger/appender/sentry.rb +5 -0
  26. data/lib/semantic_logger/appender/syslog.rb +6 -3
  27. data/lib/semantic_logger/appender/tcp.rb +4 -3
  28. data/lib/semantic_logger/appender/wrapper.rb +2 -1
  29. data/lib/semantic_logger/appender.rb +6 -3
  30. data/lib/semantic_logger/appenders.rb +2 -1
  31. data/lib/semantic_logger/base.rb +0 -2
  32. data/lib/semantic_logger/formatters/color.rb +5 -2
  33. data/lib/semantic_logger/formatters/default.rb +7 -2
  34. data/lib/semantic_logger/formatters/logfmt.rb +4 -2
  35. data/lib/semantic_logger/formatters/pattern.rb +3 -1
  36. data/lib/semantic_logger/formatters/syslog.rb +2 -1
  37. data/lib/semantic_logger/formatters/syslog_cee.rb +2 -1
  38. data/lib/semantic_logger/metric/signalfx.rb +4 -2
  39. data/lib/semantic_logger/queue_processor.rb +5 -2
  40. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  41. data/lib/semantic_logger/test/capture_log_events.rb +1 -1
  42. data/lib/semantic_logger/version.rb +1 -1
  43. 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.