logstash-logger 0.26.1 → 1.0.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/tests.yml +51 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +85 -105
  5. data/Appraisals +6 -18
  6. data/CHANGELOG.md +28 -0
  7. data/Gemfile +0 -1
  8. data/README.md +12 -833
  9. data/docs/buffering.md +70 -0
  10. data/docs/customization.md +86 -0
  11. data/docs/outputs.md +42 -0
  12. data/docs/rails.md +344 -0
  13. data/docs/ssl.md +90 -0
  14. data/docs/troubleshooting.md +84 -0
  15. data/docs/usage.md +148 -0
  16. data/gemfiles/{rails_4.2.gemfile → rails_7.2.gemfile} +1 -2
  17. data/gemfiles/{rails_5.0.gemfile → rails_8.0.gemfile} +1 -2
  18. data/gemfiles/{rails_4.0.gemfile → rails_8.1.gemfile} +1 -2
  19. data/lib/logstash-logger/buffer.rb +0 -1
  20. data/lib/logstash-logger/configuration.rb +1 -2
  21. data/lib/logstash-logger/device/aws_stream.rb +1 -1
  22. data/lib/logstash-logger/device/base.rb +2 -2
  23. data/lib/logstash-logger/device/connectable.rb +3 -11
  24. data/lib/logstash-logger/device/file.rb +21 -4
  25. data/lib/logstash-logger/device/http.rb +33 -0
  26. data/lib/logstash-logger/device/kafka.rb +153 -36
  27. data/lib/logstash-logger/device/redis.rb +8 -1
  28. data/lib/logstash-logger/device/tcp.rb +1 -5
  29. data/lib/logstash-logger/device.rb +24 -2
  30. data/lib/logstash-logger/formatter/base.rb +54 -9
  31. data/lib/logstash-logger/formatter/cee_syslog.rb +1 -1
  32. data/lib/logstash-logger/formatter/json.rb +13 -0
  33. data/lib/logstash-logger/formatter/json_lines.rb +13 -0
  34. data/lib/logstash-logger/formatter.rb +14 -6
  35. data/lib/logstash-logger/logger.rb +6 -19
  36. data/lib/logstash-logger/multi_logger.rb +2 -1
  37. data/lib/logstash-logger/railtie.rb +1 -1
  38. data/lib/logstash-logger/tagged_logging.rb +3 -1
  39. data/lib/logstash-logger/version.rb +1 -1
  40. data/logstash-logger.gemspec +9 -1
  41. data/spec/device/file_spec.rb +65 -0
  42. data/spec/device/http_spec.rb +11 -0
  43. data/spec/device/kafka_spec.rb +337 -14
  44. data/spec/device_spec.rb +13 -0
  45. data/spec/formatter/base_spec.rb +46 -1
  46. data/spec/formatter/cee_syslog_spec.rb +3 -3
  47. data/spec/formatter/json_lines_spec.rb +23 -0
  48. data/spec/formatter/json_spec.rb +49 -0
  49. data/spec/formatter_spec.rb +19 -2
  50. data/spec/logger_spec.rb +5 -5
  51. data/spec/multi_logger_spec.rb +16 -0
  52. data/spec/spec_helper.rb +2 -5
  53. data/spec/tagged_logging_spec.rb +15 -0
  54. metadata +89 -16
  55. data/.travis.yml +0 -26
  56. data/gemfiles/rails_3.2.gemfile +0 -9
  57. data/gemfiles/rails_4.1.gemfile +0 -9
  58. data/gemfiles/rails_5.1.gemfile +0 -9
@@ -20,6 +20,7 @@ module LogStashLogger
20
20
  autoload :Stderr, 'logstash-logger/device/stderr'
21
21
  autoload :Balancer, 'logstash-logger/device/balancer'
22
22
  autoload :MultiDelegator, 'logstash-logger/device/multi_delegator'
23
+ autoload :HTTP, 'logstash-logger/device/http'
23
24
 
24
25
  def self.new(opts)
25
26
  opts = opts.dup
@@ -27,7 +28,7 @@ module LogStashLogger
27
28
  end
28
29
 
29
30
  def self.build_device(opts)
30
- if parsed_uri_opts = parse_uri_config(opts)
31
+ if (parsed_uri_opts = parse_uri_config(opts))
31
32
  opts.delete(:uri)
32
33
  opts.merge!(parsed_uri_opts)
33
34
  end
@@ -38,10 +39,30 @@ module LogStashLogger
38
39
  end
39
40
 
40
41
  def self.parse_uri_config(opts)
41
- if uri = opts[:uri]
42
+ if (uri = opts[:uri])
42
43
  require 'uri'
43
44
  parsed = ::URI.parse(uri)
44
45
  {type: parsed.scheme, host: parsed.host, port: parsed.port, path: parsed.path}
46
+ .merge(parse_uri_query(parsed.query))
47
+ end
48
+ end
49
+
50
+ def self.parse_uri_query(query)
51
+ return {} unless query && !query.empty?
52
+ ::URI.decode_www_form(query).each_with_object({}) do |(key, value), acc|
53
+ next unless key
54
+ key = key.to_s.strip
55
+ next if key.empty?
56
+ acc[key.to_sym] = cast_uri_value(key, value)
57
+ end
58
+ end
59
+
60
+ def self.cast_uri_value(key, value)
61
+ case key
62
+ when 'buffer_max_items'
63
+ value.to_i
64
+ else
65
+ value
45
66
  end
46
67
  end
47
68
 
@@ -60,6 +81,7 @@ module LogStashLogger
60
81
  when :stderr then Stderr
61
82
  when :multi_delegator then MultiDelegator
62
83
  when :balancer then Balancer
84
+ when :http then HTTP
63
85
  else fail ArgumentError, 'Invalid device type'
64
86
  end
65
87
  end
@@ -4,19 +4,28 @@ require 'time'
4
4
 
5
5
  module LogStashLogger
6
6
  module Formatter
7
- HOST = ::Socket.gethostname
7
+ HOST = {
8
+ 'hostname' => ::Socket.gethostname,
9
+ 'ip' => Socket.ip_address_list.reject(&:ipv4_loopback?).reject(&:ipv6_loopback?).map(&:ip_address)
10
+ }.freeze
8
11
 
9
12
  class Base < ::Logger::Formatter
13
+ FAILED_TO_FORMAT_MSG = 'Failed to format log event'
14
+ attr_accessor :error_logger
10
15
  include ::LogStashLogger::TaggedLogging::Formatter
11
16
 
12
- def initialize(customize_event: nil)
17
+ def initialize(customize_event: nil, error_logger: LogStashLogger.configuration.default_error_logger)
13
18
  @customize_event = customize_event
19
+ @error_logger = error_logger
14
20
  super()
15
21
  end
16
22
 
17
23
  def call(severity, time, _progname, message)
18
24
  event = build_event(message, severity, time)
19
25
  format_event(event) unless event.cancelled?
26
+ rescue StandardError => e
27
+ log_error(e)
28
+ FAILED_TO_FORMAT_MSG
20
29
  end
21
30
 
22
31
  private
@@ -31,15 +40,24 @@ module LogStashLogger
31
40
  when LogStash::Event
32
41
  data.clone
33
42
  when Hash
34
- event_data = data.clone
35
- event_data['message'.freeze] = event_data.delete(:message) if event_data.key?(:message)
36
- event_data['tags'.freeze] = event_data.delete(:tags) if event_data.key?(:tags)
37
- event_data['source'.freeze] = event_data.delete(:source) if event_data.key?(:source)
38
- event_data['type'.freeze] = event_data.delete(:type) if event_data.key?(:type)
39
- event_data['@timestamp'.freeze] = time
43
+ event_data = { '@timestamp'.freeze => time }
44
+ data.each do |key, value|
45
+ case key
46
+ when :message, 'message'
47
+ event_data['message'.freeze] = value
48
+ when :tags, 'tags'
49
+ event_data['tags'.freeze] = value
50
+ when :source, 'source'
51
+ event_data['source'.freeze] = value
52
+ when :type, 'type'
53
+ event_data['type'.freeze] = value
54
+ else
55
+ event_data[key] = value
56
+ end
57
+ end
40
58
  LogStash::Event.new(event_data)
41
59
  else
42
- LogStash::Event.new("message".freeze => msg2str(data), "@timestamp".freeze => time)
60
+ LogStash::Event.new("@timestamp".freeze => time, "message".freeze => msg2str(data))
43
61
  end
44
62
 
45
63
  event['severity'.freeze] ||= severity
@@ -68,6 +86,33 @@ module LogStashLogger
68
86
  def format_event(event)
69
87
  event
70
88
  end
89
+
90
+ def force_utf8_encoding(event)
91
+ original_message = event.instance_variable_get(:@data)['message']
92
+ event.message = original_message.dup.force_encoding(Encoding::UTF_8).scrub
93
+ event
94
+ end
95
+
96
+ # Check if the message has encoding issues that would cause JSON serialization problems.
97
+ # This is needed because some Ruby implementations (e.g., JRuby) may not raise
98
+ # exceptions during JSON encoding but produce malformed output instead.
99
+ def message_has_encoding_issue?(event)
100
+ message = event.instance_variable_get(:@data)['message']
101
+ return false unless message.is_a?(String)
102
+
103
+ # Check if already valid UTF-8
104
+ return false if message.encoding == Encoding::UTF_8 && message.valid_encoding?
105
+
106
+ # Try to encode to UTF-8 to detect issues
107
+ message.encode(Encoding::UTF_8)
108
+ false
109
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
110
+ true
111
+ end
112
+
113
+ def log_error(e)
114
+ error_logger.error "[#{self.class}] #{e.class} - #{e.message}"
115
+ end
71
116
  end
72
117
  end
73
118
  end
@@ -9,7 +9,7 @@ module LogStashLogger
9
9
  private
10
10
 
11
11
  def build_facility(host)
12
- facility = host.dup
12
+ facility = host['hostname'.freeze].dup
13
13
  facility << " #{@progname}" if @progname
14
14
  facility
15
15
  end
@@ -4,7 +4,20 @@ module LogStashLogger
4
4
  private
5
5
 
6
6
  def format_event(event)
7
+ # Proactively check for encoding issues to handle cross-platform differences.
8
+ # Some Ruby implementations (e.g., JRuby) may not raise exceptions during
9
+ # JSON encoding but produce malformed output instead.
10
+ if message_has_encoding_issue?(event)
11
+ log_error(Encoding::InvalidByteSequenceError.new("Invalid encoding in message"))
12
+ return force_utf8_encoding(event).to_json
13
+ end
14
+
7
15
  event.to_json
16
+ rescue Encoding::UndefinedConversionError,
17
+ Encoding::InvalidByteSequenceError,
18
+ JSON::GeneratorError => e
19
+ log_error(e)
20
+ force_utf8_encoding(event).to_json
8
21
  end
9
22
  end
10
23
  end
@@ -4,7 +4,20 @@ module LogStashLogger
4
4
  private
5
5
 
6
6
  def format_event(event)
7
+ # Proactively check for encoding issues to handle cross-platform differences.
8
+ # Some Ruby implementations (e.g., JRuby) may not raise exceptions during
9
+ # JSON encoding but produce malformed output instead.
10
+ if message_has_encoding_issue?(event)
11
+ log_error(Encoding::InvalidByteSequenceError.new("Invalid encoding in message"))
12
+ return "#{force_utf8_encoding(event).to_json}\n"
13
+ end
14
+
7
15
  "#{event.to_json}\n"
16
+ rescue Encoding::UndefinedConversionError,
17
+ Encoding::InvalidByteSequenceError,
18
+ JSON::GeneratorError => e
19
+ log_error(e)
20
+ "#{force_utf8_encoding(event).to_json}\n"
8
21
  end
9
22
  end
10
23
  end
@@ -10,20 +10,20 @@ module LogStashLogger
10
10
  autoload :Cee, 'logstash-logger/formatter/cee'
11
11
  autoload :CeeSyslog, 'logstash-logger/formatter/cee_syslog'
12
12
 
13
- def self.new(formatter_type, customize_event: nil)
14
- build_formatter(formatter_type, customize_event)
13
+ def self.new(formatter_type, customize_event: nil, error_logger: LogStashLogger.configuration.default_error_logger)
14
+ build_formatter(formatter_type, customize_event, error_logger)
15
15
  end
16
16
 
17
- def self.build_formatter(formatter_type, customize_event)
17
+ def self.build_formatter(formatter_type, customize_event, error_logger)
18
18
  formatter_type ||= DEFAULT_FORMATTER
19
19
 
20
20
  formatter = if custom_formatter_instance?(formatter_type)
21
21
  formatter_type
22
22
  elsif custom_formatter_class?(formatter_type)
23
- formatter_type.new
23
+ initialize_custom_formatter_klass(formatter_type, customize_event)
24
24
  else
25
- formatter_klass(formatter_type).new(customize_event: customize_event)
26
- end
25
+ formatter_klass(formatter_type).new(customize_event: customize_event, error_logger: error_logger)
26
+ end
27
27
 
28
28
  formatter.send(:extend, ::LogStashLogger::TaggedLogging::Formatter)
29
29
  formatter
@@ -47,5 +47,13 @@ module LogStashLogger
47
47
  def self.custom_formatter_class?(formatter_type)
48
48
  formatter_type.is_a?(Class) && formatter_type.method_defined?(:call)
49
49
  end
50
+
51
+ def self.initialize_custom_formatter_klass(formatter_klass, customize_event)
52
+ if formatter_klass.instance_method(:initialize).parameters.include?([:key, :customize_event])
53
+ formatter_klass.new(customize_event: customize_event)
54
+ else
55
+ formatter_klass.new
56
+ end
57
+ end
50
58
  end
51
59
  end
@@ -26,31 +26,19 @@ module LogStashLogger
26
26
  end
27
27
  end
28
28
 
29
- protected
30
29
 
31
30
  def self.extract_opts(*args)
32
- args.flatten!
33
-
34
- if args.length > 1
35
- if args.all?{|arg| arg.is_a?(Hash)}
36
- # Deprecated array of hashes
37
- warn "[LogStashLogger] Passing an array of hashes to the constructor is deprecated. Please replace with an options hash: { type: :multi_delegator, outputs: [...] }"
38
- { type: :multi_delegator, outputs: args }
39
- else
40
- # Deprecated host/port/type constructor
41
- warn "[LogStashLogger] The (host, port, type) constructor is deprecated. Please use an options hash instead."
42
- host, port, type = *args
43
- { host: host, port: port, type: type }
44
- end
45
- elsif Hash === args[0]
31
+ if args.length == 1 && args[0].is_a?(Hash)
46
32
  args[0]
47
33
  else
48
- fail ArgumentError, "Invalid LogStashLogger options"
34
+ fail ArgumentError, "Invalid LogStashLogger options. Expected a single options hash."
49
35
  end
50
36
  end
51
37
 
52
38
  def self.build_logger(opts)
53
- formatter = Formatter.new(opts.delete(:formatter), customize_event: opts.delete(:customize_event))
39
+ formatter = Formatter.new(opts.delete(:formatter),
40
+ customize_event: opts.delete(:customize_event),
41
+ error_logger: opts.fetch(:error_logger, LogStashLogger.configuration.default_error_logger))
54
42
 
55
43
  logger_type = opts[:type].to_s.to_sym
56
44
  logger = case logger_type
@@ -60,13 +48,12 @@ module LogStashLogger
60
48
  build_syslog_logger(opts)
61
49
  else
62
50
  build_default_logger(opts)
63
- end
51
+ end
64
52
 
65
53
  logger.formatter = formatter if formatter
66
54
  logger
67
55
  end
68
56
 
69
- private
70
57
 
71
58
  def self.build_default_logger(opts)
72
59
  logger_class = opts.delete(:logger_class) || ::Logger
@@ -70,7 +70,7 @@ module LogStashLogger
70
70
  def method_missing(name, *args, &block)
71
71
  @loggers.each do |logger|
72
72
  if logger.respond_to?(name)
73
- logger.send(name, args, &block)
73
+ logger.send(name, *args, &block)
74
74
  end
75
75
  end
76
76
  end
@@ -101,6 +101,7 @@ module LogStashLogger
101
101
  logger.add(severity, message, progname, &block)
102
102
  end
103
103
  end
104
+ alias log add
104
105
 
105
106
  def <<(msg)
106
107
  @loggers.each do |logger|
@@ -25,7 +25,7 @@ module LogStashLogger
25
25
  # Convert logger options to OrderedOptions if regular Hash
26
26
  logger_options = ActiveSupport::OrderedOptions.new.merge(logger_options)
27
27
 
28
- if parsed_uri_options = LogStashLogger::Device.parse_uri_config(logger_options)
28
+ if (parsed_uri_options = LogStashLogger::Device.parse_uri_config(logger_options))
29
29
  logger_options.delete(:uri)
30
30
  logger_options.merge!(parsed_uri_options)
31
31
  end
@@ -19,7 +19,9 @@ module LogStashLogger
19
19
  end
20
20
 
21
21
  def push_tags(*tags)
22
- tags.flatten.reject{ |t| t.nil? || t.empty? }.tap do |new_tags|
22
+ non_empty_tags = tags.flatten.compact.reject { |t| t.respond_to?(:empty?) && t.empty? }
23
+
24
+ non_empty_tags.tap do |new_tags|
23
25
  current_tags.concat new_tags
24
26
  end
25
27
  end
@@ -1,3 +1,3 @@
1
1
  module LogStashLogger
2
- VERSION = "0.26.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -22,12 +22,14 @@ Gem::Specification.new do |gem|
22
22
 
23
23
  gem.add_development_dependency 'rails'
24
24
  gem.add_development_dependency 'redis'
25
- gem.add_development_dependency 'poseidon'
25
+ gem.add_development_dependency 'ruby-kafka'
26
26
  gem.add_development_dependency 'aws-sdk-kinesis'
27
27
  gem.add_development_dependency 'aws-sdk-firehose'
28
28
 
29
29
  if defined?(JRUBY_VERSION)
30
30
  gem.add_development_dependency 'SyslogLogger'
31
+ else
32
+ gem.add_development_dependency 'syslog'
31
33
  end
32
34
 
33
35
  gem.add_development_dependency 'rspec', '>= 3'
@@ -36,4 +38,10 @@ Gem::Specification.new do |gem|
36
38
  gem.add_development_dependency 'wwtd'
37
39
  gem.add_development_dependency 'appraisal'
38
40
  gem.add_development_dependency 'rubocop'
41
+ gem.add_development_dependency 'rubocop-performance'
42
+ gem.add_development_dependency 'rubocop-rails'
43
+ gem.add_development_dependency 'rubocop-rake'
44
+ gem.add_development_dependency 'rubocop-rspec'
45
+
46
+ gem.required_ruby_version = '>= 3.2'
39
47
  end
@@ -7,6 +7,71 @@ describe LogStashLogger::Device::File do
7
7
  expect(file_device.to_io).to be_a ::File
8
8
  end
9
9
 
10
+ context "with shift options" do
11
+ let(:shift_file) do
12
+ temp = Tempfile.new('logstash_shift')
13
+ temp.close
14
+ temp
15
+ end
16
+
17
+ let(:shift_device) do
18
+ LogStashLogger::Device.new(
19
+ type: :file,
20
+ path: shift_file.path,
21
+ shift_age: 1,
22
+ shift_size: 1
23
+ )
24
+ end
25
+
26
+ let(:period_file) do
27
+ temp = Tempfile.new('logstash_period_shift')
28
+ temp.close
29
+ temp
30
+ end
31
+
32
+ let(:period_device) do
33
+ LogStashLogger::Device.new(
34
+ type: :file,
35
+ path: period_file.path,
36
+ shift_age: 'daily',
37
+ shift_period_suffix: '%Y-%m-%d'
38
+ )
39
+ end
40
+
41
+ after do
42
+ shift_device.close
43
+ ::Dir.glob("#{shift_file.path}*").each do |path|
44
+ ::File.delete(path) if ::File.exist?(path)
45
+ end
46
+
47
+ period_device.close
48
+ ::Dir.glob("#{period_file.path}*").each do |path|
49
+ ::File.delete(path) if ::File.exist?(path)
50
+ end
51
+ end
52
+
53
+ it "wraps a Logger::LogDevice for rotation" do
54
+ expect(shift_device.io).to be_a ::Logger::LogDevice
55
+ expect(shift_device.to_io).to be_a ::File
56
+ end
57
+
58
+ it "rotates when shift_size is exceeded" do
59
+ shift_device.write("a" * 10)
60
+ shift_device.write("b" * 10)
61
+ shift_device.close
62
+
63
+ expect(::File.exist?("#{shift_file.path}.0")).to be(true)
64
+ end
65
+
66
+ it "uses shift_period_suffix for time-based rotation" do
67
+ period_device.write("a")
68
+ period_device.io.send(:shift_log_period, Time.new(2026, 1, 22))
69
+ period_device.close
70
+
71
+ expect(::File.exist?("#{period_file.path}.2026-01-22")).to be(true)
72
+ end
73
+ end
74
+
10
75
  context "when path is not specified" do
11
76
  it "raises an exception" do
12
77
  expect { described_class.new }.to raise_error(ArgumentError)
@@ -0,0 +1,11 @@
1
+ require 'logstash-logger'
2
+
3
+ describe LogStashLogger::Device::HTTP do
4
+ include_context 'device'
5
+
6
+ it "Post event to HTTP" do
7
+ expect(Net::HTTP).to receive(:post)
8
+ http_device.write('test')
9
+ end
10
+
11
+ end