logstash-logger 0.26.1 → 1.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/.github/workflows/tests.yml +53 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +85 -105
- data/Appraisals +6 -20
- data/CHANGELOG.md +61 -0
- data/Gemfile +2 -2
- data/README.md +12 -833
- data/docs/buffering.md +86 -0
- data/docs/customization.md +86 -0
- data/docs/outputs.md +65 -0
- data/docs/rails.md +349 -0
- data/docs/ssl.md +102 -0
- data/docs/troubleshooting.md +106 -0
- data/docs/usage.md +148 -0
- data/gemfiles/rails_8.0.gemfile +10 -0
- data/gemfiles/rails_8.1.gemfile +9 -0
- data/lib/logstash-logger/buffer.rb +15 -5
- data/lib/logstash-logger/configuration.rb +1 -2
- data/lib/logstash-logger/device/aws_stream.rb +96 -17
- data/lib/logstash-logger/device/base.rb +2 -2
- data/lib/logstash-logger/device/connectable.rb +30 -12
- data/lib/logstash-logger/device/file.rb +21 -4
- data/lib/logstash-logger/device/firehose.rb +6 -0
- data/lib/logstash-logger/device/http.rb +104 -0
- data/lib/logstash-logger/device/kafka.rb +182 -38
- data/lib/logstash-logger/device/kinesis.rb +10 -1
- data/lib/logstash-logger/device/redis.rb +8 -1
- data/lib/logstash-logger/device/tcp.rb +54 -12
- data/lib/logstash-logger/device.rb +24 -2
- data/lib/logstash-logger/formatter/base.rb +97 -11
- data/lib/logstash-logger/formatter/cee_syslog.rb +8 -11
- data/lib/logstash-logger/formatter/json.rb +13 -0
- data/lib/logstash-logger/formatter/json_lines.rb +13 -0
- data/lib/logstash-logger/formatter.rb +14 -6
- data/lib/logstash-logger/logger.rb +6 -19
- data/lib/logstash-logger/multi_logger.rb +2 -1
- data/lib/logstash-logger/railtie.rb +1 -1
- data/lib/logstash-logger/tagged_logging.rb +3 -1
- data/lib/logstash-logger/version.rb +1 -1
- data/logstash-logger.gemspec +8 -4
- data/spec/device/aws_stream_limits_spec.rb +219 -0
- data/spec/device/connectable_spec.rb +51 -0
- data/spec/device/file_spec.rb +65 -0
- data/spec/device/firehose_spec.rb +4 -1
- data/spec/device/http_spec.rb +176 -0
- data/spec/device/kafka_spec.rb +429 -14
- data/spec/device/kinesis_spec.rb +4 -1
- data/spec/device/tcp_spec.rb +94 -5
- data/spec/device/tcp_tls_integration_spec.rb +91 -0
- data/spec/device_spec.rb +13 -0
- data/spec/formatter/base_spec.rb +82 -1
- data/spec/formatter/cee_syslog_spec.rb +71 -19
- data/spec/formatter/framing_spec.rb +58 -0
- data/spec/formatter/json_lines_spec.rb +41 -0
- data/spec/formatter/json_spec.rb +49 -0
- data/spec/formatter_spec.rb +19 -2
- data/spec/logger_spec.rb +5 -5
- data/spec/multi_logger_spec.rb +16 -0
- data/spec/spec_helper.rb +7 -5
- data/spec/support/tls_helper.rb +30 -0
- data/spec/support/unserializable_value.rb +10 -0
- data/spec/tagged_logging_spec.rb +15 -0
- metadata +101 -16
- data/.travis.yml +0 -26
- data/gemfiles/rails_3.2.gemfile +0 -9
- data/gemfiles/rails_4.0.gemfile +0 -9
- data/gemfiles/rails_4.1.gemfile +0 -9
- data/gemfiles/rails_4.2.gemfile +0 -9
- data/gemfiles/rails_5.0.gemfile +0 -9
- data/gemfiles/rails_5.1.gemfile +0 -9
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Logstash never receives any logs
|
|
4
|
+
|
|
5
|
+
If you are using a device backed by a Ruby IO object (such as a file, UDP socket, or TCP socket), please be aware that Ruby
|
|
6
|
+
keeps its own internal buffer. Despite the fact that LogStashLogger buffers
|
|
7
|
+
messages and flushes them periodically, the data written to the IO object can
|
|
8
|
+
be buffered by Ruby internally indefinitely, and may not even write until the
|
|
9
|
+
program terminates. If this bothers you or you need to see log messages
|
|
10
|
+
immediately, your only recourse is to set the `sync: true` option.
|
|
11
|
+
|
|
12
|
+
## JSON::GeneratorError
|
|
13
|
+
|
|
14
|
+
Your application is probably attempting to log data that is not encoded in a valid way. When this happens, Ruby's
|
|
15
|
+
standard JSON library will raise an exception. You may be able to overcome this by swapping out a different JSON encoder
|
|
16
|
+
such as Oj. Use the [oj_mimic_json](https://github.com/ohler55/oj_mimic_json) gem to use Oj for JSON generation.
|
|
17
|
+
|
|
18
|
+
## Records tagged `_logstashlogger_format_failure`
|
|
19
|
+
|
|
20
|
+
When a log call cannot be turned into a record, for example because a logged
|
|
21
|
+
value cannot be serialized to JSON, LogStashLogger writes a placeholder record
|
|
22
|
+
instead. It is framed like any other record, so the records around it are not
|
|
23
|
+
affected. The placeholder has the message `Failed to format log event`, the
|
|
24
|
+
tag `_logstashlogger_format_failure`, the logger's time and severity, and the
|
|
25
|
+
error class in an `error` field. The error itself is reported to the error
|
|
26
|
+
logger. Before 1.1.0 a bare `Failed to format log event` string was written
|
|
27
|
+
without a line ending, which could merge with the next record.
|
|
28
|
+
|
|
29
|
+
## The `cee_syslog` header shows the local hostname
|
|
30
|
+
|
|
31
|
+
The `cee_syslog` formatter writes a `hostname progname:@cee:` header before
|
|
32
|
+
each record. The hostname is always this machine's hostname, and control
|
|
33
|
+
characters in the header are replaced with spaces. A `host` field in the logged
|
|
34
|
+
content, or one set in `customize_event`, appears only in the JSON body.
|
|
35
|
+
|
|
36
|
+
## No logs getting sent on Heroku
|
|
37
|
+
|
|
38
|
+
Heroku recommends installing the [rails_12factor](https://github.com/heroku/rails_12factor) so that logs get sent to STDOUT.
|
|
39
|
+
Unfortunately, this overrides LogStashLogger, preventing logs from being sent to their configured destination. The solution
|
|
40
|
+
is to remove `rails_12factor` from your Gemfile.
|
|
41
|
+
|
|
42
|
+
## Logging eventually stops in production
|
|
43
|
+
|
|
44
|
+
This is most likely not a problem with LogStashLogger, but rather a different gem changing the log level of `Rails.logger`.
|
|
45
|
+
This is especially likely if you're using a threaded server such as Puma, since gems often change the log level of
|
|
46
|
+
`Rails.logger` in a non thread-safe way. See [#17](https://github.com/dwbutler/logstash-logger/issues/17) for more information.
|
|
47
|
+
|
|
48
|
+
## Sometimes two lines of JSON log messages get sent as one message
|
|
49
|
+
|
|
50
|
+
If you're using UDP output and writing to a logstash listener, you are most likely encountering a bug in the UDP implementation
|
|
51
|
+
of the logstash listener. There is no known fix at this time. See [#43](https://github.com/dwbutler/logstash-logger/issues/43)
|
|
52
|
+
for more information.
|
|
53
|
+
|
|
54
|
+
## Errno::EMSGSIZE - Message too long
|
|
55
|
+
|
|
56
|
+
A known drawback of using TCP or UDP is the 65535 byte limit on total message size. To workaround
|
|
57
|
+
this issue, you will have to truncate the message by setting the max message size:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
LogStashLogger.configure do |config|
|
|
61
|
+
config.max_message_size = 2000
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This will truncate only the `message` field of the LogStash Event. So make sure
|
|
66
|
+
you set the max message size significantly less than 65535 bytes to make room
|
|
67
|
+
for other fields.
|
|
68
|
+
|
|
69
|
+
# Breaking changes
|
|
70
|
+
|
|
71
|
+
## Version 1.1+
|
|
72
|
+
|
|
73
|
+
MRI Ruby < 3.3 and Rails < 8.0 are no longer supported, since they have been EOL'ed. If you are on Ruby 3.2 or Rails 7.2, you will need to use 1.0.x.
|
|
74
|
+
|
|
75
|
+
## Version 0.27+
|
|
76
|
+
|
|
77
|
+
MRI Ruby < 3.2 is no longer supported, since it has been EOL'ed. If you are on an older version of Ruby, you will need to use 0.26 or below.
|
|
78
|
+
|
|
79
|
+
## Version 0.25+
|
|
80
|
+
|
|
81
|
+
Rails 3.2, MRI Ruby < 2.2, and JRuby 1.7 are no longer supported, since they have been
|
|
82
|
+
EOL'ed. If you are on an older version of Ruby, you will need to use 0.24 or below.
|
|
83
|
+
|
|
84
|
+
## Version 0.5+
|
|
85
|
+
|
|
86
|
+
* The `source` event key has been replaced with `host` to better match the latest logstash.
|
|
87
|
+
* The `(host, port, type)` constructor has been deprecated in favor of an options hash constructor.
|
|
88
|
+
|
|
89
|
+
## Version 0.4+
|
|
90
|
+
|
|
91
|
+
`LogStash::Event` uses the v1 format starting version 1.2+. If you're using the v1, you'll need to install
|
|
92
|
+
LogStashLogger version 0.4+. This is not backwards compatible with the old `LogStash::Event` v1.1.5, which uses
|
|
93
|
+
the v0 format.
|
|
94
|
+
|
|
95
|
+
## Version 0.3+
|
|
96
|
+
|
|
97
|
+
Earlier versions of this gem (<= 0.2.1) only implemented a TCP connection.
|
|
98
|
+
Newer versions (>= 0.3) also implement UDP, and use that as the new default.
|
|
99
|
+
Please be aware if you are using the default constructor and still require TCP, you should add an additional argument:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# Now defaults to UDP instead of TCP
|
|
103
|
+
logger = LogStashLogger.new('localhost', 5228)
|
|
104
|
+
# Explicitly specify TCP instead of UDP
|
|
105
|
+
logger = LogStashLogger.new('localhost', 5228, :tcp)
|
|
106
|
+
```
|
data/docs/usage.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Usage Examples
|
|
2
|
+
|
|
3
|
+
## Basic Usage
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'logstash-logger'
|
|
7
|
+
|
|
8
|
+
# Defaults to UDP on 0.0.0.0
|
|
9
|
+
logger = LogStashLogger.new(port: 5228)
|
|
10
|
+
|
|
11
|
+
# Specify host and type (UDP or TCP) explicitly
|
|
12
|
+
udp_logger = LogStashLogger.new(type: :udp, host: 'localhost', port: 5228)
|
|
13
|
+
tcp_logger = LogStashLogger.new(type: :tcp, host: 'localhost', port: 5229)
|
|
14
|
+
|
|
15
|
+
# Other types of loggers
|
|
16
|
+
file_logger = LogStashLogger.new(type: :file, path: 'log/development.log', sync: true)
|
|
17
|
+
unix_logger = LogStashLogger.new(type: :unix, path: '/tmp/sock')
|
|
18
|
+
syslog_logger = LogStashLogger.new(type: :syslog)
|
|
19
|
+
redis_logger = LogStashLogger.new(type: :redis)
|
|
20
|
+
kafka_logger = LogStashLogger.new(type: :kafka)
|
|
21
|
+
stdout_logger = LogStashLogger.new(type: :stdout)
|
|
22
|
+
stderr_logger = LogStashLogger.new(type: :stderr)
|
|
23
|
+
io_logger = LogStashLogger.new(type: :io, io: io)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Using Formatters
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Use a different formatter
|
|
30
|
+
cee_logger = LogStashLogger.new(
|
|
31
|
+
type: :tcp,
|
|
32
|
+
host: 'logsene-receiver-syslog.sematext.com',
|
|
33
|
+
port: 514,
|
|
34
|
+
formatter: :cee_syslog
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
custom_formatted_logger = LogStashLogger.new(
|
|
38
|
+
type: :redis,
|
|
39
|
+
formatter: MyCustomFormatter
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
lambda_formatted_logger = LogStashLogger.new(
|
|
43
|
+
type: :stdout,
|
|
44
|
+
formatter: ->(severity, time, progname, msg) { "[#{progname}] #{msg}" }
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ruby_default_formatter_logger = LogStashLogger.new(
|
|
48
|
+
type: :file,
|
|
49
|
+
path: 'log/development.log',
|
|
50
|
+
formatter: ::Logger::Formatter
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Multiple Outputs
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# Send messages to multiple outputs. Each output will have the same format.
|
|
58
|
+
# Syslog cannot be an output because it requires a separate logger.
|
|
59
|
+
multi_delegating_logger = LogStashLogger.new(
|
|
60
|
+
type: :multi_delegator,
|
|
61
|
+
outputs: [
|
|
62
|
+
{ type: :file, path: 'log/development.log' },
|
|
63
|
+
{ type: :udp, host: 'localhost', port: 5228 }
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
# Balance messages between several outputs.
|
|
67
|
+
# Works the same as multi delegator, but randomly chooses an output to send each message.
|
|
68
|
+
balancer_logger = LogStashLogger.new(
|
|
69
|
+
type: :balancer,
|
|
70
|
+
outputs: [
|
|
71
|
+
{ type: :udp, host: 'host1', port: 5228 },
|
|
72
|
+
{ type: :udp, host: 'host2', port: 5228 }
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
# Send messages to multiple loggers.
|
|
76
|
+
# Use this if you need to send different formats to different outputs.
|
|
77
|
+
# If you need to log to syslog, you must use this.
|
|
78
|
+
multi_logger = LogStashLogger.new(
|
|
79
|
+
type: :multi_logger,
|
|
80
|
+
outputs: [
|
|
81
|
+
{ type: :file, path: 'log/development.log', formatter: ::Logger::Formatter },
|
|
82
|
+
{ type: :tcp, host: 'localhost', port: 5228, formatter: :json }
|
|
83
|
+
])
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Logging Messages
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# The following messages are written to UDP port 5228:
|
|
90
|
+
|
|
91
|
+
logger.info 'test'
|
|
92
|
+
# {"message":"test","@timestamp":"2014-05-22T09:37:19.204-07:00","@version":"1","severity":"INFO","host":"[hostname]"}
|
|
93
|
+
|
|
94
|
+
logger.error '{"message": "error"}'
|
|
95
|
+
# {"message":"error","@timestamp":"2014-05-22T10:10:55.877-07:00","@version":"1","severity":"ERROR","host":"[hostname]"}
|
|
96
|
+
|
|
97
|
+
logger.debug message: 'test', foo: 'bar'
|
|
98
|
+
# {"message":"test","foo":"bar","@timestamp":"2014-05-22T09:43:24.004-07:00","@version":"1","severity":"DEBUG","host":"[hostname]"}
|
|
99
|
+
|
|
100
|
+
logger.warn LogStash::Event.new(message: 'test', foo: 'bar')
|
|
101
|
+
# {"message":"test","foo":"bar","@timestamp":"2014-05-22T16:44:37.364Z","@version":"1","severity":"WARN","host":"[hostname]"}
|
|
102
|
+
|
|
103
|
+
# Tagged logging
|
|
104
|
+
logger.tagged('foo') { logger.fatal('bar') }
|
|
105
|
+
# {"message":"bar","@timestamp":"2014-05-26T20:35:14.685-07:00","@version":"1","severity":"FATAL","host":"[hostname]","tags":["foo"]}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## URI Configuration
|
|
109
|
+
|
|
110
|
+
You can use a URI to configure your logstash logger instead of a hash. This is useful in environments
|
|
111
|
+
such as Heroku where you may want to read configuration values from the environment. The URI scheme
|
|
112
|
+
is `type://host:port/path?key=value`. Some sample URI configurations are given below.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
udp://localhost:5228
|
|
116
|
+
tcp://localhost:5229
|
|
117
|
+
unix:///tmp/socket
|
|
118
|
+
file:///path/to/file
|
|
119
|
+
redis://localhost:6379
|
|
120
|
+
kafka://localhost:9092
|
|
121
|
+
stdout:/
|
|
122
|
+
stderr:/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Pass the URI into your logstash logger like so:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# Read the URI from an environment variable
|
|
129
|
+
logger = LogStashLogger.new(uri: ENV['LOGSTASH_URI'])
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## What type of logger should I use?
|
|
133
|
+
|
|
134
|
+
It depends on your specific needs, but most applications should use the default (UDP). Here are the advantages and
|
|
135
|
+
disadvantages of each type:
|
|
136
|
+
|
|
137
|
+
* UDP is faster than TCP because it's asynchronous (fire-and-forget). However, this means that log messages could get dropped.
|
|
138
|
+
This is okay for many applications.
|
|
139
|
+
* TCP verifies that every message has been received via two-way communication. It also supports SSL for secure transmission
|
|
140
|
+
of log messages over a network. This could slow your app down to a crawl if the TCP listener is under heavy load.
|
|
141
|
+
* A file is simple to use, but you will have to worry about log rotation and running out of disk space.
|
|
142
|
+
* Writing to a Unix socket is faster than writing to a TCP or UDP port, but only works locally.
|
|
143
|
+
* Writing to Redis is good for distributed setups that generate tons of logs. However, you will have another moving part and
|
|
144
|
+
have to worry about Redis running out of memory.
|
|
145
|
+
* Writing to stdout is only recommended for debugging purposes.
|
|
146
|
+
|
|
147
|
+
For a more detailed discussion of UDP vs TCP, I recommend reading this article:
|
|
148
|
+
[UDP vs. TCP](http://gafferongames.com/networking-for-game-programmers/udp-vs-tcp/)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "simplecov", ">= 0.22", group: :test, require: false
|
|
6
|
+
gem "simplecov-cobertura", group: :test, require: false
|
|
7
|
+
gem "rails", "~> 8.0.0"
|
|
8
|
+
gem "json", "< 3"
|
|
9
|
+
|
|
10
|
+
gemspec path: "../"
|
|
@@ -66,7 +66,6 @@ module LogStashLogger
|
|
|
66
66
|
# to ensure that all accumulated messages are flushed.
|
|
67
67
|
module Buffer
|
|
68
68
|
|
|
69
|
-
public
|
|
70
69
|
# Initialize the buffer.
|
|
71
70
|
#
|
|
72
71
|
# Call directly from your constructor if you wish to set some non-default
|
|
@@ -181,6 +180,16 @@ module LogStashLogger
|
|
|
181
180
|
end
|
|
182
181
|
end
|
|
183
182
|
|
|
183
|
+
# Put items back into the pending queue for the next flush, without
|
|
184
|
+
# applying the full-buffer policy. Safe to call from inside a flush.
|
|
185
|
+
def buffer_requeue(items, group=nil)
|
|
186
|
+
return if items.empty?
|
|
187
|
+
@buffer_state[:pending_mutex].synchronize do
|
|
188
|
+
@buffer_state[:pending_items][group].concat(items)
|
|
189
|
+
@buffer_state[:pending_count] += items.size
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
184
193
|
# Try to flush events.
|
|
185
194
|
#
|
|
186
195
|
# Returns immediately if flushing is not necessary/possible at the moment:
|
|
@@ -306,11 +315,12 @@ module LogStashLogger
|
|
|
306
315
|
end
|
|
307
316
|
end
|
|
308
317
|
|
|
318
|
+
# Never kills the timer from inside itself: called on the timer thread,
|
|
319
|
+
# this keeps the running timer, and reset_buffer reuses it.
|
|
309
320
|
def reset_flush_timer_thread
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
end
|
|
321
|
+
return if @flush_timer_thread.nil? || @flush_timer_thread == Thread.current
|
|
322
|
+
@flush_timer_thread.kill
|
|
323
|
+
@flush_timer_thread = nil
|
|
314
324
|
end
|
|
315
325
|
|
|
316
326
|
def buffer_clear_pending
|
|
@@ -13,12 +13,11 @@ module LogStashLogger
|
|
|
13
13
|
attr_accessor :max_message_size
|
|
14
14
|
attr_accessor :default_error_logger
|
|
15
15
|
|
|
16
|
-
def initialize(*
|
|
16
|
+
def initialize(*_args)
|
|
17
17
|
@customize_event_block = nil
|
|
18
18
|
@default_error_logger = Logger.new($stderr)
|
|
19
19
|
|
|
20
20
|
yield self if block_given?
|
|
21
|
-
self
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
def customize_event(&block)
|
|
@@ -18,6 +18,7 @@ module LogStashLogger
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
attr_accessor :aws_region, :stream
|
|
21
|
+
attr_reader :max_record_bytes
|
|
21
22
|
|
|
22
23
|
def initialize(opts)
|
|
23
24
|
super
|
|
@@ -25,6 +26,22 @@ module LogStashLogger
|
|
|
25
26
|
@secret_access_key = opts[:aws_secret_access_key]
|
|
26
27
|
@aws_region = opts[:aws_region]
|
|
27
28
|
@stream = opts[:stream] || DEFAULT_STREAM
|
|
29
|
+
# The largest record the stream accepts. Kinesis streams can be
|
|
30
|
+
# configured above the 1 MiB default, up to 10 MiB.
|
|
31
|
+
@max_record_bytes = Integer(opts[:aws_max_record_bytes] || self.class.max_record_bytes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Service limits, defined by each subclass
|
|
35
|
+
def self.max_records
|
|
36
|
+
fail NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.max_record_bytes
|
|
40
|
+
fail NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.max_request_bytes
|
|
44
|
+
fail NotImplementedError
|
|
28
45
|
end
|
|
29
46
|
|
|
30
47
|
def transform_message(message)
|
|
@@ -55,27 +72,32 @@ module LogStashLogger
|
|
|
55
72
|
yield
|
|
56
73
|
rescue => e
|
|
57
74
|
log_error(e)
|
|
58
|
-
log_warning("giving up")
|
|
59
75
|
close(flush: false)
|
|
76
|
+
raise
|
|
60
77
|
end
|
|
61
78
|
|
|
62
|
-
def write_batch(messages,
|
|
63
|
-
records =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
def write_batch(messages, _group = nil)
|
|
80
|
+
records = []
|
|
81
|
+
messages.each do |message|
|
|
82
|
+
record = transform_message(message.to_s)
|
|
83
|
+
size = record_size(record)
|
|
84
|
+
if size > max_record_bytes
|
|
85
|
+
log_warning("dropping a record of #{size} bytes: over the #{max_record_bytes}-byte limit of #{self.class.stream_class.name}")
|
|
86
|
+
else
|
|
87
|
+
records << record
|
|
88
|
+
end
|
|
89
|
+
end
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
end
|
|
91
|
+
slices = request_slices(records)
|
|
92
|
+
slices.each_with_index do |slice, index|
|
|
93
|
+
begin
|
|
94
|
+
with_connection { send_slice(slice) }
|
|
95
|
+
rescue StandardError
|
|
96
|
+
# Already logged by with_connection. Only records that were not
|
|
97
|
+
# sent go back; dropped oversized records never do.
|
|
98
|
+
unsent = slices[index..-1].flatten(1).map { |r| r[:data] }
|
|
99
|
+
give_back(unsent)
|
|
100
|
+
return
|
|
79
101
|
end
|
|
80
102
|
end
|
|
81
103
|
end
|
|
@@ -88,6 +110,63 @@ module LogStashLogger
|
|
|
88
110
|
@io = nil
|
|
89
111
|
end
|
|
90
112
|
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def send_slice(records)
|
|
116
|
+
resp = put_records(records)
|
|
117
|
+
return if is_successful_response(resp)
|
|
118
|
+
|
|
119
|
+
retries = []
|
|
120
|
+
retry_codes = []
|
|
121
|
+
get_response_records(resp).each_with_index do |record, index|
|
|
122
|
+
if self.class.recoverable_error_codes.include?(record.error_code)
|
|
123
|
+
retries << records[index][:data]
|
|
124
|
+
retry_codes << record.error_code
|
|
125
|
+
elsif !record.error_code.nil? && record.error_code != ''
|
|
126
|
+
log_warning("dropping a record that #{self.class.stream_class.name} rejected: #{record.error_code} #{record.error_message}")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
return if retries.empty?
|
|
130
|
+
|
|
131
|
+
log_warning("#{retries.size} records will be retried on the next flush (#{retry_codes.tally.map { |c, n| "#{c}: #{n}" }.join(', ')})")
|
|
132
|
+
# This runs inside a flush, so requeue without the full-buffer check,
|
|
133
|
+
# which would count the batch being flushed against the limit.
|
|
134
|
+
buffer_requeue(retries, @buffer_group)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def record_size(record)
|
|
138
|
+
record[:data].bytesize + record.fetch(:partition_key, '').bytesize
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Groups records into requests that fit the service's record-count and
|
|
142
|
+
# request-size limits.
|
|
143
|
+
def request_slices(records)
|
|
144
|
+
slices = []
|
|
145
|
+
bytes = 0
|
|
146
|
+
records.each do |record|
|
|
147
|
+
size = record_size(record)
|
|
148
|
+
if slices.empty? || slices.last.size >= self.class.max_records || bytes + size > self.class.max_request_bytes
|
|
149
|
+
slices << []
|
|
150
|
+
bytes = 0
|
|
151
|
+
end
|
|
152
|
+
slices.last << record
|
|
153
|
+
bytes += size
|
|
154
|
+
end
|
|
155
|
+
slices
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Records from a request that failed: retried on the next flush, or
|
|
159
|
+
# dropped when drop_messages_on_flush_error is set.
|
|
160
|
+
def give_back(messages)
|
|
161
|
+
return if messages.empty?
|
|
162
|
+
if @drop_messages_on_flush_error
|
|
163
|
+
log_warning("dropping #{messages.size} records after a failed request")
|
|
164
|
+
else
|
|
165
|
+
log_warning("#{messages.size} records will be retried on the next flush")
|
|
166
|
+
buffer_requeue(messages, @buffer_group)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
91
170
|
end
|
|
92
171
|
end
|
|
93
172
|
end
|
|
@@ -29,7 +29,7 @@ module LogStashLogger
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def write_batch(messages,
|
|
32
|
+
def write_batch(messages, _group = nil)
|
|
33
33
|
messages.each do |message|
|
|
34
34
|
write_one(message)
|
|
35
35
|
end
|
|
@@ -43,7 +43,7 @@ module LogStashLogger
|
|
|
43
43
|
close
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def close(
|
|
46
|
+
def close(_opts = {})
|
|
47
47
|
close!
|
|
48
48
|
rescue => e
|
|
49
49
|
log_error(e)
|
|
@@ -10,17 +10,9 @@ module LogStashLogger
|
|
|
10
10
|
def initialize(opts = {})
|
|
11
11
|
super
|
|
12
12
|
|
|
13
|
-
if opts[:batch_events]
|
|
14
|
-
warn "The :batch_events option is deprecated. Please use :buffer_max_items instead"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
if opts[:batch_timeout]
|
|
18
|
-
warn "The :batch_timeout option is deprecated. Please use :buffer_max_interval instead"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
13
|
@buffer_group = nil
|
|
22
|
-
@buffer_max_items = opts[:
|
|
23
|
-
@buffer_max_interval = opts[:
|
|
14
|
+
@buffer_max_items = opts[:buffer_max_items]
|
|
15
|
+
@buffer_max_interval = opts[:buffer_max_interval]
|
|
24
16
|
@drop_messages_on_flush_error =
|
|
25
17
|
if opts.key?(:drop_messages_on_flush_error)
|
|
26
18
|
opts.delete(:drop_messages_on_flush_error)
|
|
@@ -44,6 +36,11 @@ module LogStashLogger
|
|
|
44
36
|
|
|
45
37
|
@buffer_logger = opts[:buffer_logger]
|
|
46
38
|
|
|
39
|
+
# Optional cap on the size of a single formatted record. Off by default.
|
|
40
|
+
# Deleted from opts so outputs that forward their options (Redis) never
|
|
41
|
+
# receive it.
|
|
42
|
+
@max_record_bytes = parse_max_record_bytes(opts.delete(:max_record_bytes))
|
|
43
|
+
|
|
47
44
|
buffer_initialize(
|
|
48
45
|
max_items: @buffer_max_items,
|
|
49
46
|
max_interval: @buffer_max_interval,
|
|
@@ -56,7 +53,15 @@ module LogStashLogger
|
|
|
56
53
|
end
|
|
57
54
|
|
|
58
55
|
def write(message)
|
|
59
|
-
|
|
56
|
+
return if message.nil?
|
|
57
|
+
if @max_record_bytes
|
|
58
|
+
size = message.to_s.bytesize
|
|
59
|
+
if size > @max_record_bytes
|
|
60
|
+
log_warning("dropping a record of #{size} bytes: over max_record_bytes (#{@max_record_bytes})")
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
buffer_receive(message, @buffer_group)
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
def flush(*args)
|
|
@@ -117,8 +122,21 @@ module LogStashLogger
|
|
|
117
122
|
connect
|
|
118
123
|
end
|
|
119
124
|
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def parse_max_record_bytes(value)
|
|
128
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
129
|
+
bytes = Integer(value)
|
|
130
|
+
raise ArgumentError, "max_record_bytes must be a positive number of bytes: #{value.inspect}" unless bytes > 0
|
|
131
|
+
bytes
|
|
132
|
+
rescue TypeError
|
|
133
|
+
raise ArgumentError, "max_record_bytes must be a positive number of bytes: #{value.inspect}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
public
|
|
137
|
+
|
|
120
138
|
# Ensure the block is executed with a valid connection
|
|
121
|
-
def with_connection(
|
|
139
|
+
def with_connection()
|
|
122
140
|
connect unless connected?
|
|
123
141
|
yield
|
|
124
142
|
rescue => e
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
|
-
|
|
3
2
|
module LogStashLogger
|
|
4
3
|
module Device
|
|
5
4
|
class File < Base
|
|
6
5
|
def initialize(opts)
|
|
7
6
|
super
|
|
8
7
|
@path = opts[:path] || fail(ArgumentError, "Path is required")
|
|
8
|
+
@shift_age = opts[:shift_age]
|
|
9
|
+
@shift_size = opts[:shift_size]
|
|
10
|
+
@shift_period_suffix = opts[:shift_period_suffix]
|
|
11
|
+
@use_log_device = opts.key?(:shift_age) || opts.key?(:shift_size) || opts.key?(:shift_period_suffix)
|
|
9
12
|
open
|
|
10
13
|
end
|
|
11
14
|
|
|
@@ -14,9 +17,23 @@ module LogStashLogger
|
|
|
14
17
|
::FileUtils.mkdir_p ::File.dirname @path
|
|
15
18
|
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
if @use_log_device
|
|
21
|
+
require 'logger'
|
|
22
|
+
log_device_options = { binmode: true }
|
|
23
|
+
log_device_options[:shift_age] = @shift_age unless @shift_age.nil?
|
|
24
|
+
log_device_options[:shift_size] = @shift_size unless @shift_size.nil?
|
|
25
|
+
log_device_options[:shift_period_suffix] = @shift_period_suffix unless @shift_period_suffix.nil?
|
|
26
|
+
@io = ::Logger::LogDevice.new(@path, **log_device_options)
|
|
27
|
+
@io.dev.sync = self.sync unless self.sync.nil?
|
|
28
|
+
else
|
|
29
|
+
@io = ::File.open @path, ::File::WRONLY | ::File::APPEND | ::File::CREAT
|
|
30
|
+
@io.binmode
|
|
31
|
+
@io.sync = self.sync unless self.sync.nil?
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_io
|
|
36
|
+
@io.respond_to?(:dev) ? @io.dev : @io
|
|
20
37
|
end
|
|
21
38
|
end
|
|
22
39
|
end
|
|
@@ -16,6 +16,12 @@ module LogStashLogger
|
|
|
16
16
|
"ServiceUnavailableException"
|
|
17
17
|
].freeze
|
|
18
18
|
|
|
19
|
+
# https://docs.aws.amazon.com/firehose/latest/APIReference/API_PutRecordBatch.html
|
|
20
|
+
# 1,000 KiB per record and 4 MB per request; these cannot be changed.
|
|
21
|
+
def self.max_records; 500; end
|
|
22
|
+
def self.max_record_bytes; 1_024_000; end
|
|
23
|
+
def self.max_request_bytes; 4_000_000; end
|
|
24
|
+
|
|
19
25
|
def transform_message(message)
|
|
20
26
|
{
|
|
21
27
|
data: message
|