├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── remote_syslog_logger.rb └── remote_syslog_logger │ └── udp_sender.rb ├── remote_syslog_logger.gemspec └── test ├── helper.rb └── test_remote_syslog_logger.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | .bundle 3 | pkg 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Eric Lindvall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote Syslog Logger 2 | 3 | This library providers an [ActiveSupport][] compatible logger that logs 4 | directly to a remote syslogd via UDP. 5 | 6 | [ActiveSupport]: http://as.rubyonrails.org/ 7 | 8 | 9 | # Installation 10 | 11 | The easiest way to install `remote_syslog_logger` is with Bundler. Add 12 | `remote_syslog_logger` to your `Gemfile`. 13 | 14 | If you are not using a `Gemfile`, run: 15 | 16 | $ [sudo] gem install remote_syslog_logger 17 | 18 | 19 | # Usage 20 | 21 | Use from Rails: 22 | 23 | config.logger = RemoteSyslogLogger.new('syslog.domain.com', 514, 24 | :program => "rails-#{RAILS_ENV}", 25 | :local_hostname => "optional_hostname") 26 | 27 | With Rails 3+ if you want to use tagged logging wrap in a `TaggedLogging` instance: 28 | 29 | config.logger = ActiveSupport::TaggedLogging.new( 30 | RemoteSyslogLogger.new( 31 | 'syslog.domain.com', 514, 32 | :program => "rails-#{RAILS_ENV}", 33 | :local_hostname => "optional_hostname" 34 | ) 35 | ) 36 | 37 | Use from Ruby: 38 | 39 | require 'remote_syslog_logger' 40 | $logger = RemoteSyslogLogger.new('syslog.domain.com', 514) 41 | 42 | To point the logs to your local system, use `localhost` and ensure that 43 | the system's syslog daemon is bound to `127.0.0.1`. 44 | 45 | 46 | # Source 47 | 48 | Remote Syslog Logger is available on GitHub, which can be browsed at: 49 | 50 | 51 | 52 | and cloned with: 53 | 54 | $ git clone git://github.com/papertrail/remote_syslog_logger.git 55 | 56 | 57 | # Limitations 58 | 59 | If the specified host cannot be resolved, `syslog.domain.com` in the 60 | example under the usage section above, `remote_syslog_logger` will block 61 | for approximately 20 seconds before displaying an error. This could 62 | result in the application failing to start or even stopping responding. 63 | 64 | Workarounds for this include: 65 | 66 | * use an IP address instead of a hostname. 67 | * put a hosts entry in `/etc/hosts` or equivalent, so that DNS is not 68 | actually consulted 69 | * instead of logging directly to the network, write to a file and 70 | transmit new entries with a standalone daemon like 71 | [remote_syslog](https://github.com/papertrail/remote_syslog), 72 | 73 | ## Message length 74 | 75 | All log lines are truncated to a maximum of 1024 characters. This restriction 76 | comes from [RFC 3164 section 4.1][rfc-limit]: 77 | 78 | > The total length of the packet MUST be 1024 bytes or less. 79 | 80 | Additionally, the generally-accepted [MTU][] of the Internet is 1500 bytes, so 81 | regardless of the RFC, UDP syslog packets longer than 1500 bytes would not 82 | arrive. For details or to use TCP syslog for longer messages, see 83 | [help.papertrailapp.com][troubleshoot]. 84 | 85 | There is a `max_size` option to override this restriction, but it should only be 86 | used in extraordinary circumstances. Oversize messages are more likely to be 87 | fragmented and lost, with some receivers rejecting them entirely. 88 | 89 | [rfc-limit]: https://tools.ietf.org/html/rfc3164#section-4.1 90 | [MTU]: (https://en.wikipedia.org/wiki/Maximum_transmission_unit) 91 | [troubleshoot]: http://help.papertrailapp.com/kb/configuration/troubleshooting-remote-syslog-reachability/#message-length 92 | 93 | 94 | ## Default program name 95 | 96 | By default, the `program` value is set to the name and ID of the invoking 97 | process. For example, `puma[12345]` or `rack[3456]`. 98 | 99 | The `program` value is used to populate the syslog "tag" field, must be 32 100 | or fewer characters. In a few cases, an artifact of how the app is launched 101 | may lead to a default `program` value longer than 32 characters. For example, 102 | the `thin` Web server may generate a default `program` value such 103 | as: 104 | 105 | thin server (0.0.0.0:3001)[11179] 106 | 107 | If this occurs, the following exception will be raised when a 108 | `RemoteSyslogLogger` is instantiated: 109 | 110 | Tag must not be longer than 32 characters (ArgumentError) 111 | 112 | To remedy this, explicitly provide a `program` argument which is shorter than 113 | 32 characters. See [Usage](#usage). 114 | 115 | 116 | # Contributing 117 | 118 | Once you've made your great commits: 119 | 120 | 1. [Fork][fk] `remote_syslog_logger` 121 | 2. Create a topic branch - `git checkout -b my_branch` 122 | 3. Push to your branch - `git push origin my_branch` 123 | 4. Create a Pull Request or an [Issue][is] with a link to your branch 124 | 5. That's it! 125 | 126 | You might want to checkout Resque's [Contributing][cb] wiki page for information 127 | on coding standards, new features, etc. 128 | 129 | 130 | # License 131 | 132 | Copyright (c) 2011-2016 Eric Lindvall. See [LICENSE][] for details. 133 | 134 | 135 | [cb]: https://wiki.github.com/defunkt/resque/contributing 136 | [fk]: http://help.github.com/forking/ 137 | [is]: https://github.com/papertrail/remote_syslog_logger/issues 138 | [LICENSE]: https://github.com/papertrail/remote_syslog_logger/blob/master/LICENSE 139 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | 5 | ############################################################################# 6 | # 7 | # Helper functions 8 | # 9 | ############################################################################# 10 | 11 | def name 12 | @name ||= Dir['*.gemspec'].first.split('.').first 13 | end 14 | 15 | def version 16 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 17 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 18 | end 19 | 20 | def date 21 | Date.today.to_s 22 | end 23 | 24 | def rubyforge_project 25 | name 26 | end 27 | 28 | def gemspec_file 29 | "#{name}.gemspec" 30 | end 31 | 32 | def gem_file 33 | "#{name}-#{version}.gem" 34 | end 35 | 36 | def replace_header(head, header_name) 37 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 38 | end 39 | 40 | ############################################################################# 41 | # 42 | # Standard tasks 43 | # 44 | ############################################################################# 45 | 46 | task :default => :test 47 | 48 | require 'rake/testtask' 49 | Rake::TestTask.new(:test) do |test| 50 | test.libs << 'lib' << 'test' 51 | test.pattern = 'test/**/test_*.rb' 52 | test.verbose = true 53 | end 54 | 55 | desc "Generate RCov test coverage and open in your browser" 56 | task :coverage do 57 | require 'rcov' 58 | sh "rm -fr coverage" 59 | sh "rcov test/test_*.rb" 60 | sh "open coverage/index.html" 61 | end 62 | 63 | require 'rdoc/task' 64 | RDoc::Task.new do |rdoc| 65 | rdoc.rdoc_dir = 'rdoc' 66 | rdoc.title = "#{name} #{version}" 67 | rdoc.rdoc_files.include('README*') 68 | rdoc.rdoc_files.include('lib/**/*.rb') 69 | end 70 | 71 | desc "Open an irb session preloaded with this library" 72 | task :console do 73 | sh "irb -rubygems -r ./lib/#{name}.rb" 74 | end 75 | 76 | ############################################################################# 77 | # 78 | # Custom tasks (add your own tasks here) 79 | # 80 | ############################################################################# 81 | 82 | 83 | 84 | ############################################################################# 85 | # 86 | # Packaging tasks 87 | # 88 | ############################################################################# 89 | 90 | desc "Create tag v#{version} and build and push #{gem_file} to Rubygems" 91 | task :release => :build do 92 | unless `git branch` =~ /^\* master$/ 93 | puts "You must be on the master branch to release!" 94 | exit! 95 | end 96 | sh "git commit --allow-empty -a -m 'Release #{version}'" 97 | sh "git tag v#{version}" 98 | sh "git push origin master" 99 | sh "git push origin v#{version}" 100 | sh "gem push pkg/#{name}-#{version}.gem" 101 | end 102 | 103 | desc "Build #{gem_file} into the pkg directory" 104 | task :build => :gemspec do 105 | sh "mkdir -p pkg" 106 | sh "gem build #{gemspec_file}" 107 | sh "mv #{gem_file} pkg" 108 | end 109 | 110 | desc "Generate #{gemspec_file}" 111 | task :gemspec => :validate do 112 | # read spec file and split out manifest section 113 | spec = File.read(gemspec_file) 114 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 115 | 116 | # replace name version and date 117 | replace_header(head, :name) 118 | replace_header(head, :version) 119 | replace_header(head, :date) 120 | #comment this out if your rubyforge_project has a different name 121 | replace_header(head, :rubyforge_project) 122 | 123 | # determine file list from git ls-files 124 | files = `git ls-files`. 125 | split("\n"). 126 | sort. 127 | reject { |file| file =~ /^\./ }. 128 | reject { |file| file =~ /^(rdoc|pkg)/ }. 129 | map { |file| " #{file}" }. 130 | join("\n") 131 | 132 | # piece file back together and write 133 | manifest = " s.files = %w[\n#{files}\n ]\n" 134 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 135 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 136 | puts "Updated #{gemspec_file}" 137 | end 138 | 139 | desc "Validate #{gemspec_file}" 140 | task :validate do 141 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 142 | unless libfiles.empty? 143 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 144 | exit! 145 | end 146 | unless Dir['VERSION*'].empty? 147 | puts "A `VERSION` file at root level violates Gem best practices." 148 | exit! 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/remote_syslog_logger.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'remote_syslog_logger/udp_sender' 3 | require 'logger' 4 | 5 | module RemoteSyslogLogger 6 | VERSION = '1.0.4' 7 | 8 | def self.new(remote_hostname, remote_port, options = {}) 9 | Logger.new(RemoteSyslogLogger::UdpSender.new(remote_hostname, remote_port, options)) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/remote_syslog_logger/udp_sender.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'syslog_protocol' 3 | 4 | module RemoteSyslogLogger 5 | class UdpSender 6 | def initialize(remote_hostname, remote_port, options = {}) 7 | @remote_hostname = remote_hostname 8 | @remote_port = remote_port 9 | @whinyerrors = options[:whinyerrors] 10 | @max_size = options[:max_size] 11 | 12 | @socket = UDPSocket.new 13 | @packet = SyslogProtocol::Packet.new 14 | 15 | local_hostname = options[:local_hostname] || (Socket.gethostname rescue `hostname`.chomp) 16 | local_hostname = 'localhost' if local_hostname.nil? || local_hostname.empty? 17 | @packet.hostname = local_hostname 18 | 19 | @packet.facility = options[:facility] || 'user' 20 | @packet.severity = options[:severity] || 'notice' 21 | @packet.tag = options[:program] || "#{File.basename($0)}[#{$$}]" 22 | end 23 | 24 | def transmit(message) 25 | message.split(/\r?\n/).each do |line| 26 | begin 27 | next if line =~ /^\s*$/ 28 | packet = @packet.dup 29 | packet.content = line 30 | payload = @max_size ? packet.assemble(@max_size) : packet.assemble 31 | @socket.send(payload, 0, @remote_hostname, @remote_port) 32 | rescue 33 | $stderr.puts "#{self.class} error: #{$!.class}: #{$!}\nOriginal message: #{line}" 34 | raise if @whinyerrors 35 | end 36 | end 37 | end 38 | 39 | # Make this act a little bit like an `IO` object 40 | alias_method :write, :transmit 41 | 42 | def close 43 | @socket.close 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /remote_syslog_logger.gemspec: -------------------------------------------------------------------------------- 1 | ## This is the rakegem gemspec template. Make sure you read and understand 2 | ## all of the comments. Some sections require modification, and others can 3 | ## be deleted if you don't need them. Once you understand the contents of 4 | ## this file, feel free to delete any comments that begin with two hash marks. 5 | ## You can find comprehensive Gem::Specification documentation, at 6 | ## http://docs.rubygems.org/read/chapter/20 7 | Gem::Specification.new do |s| 8 | s.specification_version = 2 if s.respond_to? :specification_version= 9 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 10 | s.rubygems_version = '1.3.5' 11 | 12 | ## Leave these as is they will be modified for you by the rake gemspec task. 13 | ## If your rubyforge_project name is different, then edit it and comment out 14 | ## the sub! line in the Rakefile 15 | s.name = 'remote_syslog_logger' 16 | s.version = '1.0.4' 17 | s.date = '2018-10-10' 18 | # s.rubyforge_project = 'remote_syslog_logger' 19 | 20 | ## Make sure your summary is short. The description may be as long 21 | ## as you like. 22 | s.summary = "Ruby Logger that sends directly to a remote syslog endpoint" 23 | s.description = "A ruby Logger that sends UDP directly to a remote syslog endpoint" 24 | 25 | ## List the primary authors. If there are a bunch of authors, it's probably 26 | ## better to set the email to an email list or something. If you don't have 27 | ## a custom homepage, consider using your GitHub URL or the like. 28 | s.authors = ["Eric Lindvall"] 29 | s.email = 'eric@5stops.com' 30 | s.homepage = 'https://github.com/papertrail/remote_syslog_logger' 31 | 32 | ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as 33 | ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb' 34 | s.require_paths = %w[lib] 35 | 36 | ## This sections is only necessary if you have C extensions. 37 | # s.require_paths << 'ext' 38 | # s.extensions = %w[ext/extconf.rb] 39 | 40 | ## If your gem includes any executables, list them here. 41 | # s.executables = ["name"] 42 | # s.default_executable = 'name' 43 | 44 | ## Specify any RDoc options here. You'll want to add your README and 45 | ## LICENSE files to the extra_rdoc_files list. 46 | s.rdoc_options = ["--charset=UTF-8"] 47 | s.extra_rdoc_files = %w[README.md LICENSE] 48 | 49 | ## List your runtime dependencies here. Runtime dependencies are those 50 | ## that are needed for an end user to actually USE your code. 51 | s.add_dependency('syslog_protocol') 52 | 53 | ## List your development dependencies here. Development dependencies are 54 | ## those that are only needed during development 55 | # s.add_development_dependency('DEVDEPNAME', [">= 1.1.0", "< 2.0.0"]) 56 | 57 | ## Leave this section as-is. It will be automatically generated from the 58 | ## contents of your Git repository via the gemspec task. DO NOT REMOVE 59 | ## THE MANIFEST COMMENTS, they are used as delimiters by the task. 60 | # = MANIFEST = 61 | s.files = %w[ 62 | Gemfile 63 | LICENSE 64 | README.md 65 | Rakefile 66 | lib/remote_syslog_logger.rb 67 | lib/remote_syslog_logger/udp_sender.rb 68 | remote_syslog_logger.gemspec 69 | test/helper.rb 70 | test/test_remote_syslog_logger.rb 71 | ] 72 | # = MANIFEST = 73 | 74 | ## Test files will be grabbed from the file list. Make sure the path glob 75 | ## matches what you actually use. 76 | s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ } 77 | end 78 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | unless ENV['BUNDLE_GEMFILE'] 4 | require 'rubygems' 5 | require 'bundler' 6 | Bundler.setup 7 | Bundler.require 8 | end 9 | 10 | require 'remote_syslog_logger' 11 | 12 | require 'test/unit' 13 | -------------------------------------------------------------------------------- /test/test_remote_syslog_logger.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | class TestRemoteSyslogLogger < Test::Unit::TestCase 4 | def setup 5 | @server_port = rand(50000) + 1024 6 | @socket = UDPSocket.new 7 | @socket.bind('127.0.0.1', @server_port) 8 | end 9 | 10 | def test_logger 11 | @logger = RemoteSyslogLogger.new('127.0.0.1', @server_port) 12 | @logger.info "This is a test" 13 | 14 | message, _ = *@socket.recvfrom(1024) 15 | assert_match "This is a test", message 16 | end 17 | 18 | def test_logger_multiline 19 | @logger = RemoteSyslogLogger.new('127.0.0.1', @server_port) 20 | @logger.info "This is a test\nThis is the second line" 21 | 22 | message, _ = *@socket.recvfrom(1024) 23 | assert_match "This is a test", message 24 | 25 | message, _ = *@socket.recvfrom(1024) 26 | assert_match "This is the second line", message 27 | end 28 | end 29 | --------------------------------------------------------------------------------