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/appenders.md ADDED
@@ -0,0 +1,990 @@
1
+ ---
2
+ layout: default
3
+ ---
4
+
5
+ ## Appenders
6
+ {:.no_toc}
7
+
8
+ **Contents**
9
+
10
+ * TOC
11
+ {:toc}
12
+
13
+ An **appender** is a destination that log entries are written to: a file, the screen, a database, or
14
+ a remote service. Semantic Logger can write to several appenders at the same time, each with its own
15
+ log level and format, all from the same log call. For example you might log colorized text to the
16
+ screen, JSON to a file, and errors to an external service simultaneously.
17
+
18
+ ## Step 1: Add a destination
19
+
20
+ Every destination is added through one method, `SemanticLogger.add_appender`, usually once at startup.
21
+ The keyword you pass selects the kind of destination:
22
+
23
+ ~~~ruby
24
+ # A text file
25
+ SemanticLogger.add_appender(file_name: "development.log")
26
+
27
+ # An IO stream, such as the screen
28
+ SemanticLogger.add_appender(io: $stdout)
29
+
30
+ # A built-in appender, selected by name
31
+ SemanticLogger.add_appender(appender: :elasticsearch, url: "http://localhost:9200")
32
+
33
+ # An existing Ruby or Rails logger
34
+ SemanticLogger.add_appender(logger: Logger.new($stdout))
35
+
36
+ # A metrics destination (see the Metrics guide)
37
+ SemanticLogger.add_appender(metric: :statsd, url: "udp://localhost:8125")
38
+ ~~~
39
+
40
+ In a Rails app using [rails_semantic_logger](rails.html), put `add_appender` calls in an initializer
41
+ such as `config/initializers/semantic_logger.rb`. Otherwise add them wherever you configure Semantic
42
+ Logger when the application boots.
43
+
44
+ ## Step 2: Set the level, format, and filter
45
+
46
+ In addition to its own settings, almost every appender accepts these common options, so each
47
+ destination can keep a different subset of the logs in a different format:
48
+
49
+ | Option | Description |
50
+ |--------|-------------|
51
+ | `level` | Only write entries at this level or higher to this appender. Defaults to `SemanticLogger.default_level`. |
52
+ | `formatter` | How to format the output, for example `:default`, `:color`, or `:json`. See [Custom formatters](config.html#custom-formatters). |
53
+ | `filter` | A `Regexp` or `Proc` selecting which entries this appender accepts. See [Filtering](config.html#filtering). |
54
+ | `application`, `environment`, `host` | Override the global values for this appender only. |
55
+
56
+ For example, write only warnings and above to a file, formatted as JSON:
57
+
58
+ ~~~ruby
59
+ SemanticLogger.add_appender(file_name: "errors.log", level: :warn, formatter: :json)
60
+ ~~~
61
+
62
+ ## Step 3: Pick your destinations
63
+
64
+ The tables below list every built-in destination. Pick the ones you need and jump to their section
65
+ for a full example.
66
+
67
+ Appenders for third-party services need their backing gem (shown in the "Gem" column). Add it to your
68
+ `Gemfile` and run `bundle install`, or `gem install` it directly. The gem is loaded lazily the first
69
+ time the appender is used, and is never a hard dependency of Semantic Logger itself.
70
+
71
+ **Files and streams**
72
+
73
+ | Destination | `add_appender` argument | Gem |
74
+ |-------------|-------------------------|-----|
75
+ | [Text file](#text-file) | `file_name:` | |
76
+ | [IO stream](#io-streams) (`$stdout`, `$stderr`, ...) | `io:` | |
77
+ | [Another Ruby or Rails logger](#logger-log4r-etc) | `logger:` | |
78
+
79
+ **Network protocols**
80
+
81
+ | Destination | `add_appender` argument | Gem |
82
+ |-------------|-------------------------|-----|
83
+ | [HTTP(S)](#https) | `appender: :http` | |
84
+ | [TCP (+SSL)](#tcp-appender-ssl) | `appender: :tcp` | `net_tcp_client` |
85
+ | [UDP](#udp-appender) | `appender: :udp` | |
86
+ | [Syslog](#syslog) | `appender: :syslog` | `syslog_protocol`, `net_tcp_client` (remote) |
87
+
88
+ **Centralized logging and log aggregators**
89
+
90
+ | Destination | `add_appender` argument | Gem |
91
+ |-------------|-------------------------|-----|
92
+ | [Elasticsearch](#elasticsearch) | `appender: :elasticsearch` | `elasticsearch` |
93
+ | [OpenSearch](#opensearch) | `appender: :opensearch` | `opensearch-ruby` |
94
+ | [Graylog](#graylog) | `appender: :graylog` | `gelf` |
95
+ | [Splunk over HTTP](#splunk-http) | `appender: :splunk_http` | |
96
+ | [Splunk SDK](#splunk-sdk) | `appender: :splunk` | `splunk-sdk-ruby` |
97
+ | [Grafana Loki](#grafana-loki) | `appender: :loki` | |
98
+ | [CloudWatch Logs](#cloudwatch-logs) | `appender: :cloudwatch_logs` | `aws-sdk-cloudwatchlogs` |
99
+ | [OpenTelemetry](#opentelemetry) | `appender: :open_telemetry` | `opentelemetry-logs-sdk` |
100
+ | [Logstash](#logstash) | `logger:` | `logstash-logger` |
101
+ | [logentries.com](#logentriescom) | `appender: :tcp` | `net_tcp_client` |
102
+ | [loggly.com](#logglycom) | `appender: :http` | |
103
+ | [Papertrail](#papertrail) | `appender: :syslog` | `syslog_protocol`, `net_tcp_client` |
104
+
105
+ **Error and exception monitoring**
106
+
107
+ | Destination | `add_appender` argument | Gem |
108
+ |-------------|-------------------------|-----|
109
+ | [Bugsnag](#bugsnag) | `appender: :bugsnag` | `bugsnag` |
110
+ | [Sentry](#sentry) | `appender: :sentry_ruby` | `sentry-ruby` |
111
+ | [Honeybadger](#honeybadger-and-honeybadger-insights) | `appender: :honeybadger` | `honeybadger` |
112
+ | [Honeybadger Insights](#honeybadger-and-honeybadger-insights) | `appender: :honeybadger_insights` | `honeybadger` |
113
+ | [New Relic](#newrelic) | `appender: :new_relic` | `newrelic_rpm` |
114
+ | [Rollbar](#rollbar) | via `SemanticLogger.on_log` | `rollbar` |
115
+
116
+ **Databases and message queues**
117
+
118
+ | Destination | `add_appender` argument | Gem |
119
+ |-------------|-------------------------|-----|
120
+ | [MongoDB](#mongodb) | `appender: :mongodb` | `mongo` |
121
+ | [Apache Kafka](#apache-kafka) | `appender: :kafka` | `ruby-kafka` |
122
+ | [RabbitMQ](#rabbitmq-amqp) | `appender: :rabbitmq` | `bunny` |
123
+
124
+ For metrics destinations such as Statsd, SignalFx, and New Relic, see [Metrics](metrics.html).
125
+
126
+ > **Tip:** To ensure no log messages are lost, prefer TCP over UDP. Because of Semantic Logger's
127
+ > asynchronous design, the performance difference between the two will not impact your application.
128
+
129
+ ## Files and streams
130
+
131
+ ### Text File
132
+
133
+ Log to a file, choosing a formatter to suit the reader:
134
+
135
+ ~~~ruby
136
+ # Standard text:
137
+ SemanticLogger.add_appender(file_name: "development.log")
138
+
139
+ # Colorized text, for a terminal:
140
+ SemanticLogger.add_appender(file_name: "development.log", formatter: :color)
141
+
142
+ # JSON, for a machine:
143
+ SemanticLogger.add_appender(file_name: "development.log", formatter: :json)
144
+ ~~~
145
+
146
+ The file is opened when the appender is added, so a bad path or insufficient permissions raise
147
+ immediately from `add_appender` instead of failing later on the logging thread.
148
+
149
+ For performance the log file is not re-opened on every call, so rotate it with a copy-truncate
150
+ operation rather than deleting the file. See [Log rotation](operations.html#log-rotation).
151
+
152
+ Log files frequently contain sensitive information. By default the file is created using the process
153
+ umask (the standard Ruby behavior). To restrict access, supply `permissions:`, applied both when the
154
+ file is created and to an existing log file:
155
+
156
+ ~~~ruby
157
+ # Owner read/write, group read, no access for others:
158
+ SemanticLogger.add_appender(file_name: "production.log", permissions: 0o640)
159
+ ~~~
160
+
161
+ ### IO Streams
162
+
163
+ Log to any IO stream instance, such as `$stdout` or `$stderr`:
164
+
165
+ ~~~ruby
166
+ # Log errors and above to standard error:
167
+ SemanticLogger.add_appender(io: $stderr, level: :error)
168
+ ~~~
169
+
170
+ #### Splitting output across stdout and stderr
171
+
172
+ A common pattern routes lower severity entries to `$stdout` and warnings and errors to `$stderr`.
173
+ Semantic Logger allows one appender per console stream, so add one for each and use `level:` and/or
174
+ `filter:` to control what each writes:
175
+
176
+ ~~~ruby
177
+ stdout_filter = ->(log) { %i[trace debug info].include?(log.level) }
178
+
179
+ # Informational messages to stdout:
180
+ SemanticLogger.add_appender(io: $stdout, formatter: :color, level: :trace, filter: stdout_filter)
181
+
182
+ # Warnings and above to stderr:
183
+ SemanticLogger.add_appender(io: $stderr, formatter: :color, level: :warn)
184
+ ~~~
185
+
186
+ Adding a second appender for a console stream that already has one is ignored (to avoid duplicate
187
+ console output), but `$stdout` and `$stderr` are tracked separately.
188
+
189
+ ### Logger, log4r, etc.
190
+
191
+ Semantic Logger can write to another logging library, either to gain the Semantic Logger interface
192
+ while still writing to an existing destination, or to reach a destination it does not support natively:
193
+
194
+ ~~~ruby
195
+ ruby_logger = Logger.new($stdout)
196
+
197
+ # Log to an existing Ruby Logger instance
198
+ SemanticLogger.add_appender(logger: ruby_logger)
199
+ ~~~
200
+
201
+ Note: `:trace` level messages are mapped to `:debug`.
202
+
203
+ ## Structured output formats
204
+
205
+ The file, IO, and HTTP appenders can emit machine-readable output by choosing a structured
206
+ `formatter`. Each format below produces a single line per entry.
207
+
208
+ ### JSON
209
+
210
+ `formatter: :json` produces output with this layout:
211
+
212
+ ~~~json
213
+ {
214
+ "timestamp": "ISO-8601",
215
+
216
+ "application": "Application name",
217
+ "environment": "Custom Environment name",
218
+ "host": "Host name",
219
+ "pid": "Process Id",
220
+ "thread": "Thread name or id",
221
+ "file": "filename",
222
+ "line": "line number",
223
+
224
+ "level": "trace|debug|info|warn|error|fatal",
225
+ "level_index": "0|1|2|3|4|5",
226
+ "message": "The message text without any colorization",
227
+ "name": "Name of the class that generated the log message. Including namespace, if any.",
228
+ "tags": ["tag_name 1", "tag_name 2"],
229
+ "duration": "Human readable duration",
230
+ "duration_ms": "Duration in milliseconds",
231
+ "metric": "Name of the metric",
232
+ "metric_amount": "Size of the metric, usually 1",
233
+
234
+ "named_tags": {
235
+ "tag1": "any named tags will be inside this named_tags tag",
236
+ "tag2": "any named tags will be inside this named_tags tag"
237
+ },
238
+
239
+ "payload": {
240
+ "field1": "any custom payload fields will be inside this payload tag",
241
+ "field2": "any custom payload fields will be inside this payload tag"
242
+ },
243
+
244
+ "exception": {
245
+ "name": "Exception class name",
246
+ "message": "Exception message",
247
+ "stack_trace": ["line 1", "line 2"],
248
+ "cause": {
249
+ "name": "Exception class name",
250
+ "message": "Exception message",
251
+ "stack_trace": ["line 1", "line 2"]
252
+ }
253
+ }
254
+ }
255
+ ~~~
256
+
257
+ Notes:
258
+
259
+ * The layout above is formatted for readability. The real output is a single line terminated by one
260
+ newline, with no embedded newlines.
261
+ * A field with a `nil` value is excluded from the output.
262
+
263
+ ### Fluentd
264
+
265
+ `formatter: :fluentd` is the same as `:json`, except it renames the log level fields to `severity` and
266
+ `severity_index` so they are recognized by the Kubernetes Fluentd log collector:
267
+
268
+ ~~~ruby
269
+ SemanticLogger.add_appender(io: $stdout, formatter: :fluentd)
270
+ ~~~
271
+
272
+ Differences from `:json`:
273
+
274
+ * `level` / `level_index` become `severity` / `severity_index`.
275
+ * `host` is excluded by default (under Fluentd it is usually the not-very-useful container id). Pass
276
+ `log_host: true` to include it.
277
+ * The process fields `pid`, `thread`, `file`, and `line` are excluded by default. Pass
278
+ `need_process_info: true` to include them.
279
+
280
+ Construct the formatter explicitly to override these defaults:
281
+
282
+ ~~~ruby
283
+ formatter = SemanticLogger::Formatters::Fluentd.new(log_host: true, need_process_info: true)
284
+ SemanticLogger.add_appender(io: $stdout, formatter: formatter)
285
+ ~~~
286
+
287
+ ### ECS (Elastic Common Schema)
288
+
289
+ `formatter: :ecs` emits each entry using the nested field names of the
290
+ [Elastic Common Schema](https://www.elastic.co/docs/reference/ecs) (targeting ECS 8.x), so logs
291
+ integrate cleanly with Filebeat and the Elastic stack without an ingest pipeline to rename fields. The
292
+ typical deployment writes ECS JSON to stdout or a file and lets Filebeat or Elastic Agent ship it to
293
+ Elasticsearch:
294
+
295
+ ~~~ruby
296
+ SemanticLogger.add_appender(io: $stdout, formatter: :ecs)
297
+ SemanticLogger.add_appender(file_name: "production.log", formatter: :ecs)
298
+ ~~~
299
+
300
+ Semantic Logger fields map to ECS as follows:
301
+
302
+ | Semantic Logger | ECS |
303
+ | :--- | :--- |
304
+ | `time` | `@timestamp` |
305
+ | `level` | `log.level` |
306
+ | `name` | `log.logger` |
307
+ | `file` / `line` | `log.origin.file.name` / `log.origin.file.line` |
308
+ | `message` | `message` |
309
+ | `thread` | `process.thread.name` |
310
+ | `pid` | `process.pid` |
311
+ | `host` | `host.hostname` |
312
+ | `application` | `service.name` |
313
+ | `environment` | `service.environment` |
314
+ | `exception` | `error.type` / `error.message` / `error.stack_trace` |
315
+ | `duration` | `event.duration` (nanoseconds) |
316
+ | `tags` | `tags` |
317
+ | `named_tags` | `labels.*` |
318
+ | `payload`, `metric`, `metric_amount` | nested under a custom namespace (see below) |
319
+
320
+ ECS reserves the top-level field names it defines, so Semantic Logger data with no native ECS home
321
+ (`payload`, `metric`, and `metric_amount`) is nested under a custom top-level namespace,
322
+ `semantic_logger` by default. A proper-noun namespace is
323
+ [the approach ECS recommends](https://www.elastic.co/docs/reference/ecs/ecs-custom-fields-in-ecs) for
324
+ custom fields, since it never collides with a current or future ECS field. Rename it with
325
+ `namespace:`:
326
+
327
+ ~~~ruby
328
+ formatter = SemanticLogger::Formatters::Ecs.new(namespace: "my_app")
329
+ SemanticLogger.add_appender(io: $stdout, formatter: formatter)
330
+ ~~~
331
+
332
+ Or set `namespace: nil` to merge the payload directly into ECS `labels` alongside the named tags:
333
+
334
+ ~~~ruby
335
+ formatter = SemanticLogger::Formatters::Ecs.new(namespace: nil)
336
+ SemanticLogger.add_appender(io: $stdout, formatter: formatter)
337
+ ~~~
338
+
339
+ ### Logfmt
340
+
341
+ `formatter: :logfmt` emits each entry as a single line of `key=value` pairs in
342
+ [logfmt](https://brandur.org/logfmt) format, common with Heroku and Grafana Loki tooling:
343
+
344
+ ~~~ruby
345
+ SemanticLogger.add_appender(io: $stdout, formatter: :logfmt)
346
+ ~~~
347
+
348
+ ~~~
349
+ timestamp="2024-07-20T08:32:05.375276Z" level="info" name="MyClass" message="Hello World" tag="success"
350
+ ~~~
351
+
352
+ Notes:
353
+
354
+ * The timestamp is ISO 8601. All values are quoted and escaped, so a newline in the data cannot
355
+ forge or split a record.
356
+ * Named tags and payload fields are merged into the top-level `key=value` pairs. On a name
357
+ conflict the payload wins.
358
+ * Unnamed tags are emitted as keys with a `true` value.
359
+ * `tag="success"` is emitted on every entry, becoming `tag="exception"` when the entry carries an
360
+ exception (the exception class, message, and backtrace are then included as fields).
361
+
362
+ ## Network protocols
363
+
364
+ ### HTTP(S)
365
+
366
+ The HTTP appender sends JSON to most services that accept log messages over HTTP or HTTPS:
367
+
368
+ ~~~ruby
369
+ SemanticLogger.add_appender(appender: :http, url: "http://localhost:8088/path")
370
+
371
+ # For HTTPS, just change the scheme:
372
+ SemanticLogger.add_appender(appender: :http, url: "https://localhost:8088/path")
373
+ ~~~
374
+
375
+ The JSON being sent can be customized with a formatter:
376
+
377
+ ~~~ruby
378
+ formatter = Proc.new do |log, logger|
379
+ h = log.to_h(logger.host, logger.application)
380
+
381
+ # Change time from iso8601 to seconds since epoch
382
+ h[:timestamp] = log.time.utc.to_f
383
+
384
+ # Render to JSON
385
+ h.to_json
386
+ end
387
+
388
+ SemanticLogger.add_appender(appender: :http, url: "https://localhost:8088/path", formatter: formatter)
389
+ ~~~
390
+
391
+ #### Batching
392
+
393
+ By default each entry is sent in its own HTTP request. To send multiple entries in one request as a
394
+ JSON array, enable batching with `batch: true`. This suits endpoints that accept an array and create
395
+ one document per element, such as the
396
+ [Filebeat http_endpoint input](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-http_endpoint.html):
397
+
398
+ ~~~ruby
399
+ SemanticLogger.add_appender(appender: :http, url: "http://localhost:8088/path", batch: true)
400
+ ~~~
401
+
402
+ With batching the appender runs on its own thread and flushes once `batch_size` entries have
403
+ accumulated (default 300) or `batch_seconds` have elapsed (default 5), whichever comes first:
404
+
405
+ ~~~ruby
406
+ SemanticLogger.add_appender(
407
+ appender: :http,
408
+ url: "http://localhost:8088/path",
409
+ batch: true,
410
+ batch_size: 100,
411
+ batch_seconds: 10
412
+ )
413
+ ~~~
414
+
415
+ ### TCP Appender (+SSL)
416
+
417
+ The TCP appender sends JSON or other formatted messages to services that accept log messages over TCP,
418
+ optionally with SSL:
419
+
420
+ ~~~ruby
421
+ # Plain TCP:
422
+ SemanticLogger.add_appender(appender: :tcp, server: "localhost:8088")
423
+
424
+ # TCP with SSL:
425
+ SemanticLogger.add_appender(appender: :tcp, server: "localhost:8088", ssl: true)
426
+
427
+ # With self-signed certificates, or to disable server certificate verification:
428
+ SemanticLogger.add_appender(
429
+ appender: :tcp,
430
+ server: "localhost:8088",
431
+ ssl: {verify_mode: OpenSSL::SSL::VERIFY_NONE}
432
+ )
433
+ ~~~
434
+
435
+ Customize the message with a formatter:
436
+
437
+ ~~~ruby
438
+ formatter = Proc.new do |log, logger|
439
+ h = log.to_h(logger.host, logger.application)
440
+ h[:timestamp] = log.time.utc.to_f # seconds since epoch
441
+ h.to_json
442
+ end
443
+
444
+ SemanticLogger.add_appender(appender: :tcp, server: "localhost:8088", formatter: formatter)
445
+ ~~~
446
+
447
+ See [Net::TCPClient](https://github.com/reidmorrison/net_tcp_client) for the remaining connection
448
+ options.
449
+
450
+ The TCP and UDP appenders separate records with a newline, so they default to the JSON formatter,
451
+ which escapes embedded newlines and is safe with untrusted data. If you switch to a text formatter
452
+ such as `:default` or `:color`, enable `escape_control_chars` so a newline in the data cannot forge or
453
+ split a record:
454
+
455
+ ~~~ruby
456
+ SemanticLogger.add_appender(
457
+ appender: :tcp,
458
+ server: "localhost:8088",
459
+ formatter: {default: {escape_control_chars: true}}
460
+ )
461
+ ~~~
462
+
463
+ ### UDP Appender
464
+
465
+ The UDP appender sends JSON or other formatted messages over UDP:
466
+
467
+ ~~~ruby
468
+ SemanticLogger.add_appender(appender: :udp, server: "localhost:8088")
469
+ ~~~
470
+
471
+ Customize the message with a formatter:
472
+
473
+ ~~~ruby
474
+ formatter = Proc.new do |log, logger|
475
+ h = log.to_h(logger.host, logger.application)
476
+ h[:timestamp] = log.time.utc.to_f # seconds since epoch
477
+ h.to_json
478
+ end
479
+
480
+ SemanticLogger.add_appender(appender: :udp, server: "localhost:8088", formatter: formatter)
481
+ ~~~
482
+
483
+ ### Syslog
484
+
485
+ Log to a local Syslog daemon:
486
+
487
+ ~~~ruby
488
+ SemanticLogger.add_appender(appender: :syslog)
489
+ ~~~
490
+
491
+ Log to a remote Syslog server over TCP (the `net_tcp_client` and `syslog_protocol` gems are required).
492
+ The default packet size is 1024 bytes:
493
+
494
+ ~~~ruby
495
+ SemanticLogger.add_appender(appender: :syslog, url: "tcp://myloghost:514", max_size: 2048)
496
+ ~~~
497
+
498
+ Or over UDP (the `syslog_protocol` gem is required):
499
+
500
+ ~~~ruby
501
+ SemanticLogger.add_appender(appender: :syslog, url: "udp://myloghost:514")
502
+ ~~~
503
+
504
+ Add a filter to exclude noisy entries such as health checks:
505
+
506
+ ~~~ruby
507
+ SemanticLogger.add_appender(
508
+ appender: :syslog,
509
+ url: "udp://myloghost:514",
510
+ filter: Proc.new { |log| log.message !~ /(health_check|Not logged in)/ }
511
+ )
512
+ ~~~
513
+
514
+ Syslog frames each record, so embedded newlines or other control characters in untrusted data could
515
+ forge or split records. The syslog formatters therefore escape control characters by default. To pass
516
+ them through unchanged:
517
+
518
+ ~~~ruby
519
+ SemanticLogger.add_appender(
520
+ appender: :syslog,
521
+ url: "tcp://myloghost:514",
522
+ formatter: {syslog: {escape_control_chars: false}}
523
+ )
524
+ ~~~
525
+
526
+ Note: `:trace` level messages are mapped to `:debug`.
527
+
528
+ ## Centralized logging and aggregators
529
+
530
+ ### Elasticsearch
531
+
532
+ Forward all log entries to Elasticsearch (requires the `elasticsearch` gem). By default entries are
533
+ written to a daily index named `semantic_logger-YYYY.MM.DD`; override it with `index:`:
534
+
535
+ ~~~ruby
536
+ SemanticLogger.add_appender(
537
+ appender: :elasticsearch,
538
+ url: "http://localhost:9200",
539
+ index: "my-index",
540
+ data_stream: true
541
+ )
542
+ ~~~
543
+
544
+ For an end-to-end walkthrough with Kibana, see [Centralized Logging](operations.html#centralized-logging).
545
+
546
+ ### OpenSearch
547
+
548
+ Forward all log entries to OpenSearch, for example AWS OpenSearch (requires the `opensearch-ruby`
549
+ gem). OpenSearch is a fork of Elasticsearch and uses the same bulk indexing API, so this appender
550
+ accepts the same options as [Elasticsearch](#elasticsearch). Use it instead of the Elasticsearch
551
+ appender when talking to an OpenSearch server, since recent `elasticsearch` gems reject
552
+ non-Elasticsearch servers with an `Elasticsearch::UnsupportedProductError`:
553
+
554
+ ~~~ruby
555
+ SemanticLogger.add_appender(
556
+ appender: :opensearch,
557
+ url: "http://localhost:9200",
558
+ index: "my-index",
559
+ data_stream: true
560
+ )
561
+ ~~~
562
+
563
+ ### Graylog
564
+
565
+ Send log entries to a [Graylog](https://www.graylog.org) server (requires the `gelf` gem). Data is
566
+ sent as JSON, so all of the semantic structure is retained rather than being flattened into text.
567
+
568
+ Over TCP:
569
+
570
+ ~~~ruby
571
+ SemanticLogger.add_appender(appender: :graylog, url: "tcp://localhost:12201")
572
+ ~~~
573
+
574
+ Or over UDP:
575
+
576
+ ~~~ruby
577
+ SemanticLogger.add_appender(appender: :graylog, url: "udp://localhost:12201")
578
+ ~~~
579
+
580
+ If not using Rails, the `facility` can be removed, or set to a custom string describing the
581
+ application. Note: `:trace` level messages are mapped to `:debug`.
582
+
583
+ ### Splunk HTTP
584
+
585
+ To write to the Splunk HTTP Collector, follow the Splunk instructions to enable the
586
+ [HTTP Event Collector](http://dev.splunk.com/view/event-collector/SP-CAAAE7F) and generate a token:
587
+
588
+ ~~~ruby
589
+ SemanticLogger.add_appender(
590
+ appender: :splunk_http,
591
+ url: "http://localhost:8088/services/collector/event",
592
+ token: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
593
+ )
594
+ ~~~
595
+
596
+ For HTTPS, change the URL scheme to `https`. Once entries have been sent, open the Splunk web
597
+ interface, select Search, limit to the new host (`host=hostname`), switch to table view, and select
598
+ the interesting columns: `host`, `duration`, `name`, `level`, `message`.
599
+
600
+ **Performance:** against a local Splunk instance, the HTTP collector handled only about 30 entries per
601
+ second. For much higher throughput, write to Splunk with the [TCP appender](#tcp-appender-ssl)
602
+ instead, which reached about 1,400 entries per second (1,200 with SSL).
603
+
604
+ ### Splunk SDK
605
+
606
+ `appender: :splunk` submits entries through the official `splunk-sdk-ruby` gem to the Splunk
607
+ management port (8089 by default). Authenticate with either a username and password, or a
608
+ pre-authenticated `token:`:
609
+
610
+ ~~~ruby
611
+ SemanticLogger.add_appender(
612
+ appender: :splunk,
613
+ username: "username",
614
+ password: "password",
615
+ host: "localhost",
616
+ port: 8089,
617
+ scheme: :https,
618
+ index: "main"
619
+ )
620
+ ~~~
621
+
622
+ For most deployments the [HTTP Event Collector](#splunk-http) or the [TCP appender](#tcp-appender-ssl)
623
+ is the simpler choice; use the SDK appender when the management API is the only ingress available.
624
+
625
+ ### Grafana Loki
626
+
627
+ Send log entries to [Grafana Loki](https://grafana.com/docs/loki) via its
628
+ [HTTP push API](https://grafana.com/docs/loki/latest/reference/loki-http-api/#ingest-logs):
629
+
630
+ ~~~ruby
631
+ SemanticLogger.add_appender(
632
+ appender: :loki,
633
+ url: "https://logs-prod-001.grafana.net",
634
+ username: "grafana_username",
635
+ password: "grafana_token_here",
636
+ compress: true
637
+ )
638
+ ~~~
639
+
640
+ Set the URL, username, and password to match your Loki instance. Set `compress: true` to compress the
641
+ log messages.
642
+
643
+ ### CloudWatch Logs
644
+
645
+ Forward all log entries to AWS CloudWatch Logs (requires the `aws-sdk-cloudwatchlogs` gem):
646
+
647
+ ~~~ruby
648
+ SemanticLogger.add_appender(
649
+ appender: :cloudwatch_logs,
650
+ client_kwargs: {region: "eu-west-1"},
651
+ group: "/my/application",
652
+ create_stream: true
653
+ )
654
+ ~~~
655
+
656
+ ### OpenTelemetry
657
+
658
+ Send log entries to [OpenTelemetry](https://opentelemetry.io) through its
659
+ [Logs API](https://opentelemetry.io/docs/specs/otel/logs/), so they can be exported to any
660
+ OpenTelemetry-compatible backend (the OTLP collector, Honeycomb, Datadog, Grafana, and so on).
661
+
662
+ This appender requires the `opentelemetry-logs-sdk` gem plus an exporter such as
663
+ `opentelemetry-exporter-otlp-logs`:
664
+
665
+ ~~~ruby
666
+ gem "opentelemetry-logs-sdk"
667
+ gem "opentelemetry-exporter-otlp-logs"
668
+ ~~~
669
+
670
+ Configure the OpenTelemetry SDK once at startup, then add the appender. `OpenTelemetry::SDK.configure`
671
+ reads the standard `OTEL_*` environment variables (for example `OTEL_EXPORTER_OTLP_ENDPOINT`) and
672
+ installs a logger provider, which the appender picks up automatically:
673
+
674
+ ~~~ruby
675
+ require "opentelemetry-logs-sdk"
676
+ require "opentelemetry-exporter-otlp-logs"
677
+
678
+ OpenTelemetry::SDK.configure
679
+
680
+ SemanticLogger.add_appender(appender: :open_telemetry)
681
+ ~~~
682
+
683
+ Each entry is emitted with its level mapped to the matching OpenTelemetry severity number, the message
684
+ as the record body, and the payload as record attributes. The appender registers a
685
+ `SemanticLogger.on_log` subscriber that captures the current OpenTelemetry context as each entry is
686
+ logged, so log records are correlated with the active trace and span.
687
+
688
+ | Option | Description |
689
+ |--------|-------------|
690
+ | `name` | Instrumentation scope name reported to OpenTelemetry. Defaults to `"SemanticLogger"`. |
691
+ | `version` | Instrumentation scope version. Defaults to the Semantic Logger gem version. |
692
+ | `metrics` | Whether to forward metric-only log entries. Defaults to `true`. |
693
+
694
+ ### Logstash
695
+
696
+ Forward log entries to Logstash through the `logstash-logger` gem. Configure a `LogStashLogger` and
697
+ hand it to Semantic Logger as a `logger:` appender:
698
+
699
+ ~~~ruby
700
+ require "logstash-logger"
701
+
702
+ # See https://github.com/dwbutler/logstash-logger for further options
703
+ log_stash = LogStashLogger.new(type: :tcp, host: "localhost", port: 5229)
704
+
705
+ SemanticLogger.add_appender(logger: log_stash)
706
+ ~~~
707
+
708
+ Note: `:trace` level messages are mapped to `:debug`.
709
+
710
+ ### logentries.com
711
+
712
+ Obtain a token by following the [logentries instructions](https://logentries.com/doc/input-token/),
713
+ then prefix each JSON line with the token using a small custom formatter over the TCP appender:
714
+
715
+ ~~~ruby
716
+ module Logentries
717
+ class Formatter < SemanticLogger::Formatters::Json
718
+ attr_accessor :token
719
+
720
+ def initialize(token)
721
+ @token = token
722
+ end
723
+
724
+ def call(log, logger)
725
+ "#{token} #{super(log, logger)}"
726
+ end
727
+ end
728
+ end
729
+
730
+ formatter = Logentries::Formatter.new("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
731
+ SemanticLogger.add_appender(appender: :tcp, server: "api.logentries.com:20000", ssl: true, formatter: formatter)
732
+ ~~~
733
+
734
+ ### loggly.com
735
+
736
+ After signing up with Loggly, obtain a token under `Source Setup` -> `Customer Tokens`, then post to
737
+ the Loggly input URL with the HTTP appender:
738
+
739
+ ~~~ruby
740
+ SemanticLogger.add_appender(
741
+ appender: :http,
742
+ url: "https://logs-01.loggly.com/inputs/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/tag/semantic_logger/"
743
+ )
744
+ ~~~
745
+
746
+ Once entries have been sent, open the Loggly web interface, select Search, and start with
747
+ `tag:"semantic_logger"`. In the Field Explorer switch to Grid view and add the columns `host`,
748
+ `duration`, `name`, `level`, and `message`.
749
+
750
+ ### Papertrail
751
+
752
+ Papertrail accepts log entries over TLS TCP in syslog format:
753
+
754
+ ~~~ruby
755
+ config.semantic_logger.add_appender(
756
+ appender: :syslog,
757
+ url: "tcp://something.papertrailapp.com:1234",
758
+ tcp_client: {
759
+ ssl: {
760
+ ca_file: File.join(Rails.root, "config", "papertrail-bundle.pem")
761
+ }
762
+ }
763
+ )
764
+ ~~~
765
+
766
+ See [Papertrail's documentation](http://help.papertrailapp.com/kb/configuration/encrypting-remote-syslog-with-tls-ssl/)
767
+ for more.
768
+
769
+ ## Error and exception monitoring
770
+
771
+ > **Note:** These appenders forward the payload as supplied. Take care not to push sensitive
772
+ > information in tags or a payload.
773
+
774
+ ### Bugsnag
775
+
776
+ Forward `:info`, `:warn`, or `:error` entries to Bugsnag (requires the `bugsnag` gem). Configure
777
+ Bugsnag following the
778
+ [Ruby Bugsnag documentation](https://bugsnag.com/docs/notifiers/ruby#sending-handled-exceptions), then
779
+ add the appender, choosing the minimum level to forward:
780
+
781
+ ~~~ruby
782
+ # :error and above (the default)
783
+ SemanticLogger.add_appender(appender: :bugsnag)
784
+
785
+ # :warn and above
786
+ SemanticLogger.add_appender(appender: :bugsnag, level: :warn)
787
+
788
+ # :info and above
789
+ SemanticLogger.add_appender(appender: :bugsnag, level: :info)
790
+ ~~~
791
+
792
+ ### Sentry
793
+
794
+ Use the `sentry-ruby` gem and the corresponding appender:
795
+
796
+ ~~~ruby
797
+ SemanticLogger.add_appender(appender: :sentry_ruby)
798
+ ~~~
799
+
800
+ > **Deprecated:** the older `appender: :sentry` (backed by the end-of-life `sentry-raven` gem) still
801
+ > works but is deprecated and will be removed in v6. Switch to `appender: :sentry_ruby`.
802
+
803
+ Some logging context is forwarded to Sentry:
804
+
805
+ * From named tags, `transaction_name`. See
806
+ <https://docs.sentry.io/platforms/ruby/enriching-events/transaction-name/>.
807
+ * From named tags or payload, `user` is built from the `user_id`, `username`, `user_email`, and
808
+ `ip_address` keys, plus any `user` key that is itself a hash. See
809
+ <https://docs.sentry.io/platforms/ruby/enriching-events/identify-user/>.
810
+ * From the payload, `fingerprint` configures grouping granularity. See
811
+ <https://docs.sentry.io/platforms/ruby/usage/sdk-fingerprinting/>.
812
+ * Named tags are sent as Sentry tags. See
813
+ <https://docs.sentry.io/platforms/ruby/enriching-events/tags/>.
814
+ * Unnamed tags are sent as the `:tag` tag, separated by commas.
815
+ * Everything else from the payload and context is added to `extras`.
816
+
817
+ ~~~ruby
818
+ SemanticLogger.tagged(transaction_name: "foo", user_id: 42, baz: "quz") do
819
+ logger.error("some message", username: "joe", fingerprint: ["bar"])
820
+ end
821
+ ~~~
822
+
823
+ ### Honeybadger and Honeybadger Insights
824
+
825
+ Forward errors to Honeybadger (requires the `honeybadger` gem):
826
+
827
+ ~~~ruby
828
+ SemanticLogger.add_appender(appender: :honeybadger)
829
+ ~~~
830
+
831
+ Or forward all log entries to Honeybadger Insights as events:
832
+
833
+ ~~~ruby
834
+ SemanticLogger.add_appender(appender: :honeybadger_insights)
835
+ ~~~
836
+
837
+ Both appenders use the Honeybadger
838
+ [gem configuration](https://docs.honeybadger.io/lib/ruby/gem-reference/configuration/).
839
+
840
+ ### NewRelic
841
+
842
+ New Relic supports Error Events on both its paid and free plans. The appender sends `:error` and
843
+ `:fatal` entries to New Relic as error events by default (requires the `newrelic_rpm` gem):
844
+
845
+ ~~~ruby
846
+ SemanticLogger.add_appender(appender: :new_relic)
847
+
848
+ # To also send warnings:
849
+ SemanticLogger.add_appender(appender: :new_relic, level: :warn)
850
+ ~~~
851
+
852
+ > **Deprecated:** `appender: :new_relic` (error events via `newrelic_rpm`) is deprecated and will be
853
+ > removed in v6. To forward log entries to New Relic, use `appender: :new_relic_logs` instead.
854
+
855
+ ### Rollbar
856
+
857
+ Rollbar needs its own current-thread context, so it cannot run as a regular appender unless logging is
858
+ [synchronous](operations.html#synchronous-operation). Integrate it with an `on_log` subscriber instead
859
+ (as recommended by @gingerlime):
860
+
861
+ ~~~ruby
862
+ SemanticLogger.on_log do |log|
863
+ next unless log.try(:level) == :error
864
+
865
+ err = RuntimeError.new(log.try(:message))
866
+ err.set_backtrace(log.backtrace) if log.backtrace
867
+ Rollbar.error(err, :log_extra => log.to_h)
868
+ end
869
+ ~~~
870
+
871
+ ## Databases and message queues
872
+
873
+ ### MongoDB
874
+
875
+ Write log entries as documents into a MongoDB capped collection (requires the `mongo` gem):
876
+
877
+ ~~~ruby
878
+ appender = SemanticLogger::Appender::MongoDB.new(
879
+ uri: "mongodb://127.0.0.1:27017/test",
880
+ collection_size: 1024**3, # 1 gigabyte
881
+ application: Rails.application.class.name
882
+ )
883
+ SemanticLogger.add_appender(appender: appender)
884
+ ~~~
885
+
886
+ Each entry is stored as a document:
887
+
888
+ ~~~javascript
889
+ > db.semantic_logger.findOne()
890
+ {
891
+ "_id" : ObjectId("53a8d5b99b9eb4f282000001"),
892
+ "time" : ISODate("2014-06-24T01:34:49.489Z"),
893
+ "host_name" : "appserver1",
894
+ "pid" : null,
895
+ "thread_name" : "2160245740",
896
+ "name" : "Example",
897
+ "level" : "info",
898
+ "level_index" : 2,
899
+ "application" : "my_application",
900
+ "message" : "This message is written to mongo as a document"
901
+ }
902
+ ~~~
903
+
904
+ ### Apache Kafka
905
+
906
+ Publish log entries to an Apache Kafka broker (requires the `ruby-kafka` gem):
907
+
908
+ ~~~ruby
909
+ SemanticLogger.add_appender(
910
+ appender: :kafka,
911
+ seed_brokers: ["kafka1:9092", "kafka2:9092"]
912
+ )
913
+ ~~~
914
+
915
+ ### RabbitMQ (AMQP)
916
+
917
+ Stream log entries through a queue on a RabbitMQ broker (requires the `bunny` gem):
918
+
919
+ ~~~ruby
920
+ SemanticLogger.add_appender(
921
+ appender: :rabbitmq,
922
+ queue_name: "semantic_logger",
923
+ rabbitmq_host: "localhost",
924
+ username: "the-username",
925
+ password: "the-password"
926
+ )
927
+ ~~~
928
+
929
+ ## Logging to several destinations at once
930
+
931
+ Add as many appenders as you like; every entry is written to all of them. Use a per-appender `level`
932
+ so each destination keeps a different subset.
933
+
934
+ ~~~ruby
935
+ require "semantic_logger"
936
+
937
+ SemanticLogger.default_level = :info
938
+
939
+ # Everything at :warn and above to one file:
940
+ SemanticLogger.add_appender(file_name: "log/warnings.log", level: :warn)
941
+
942
+ # Everything at :trace and above to another:
943
+ SemanticLogger.add_appender(file_name: "log/trace.log", level: :trace)
944
+
945
+ logger = SemanticLogger["MyClass"]
946
+ logger.level = :trace
947
+ logger.trace "This is a trace message"
948
+ logger.info "This is an info message"
949
+ logger.warn "This is a warning message"
950
+ ~~~
951
+
952
+ Each file receives only the entries at or above its level:
953
+
954
+ ~~~
955
+ ==> trace.log <==
956
+ 2013-08-02 14:15:56.733532 T [35669:70176909690580] MyClass -- This is a trace message
957
+ 2013-08-02 14:15:56.734273 I [35669:70176909690580] MyClass -- This is an info message
958
+ 2013-08-02 14:15:56.735273 W [35669:70176909690580] MyClass -- This is a warning message
959
+
960
+ ==> warnings.log <==
961
+ 2013-08-02 14:15:56.735273 W [35669:70176909690580] MyClass -- This is a warning message
962
+ ~~~
963
+
964
+ A common combination is colorized text to a local file plus a remote aggregator:
965
+
966
+ ~~~ruby
967
+ SemanticLogger.add_appender(file_name: "development.log", formatter: :color)
968
+ SemanticLogger.add_appender(appender: :syslog, url: "tcp://myloghost:514")
969
+ ~~~
970
+
971
+ ## Standalone appenders
972
+
973
+ An appender can be created and logged to directly, using the same API as any logger. This is useful to
974
+ send specific activity on the current thread to a separate destination, without writing it to the
975
+ appenders registered with Semantic Logger:
976
+
977
+ ~~~ruby
978
+ require "semantic_logger"
979
+
980
+ appender = SemanticLogger::Appender::File.new("separate.log", level: :info, formatter: :color)
981
+
982
+ appender.warn "Only send this to separate.log"
983
+
984
+ appender.measure_info "Called supplier" do
985
+ # Call supplier ...
986
+ end
987
+ ~~~
988
+
989
+ Note: once an appender has been registered with Semantic Logger, do not also call it directly.
990
+ Non-deterministic concurrency issues arise when it is used across threads.