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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/tests.yml +53 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +85 -105
  5. data/Appraisals +6 -20
  6. data/CHANGELOG.md +61 -0
  7. data/Gemfile +2 -2
  8. data/README.md +12 -833
  9. data/docs/buffering.md +86 -0
  10. data/docs/customization.md +86 -0
  11. data/docs/outputs.md +65 -0
  12. data/docs/rails.md +349 -0
  13. data/docs/ssl.md +102 -0
  14. data/docs/troubleshooting.md +106 -0
  15. data/docs/usage.md +148 -0
  16. data/gemfiles/rails_8.0.gemfile +10 -0
  17. data/gemfiles/rails_8.1.gemfile +9 -0
  18. data/lib/logstash-logger/buffer.rb +15 -5
  19. data/lib/logstash-logger/configuration.rb +1 -2
  20. data/lib/logstash-logger/device/aws_stream.rb +96 -17
  21. data/lib/logstash-logger/device/base.rb +2 -2
  22. data/lib/logstash-logger/device/connectable.rb +30 -12
  23. data/lib/logstash-logger/device/file.rb +21 -4
  24. data/lib/logstash-logger/device/firehose.rb +6 -0
  25. data/lib/logstash-logger/device/http.rb +104 -0
  26. data/lib/logstash-logger/device/kafka.rb +182 -38
  27. data/lib/logstash-logger/device/kinesis.rb +10 -1
  28. data/lib/logstash-logger/device/redis.rb +8 -1
  29. data/lib/logstash-logger/device/tcp.rb +54 -12
  30. data/lib/logstash-logger/device.rb +24 -2
  31. data/lib/logstash-logger/formatter/base.rb +97 -11
  32. data/lib/logstash-logger/formatter/cee_syslog.rb +8 -11
  33. data/lib/logstash-logger/formatter/json.rb +13 -0
  34. data/lib/logstash-logger/formatter/json_lines.rb +13 -0
  35. data/lib/logstash-logger/formatter.rb +14 -6
  36. data/lib/logstash-logger/logger.rb +6 -19
  37. data/lib/logstash-logger/multi_logger.rb +2 -1
  38. data/lib/logstash-logger/railtie.rb +1 -1
  39. data/lib/logstash-logger/tagged_logging.rb +3 -1
  40. data/lib/logstash-logger/version.rb +1 -1
  41. data/logstash-logger.gemspec +8 -4
  42. data/spec/device/aws_stream_limits_spec.rb +219 -0
  43. data/spec/device/connectable_spec.rb +51 -0
  44. data/spec/device/file_spec.rb +65 -0
  45. data/spec/device/firehose_spec.rb +4 -1
  46. data/spec/device/http_spec.rb +176 -0
  47. data/spec/device/kafka_spec.rb +429 -14
  48. data/spec/device/kinesis_spec.rb +4 -1
  49. data/spec/device/tcp_spec.rb +94 -5
  50. data/spec/device/tcp_tls_integration_spec.rb +91 -0
  51. data/spec/device_spec.rb +13 -0
  52. data/spec/formatter/base_spec.rb +82 -1
  53. data/spec/formatter/cee_syslog_spec.rb +71 -19
  54. data/spec/formatter/framing_spec.rb +58 -0
  55. data/spec/formatter/json_lines_spec.rb +41 -0
  56. data/spec/formatter/json_spec.rb +49 -0
  57. data/spec/formatter_spec.rb +19 -2
  58. data/spec/logger_spec.rb +5 -5
  59. data/spec/multi_logger_spec.rb +16 -0
  60. data/spec/spec_helper.rb +7 -5
  61. data/spec/support/tls_helper.rb +30 -0
  62. data/spec/support/unserializable_value.rb +10 -0
  63. data/spec/tagged_logging_spec.rb +15 -0
  64. metadata +101 -16
  65. data/.travis.yml +0 -26
  66. data/gemfiles/rails_3.2.gemfile +0 -9
  67. data/gemfiles/rails_4.0.gemfile +0 -9
  68. data/gemfiles/rails_4.1.gemfile +0 -9
  69. data/gemfiles/rails_4.2.gemfile +0 -9
  70. data/gemfiles/rails_5.0.gemfile +0 -9
  71. 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: "../"
@@ -0,0 +1,9 @@
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.1.0"
8
+
9
+ 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
- unless @flush_timer_thread.nil?
311
- @flush_timer_thread.kill
312
- @flush_timer_thread = nil
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(*args)
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, group = nil)
63
- records = messages.map{ |m| transform_message(m) }
64
-
65
- with_connection do
66
- resp = put_records(records)
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
- # Put any failed records back into the buffer
69
- if !is_successful_response(resp)
70
- get_response_records(resp).each_with_index do |record, index|
71
- if self.class.recoverable_error_codes.include?(record.error_code)
72
- log_warning("Failed to post record using #{self.class.stream_class.name} with error: #{record.error_code} #{record.error_message}")
73
- log_warning("Retrying")
74
- write(records[index][:data])
75
- elsif !record.error_code.nil? && record.error_code != ''
76
- log_error("Failed to post record using #{self.class.stream_class.name} with error: #{record.error_code} #{record.error_message}")
77
- end
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, group = nil)
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(opts = {})
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[:batch_events] || opts[:buffer_max_items]
23
- @buffer_max_interval = opts[:batch_timeout] || opts[:buffer_max_interval]
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
- buffer_receive(message, @buffer_group) unless message.nil?
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(&block)
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
- @io = ::File.open @path, ::File::WRONLY | ::File::APPEND | ::File::CREAT
18
- @io.binmode
19
- @io.sync = self.sync
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