Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Obey Syslog size limits #9

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion Rakefile
Expand Up @@ -60,7 +60,7 @@ task :coverage do
sh "open coverage/index.html"
end

require 'rake/rdoctask'
require 'rdoc/task'
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "#{name} #{version}"
Expand Down
18 changes: 18 additions & 0 deletions lib/remote_syslog_logger/limit_bytesize.rb
@@ -0,0 +1,18 @@
# Adapted from http://stackoverflow.com/a/12536366/2778142
def limit_bytesize(str, size)
# Change to canonical unicode form (compose any decomposed characters).
# Works only if you're using active_support
str = str.mb_chars.compose.to_s if str.respond_to?(:mb_chars)

# Start with a string of the correct byte size, but
# with a possibly incomplete char at the end.
new_str = str.byteslice(0, size)

# We need to force_encoding from utf-8 to utf-8 so ruby will re-validate
# (idea from halfelf).
until new_str[-1].force_encoding(new_str.encoding).valid_encoding?
# remove the invalid char
new_str = new_str.slice(0..-2)
end
new_str
end
26 changes: 22 additions & 4 deletions lib/remote_syslog_logger/udp_sender.rb
@@ -1,13 +1,16 @@
require 'socket'
require 'syslog_protocol'
require File.expand_path('../limit_bytesize', __FILE__)

module RemoteSyslogLogger
class UdpSender
def initialize(remote_hostname, remote_port, options = {})
@remote_hostname = remote_hostname
@remote_port = remote_port
@whinyerrors = options[:whinyerrors]

@max_packet_size = options[:max_packet_size] || 1024
@continuation_prefix = options[:continuation_prefix] || '... '

@socket = UDPSocket.new
@packet = SyslogProtocol::Packet.new

Expand All @@ -17,16 +20,31 @@ def initialize(remote_hostname, remote_port, options = {})

@packet.facility = options[:facility] || 'user'
@packet.severity = options[:severity] || 'notice'
@packet.tag = options[:program] || "#{File.basename($0)}[#{$$}]"
@packet.tag = options[:program] || default_tag
end

def default_tag
pid_suffix = "[#{$$}]"
max_basename_size = 32 - pid_suffix.size
"#{File.basename($0)}"[0...max_basename_size].gsub(/[^\x21-\x7E]/, '_') + pid_suffix
end

def transmit(message)
message.split(/\r?\n/).each do |line|
begin
next if line =~ /^\s*$/
packet = @packet.dup
packet.content = line
@socket.send(packet.assemble, 0, @remote_hostname, @remote_port)
max_content_size = @max_packet_size - packet.assemble(@max_packet_size).size
line_prefix = ''
remaining_line = line
until remaining_line.empty?
chunk_byte_size = max_content_size - line_prefix.bytesize
chunk = limit_bytesize(remaining_line, chunk_byte_size)
packet.content = line_prefix + chunk
@socket.send(packet.assemble(@max_packet_size), 0, @remote_hostname, @remote_port)
remaining_line = remaining_line[chunk.size..-1]
line_prefix = @continuation_prefix
end
rescue
$stderr.puts "#{self.class} error: #{$!.class}: #{$!}\nOriginal message: #{line}"
raise if @whinyerrors
Expand Down
4 changes: 4 additions & 0 deletions remote_syslog_logger.gemspec
Expand Up @@ -49,6 +49,10 @@ Gem::Specification.new do |s|
## List your runtime dependencies here. Runtime dependencies are those
## that are needed for an end user to actually USE your code.
s.add_dependency('syslog_protocol')
s.add_dependency('activesupport', '>= 3.2.14')

s.add_development_dependency('rake')
s.add_development_dependency('test-unit')

## List your development dependencies here. Development dependencies are
## those that are only needed during development
Expand Down
2 changes: 1 addition & 1 deletion test/helper.rb
Expand Up @@ -9,4 +9,4 @@

require 'remote_syslog_logger'

require 'test/unit'
require 'minitest/autorun'
136 changes: 134 additions & 2 deletions test/test_remote_syslog_logger.rb
@@ -1,6 +1,9 @@
# encoding: utf-8

require File.expand_path('../helper', __FILE__)
require File.expand_path('../../lib/remote_syslog_logger/limit_bytesize', __FILE__)

class TestRemoteSyslogLogger < Test::Unit::TestCase
class TestRemoteSyslogLogger < MiniTest::Test
def setup
@server_port = rand(50000) + 1024
@socket = UDPSocket.new
Expand All @@ -25,4 +28,133 @@ def test_logger_multiline
message, addr = *@socket.recvfrom(1024)
assert_match /This is the second line/, message
end
end

def test_logger_default_tag
$0 = 'foo'
logger = RemoteSyslogLogger.new('127.0.0.1', @server_port)
logger.info ""

message, addr = *@socket.recvfrom(1024)
assert_match "foo[#{$$}]: I,", message
end

def test_logger_long_default_tag
$0 = 'x' * 64
pid_suffix = "[#{$$}]"
logger = RemoteSyslogLogger.new('127.0.0.1', @server_port)
logger.info ""

message, addr = *@socket.recvfrom(1024)
assert_match 'x' * (32 - pid_suffix.size) + pid_suffix + ': I,', message
end

TEST_TAG = 'foo'
TEST_HOSTNAME = 'bar'
TEST_FACILITY = 'user'
TEST_SEVERITY = 'notice'
TEST_MESSAGE = "abcdefg✓" * 512
TEST_MESSAGE_ASCII8 = "abcdefg".force_encoding('ASCII')

def test_logger_long_message
_test_msg_splitting_with(
tag: TEST_TAG,
hostname: TEST_HOSTNAME,
severity: TEST_SEVERITY,
facility: TEST_FACILITY,
message: TEST_MESSAGE,
max_packet_size: nil,
continuation_prefix: nil)
end

def test_logger_long_message_custom_packet_size
_test_msg_splitting_with(
tag: TEST_TAG,
hostname: TEST_HOSTNAME,
severity: TEST_SEVERITY,
facility: TEST_FACILITY,
message: TEST_MESSAGE,
max_packet_size: 2048,
continuation_prefix: nil)
end

def test_logger_long_message_custom_continuation
_test_msg_splitting_with(
tag: TEST_TAG,
hostname: TEST_HOSTNAME,
severity: TEST_SEVERITY,
facility: TEST_FACILITY,
message: TEST_MESSAGE,
max_packet_size: nil,
continuation_prefix: 'frobnicate')
end

def test_logger_ascii8_message
_test_msg_splitting_with(
tag: TEST_TAG,
hostname: TEST_HOSTNAME,
severity: TEST_SEVERITY,
facility: TEST_FACILITY,
message: TEST_MESSAGE_ASCII8,
max_packet_size: nil,
continuation_prefix: nil)
end

def test_logger_empty_message
_test_msg_splitting_with(
tag: TEST_TAG,
hostname: TEST_HOSTNAME,
severity: TEST_SEVERITY,
facility: TEST_FACILITY,
message: '',
max_packet_size: nil,
continuation_prefix: nil)
end

private

class MessageOnlyFormatter < ::Logger::Formatter
def call(severity, timestamp, progname, msg)
msg
end
end

def _test_msg_splitting_with(options)
logger = RemoteSyslogLogger.new('127.0.0.1', @server_port,
program: options[:tag],
local_hostname: options[:hostname],
severity: options[:severity],
facility: options[:facility],
max_packet_size: options[:max_packet_size],
continuation_prefix: options[:continuation_prefix])
logger.formatter = MessageOnlyFormatter.new
logger.info options[:message]

packet_size = options[:max_packet_size] || 1024
continuation_prefix = options[:continuation_prefix] || '... '

test_packet = SyslogProtocol::Packet.new
test_packet.hostname = options[:hostname]
test_packet.tag = options[:tag]
test_packet.severity = options[:severity]
test_packet.facility = options[:facility]
test_packet.content = ''
max_content_size = packet_size - test_packet.assemble.size

line_prefix = ''
remaining_message = options[:message]
reassembled_message = ''
until remaining_message.empty?
chunk_size = max_content_size - line_prefix.bytesize
chunk = limit_bytesize(remaining_message, chunk_size)
message, = *@socket.recvfrom(packet_size * 2)
message.force_encoding('UTF-8')
match = Regexp.new(
': ' + line_prefix + '(' + Regexp.escape(chunk) + ')$').match(message)
assert !match.nil?
reassembled_message += match[1]
remaining_message = remaining_message[chunk.size..-1]
line_prefix = continuation_prefix
end
assert_equal(reassembled_message, options[:message])
end
end