├── .gemtest ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── benchmarks └── notifier.rb ├── gelf.gemspec ├── lib ├── gelf.rb └── gelf │ ├── logger.rb │ ├── notifier.rb │ ├── severity.rb │ └── transport │ ├── tcp.rb │ ├── tcp_tls.rb │ └── udp.rb └── test ├── helper.rb ├── test_logger.rb ├── test_notifier.rb ├── test_ruby_sender.rb └── test_severity.rb /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graylog-labs/gelf-rb/eb2d31cdc4b37c316de880122279bcac52a08ba2/.gemtest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | coverage/ 3 | rdoc/ 4 | tmp/ 5 | 6 | .DS_Store 7 | *.gem 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - 2.1.0 5 | - 2.2.0 6 | - 2.3.0 7 | - 2.4.0 8 | - jruby-19mode 9 | deploy: 10 | provider: rubygems 11 | api_key: 12 | secure: Pm1yAqo2ldet/Fd2jK5dKZkDNmyx5KkYtViL+eOR4Rxs82dwqOcCSyrBL6BOzuRfJtpjgXUX6FDapKgLDYW/2EDW8mmiciOL0WLzxYQsmJXcerJeUl2tTeFKpyEf+8aaDQpAlsK0m1dPmQjCJF9MWsa7m3oshm3mJ1VITY90chM= 13 | gem: gelf 14 | gemspec: gelf.gemspec 15 | on: 16 | tags: true 17 | repo: graylog-labs/gelf-rb 18 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 3.0.0, 2016-08-21 2 | + Overhaul TCP support 3 | + Include automatic support for Celluloid::IO if available 4 | + Add TLS support to TCP transport 5 | - Remove support for ancient Rubygems versions 6 | - Remove support for Ruby 1.9.2 (1.9.3 works!) 7 | - Remove already-deprecated `host` and `port` methods on GELF::Notifier 8 | 9 | 2.0.0, 2016-02-02 10 | + Added GELF TCP support. 11 | 12 | 1.3.2, 2011-12-02: 13 | * support for rubygems-test. 14 | * rescue from more network errors. 15 | 16 | 1.3.1, 2011-10-28: 17 | + allow to rescue from network errors. 18 | 19 | 1.3.0, 2011-07-27: 20 | + allow to set timestamp manually. 21 | 22 | 1.2.0.beta1, 2011-05-23: 23 | + compatibility with GELF specification 1.0: 24 | * requires modern graylog2-server and graylog2-web-interface; 25 | + Notifier#default_options, Notifier#default_options=; 26 | + severity (level) threshold; 27 | + automatically set 'file', 'line' and 'timestamp' fields; 28 | + wrappers for GELF::Notifier#notify with severity: 29 | + GELF::Notifier.debug 30 | + GELF::Notifier.info 31 | + GELF::Notifier.warn 32 | + GELF::Notifier.error 33 | + GELF::Notifier.fatal 34 | + GELF::Notifier.unknown 35 | + full compatibility with Ruby Logger and other loggers: 36 | + GELF::Logger#fatal { "Argument 'foo' not given." } 37 | + GELF::Logger#error "Argument #{ @foo } mismatch." 38 | + GELF::Logger#info('initialize') { "Initializing..." } 39 | + GELF::Logger#add(GELF::FATAL) { 'Fatal error!' } 40 | + GELF::Logger#close 41 | + GELF::Logger#level = GELF::INFO 42 | + allow to change severity mapping; 43 | + send messages to receivers in round-robin; 44 | * GELF::Notifier#host and #port are attr_readers now and deprecated (were attr_accessor); 45 | + allow to disable file and line collection (GELF::Notifier#collect_file_and_line = false); 46 | - deprecated Gelf class removed. 47 | 48 | 1.0.2, 2010-11-29: 49 | 1.0.1, 2010-11-29: 50 | - added more tests for chunking in attempt to locate not existing bug. 51 | 52 | 1.0.0, 2010-11-10: 53 | + initial stable version; 54 | * deprecated Gelf class is still there. 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please follow [the instructions on graylog.org](https://www.graylog.org/contributing-to-graylog/). 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development do 4 | gem "shoulda", "~> 2.11.3" 5 | gem "jeweler", "~> 2.1.1" 6 | # Because of a dependency chain jeweler->github_api->oauth2->rack, 7 | # pin the version: Rack 2.0.x doesn't work on < Ruby 2.2 8 | gem 'rack', '< 2.0' 9 | gem "mocha", "~> 1.1.0" 10 | gem "test-unit", "~> 3.2.0" 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.4.0) 5 | builder (3.2.2) 6 | descendants_tracker (0.0.4) 7 | thread_safe (~> 0.3, >= 0.3.1) 8 | faraday (0.9.2) 9 | multipart-post (>= 1.2, < 3) 10 | git (1.3.0) 11 | github_api (0.14.5) 12 | addressable (~> 2.4.0) 13 | descendants_tracker (~> 0.0.4) 14 | faraday (~> 0.8, < 0.10) 15 | hashie (>= 3.4) 16 | oauth2 (~> 1.0) 17 | hashie (3.4.4) 18 | highline (1.7.8) 19 | jeweler (2.1.1) 20 | builder 21 | bundler (>= 1.0) 22 | git (>= 1.2.5) 23 | github_api 24 | highline (>= 1.6.15) 25 | nokogiri (>= 1.5.10) 26 | rake 27 | rdoc 28 | semver 29 | json (1.8.6) 30 | json (1.8.6-java) 31 | jwt (1.5.4) 32 | metaclass (0.0.4) 33 | mini_portile2 (2.1.0) 34 | mocha (1.1.0) 35 | metaclass (~> 0.0.1) 36 | multi_json (1.12.1) 37 | multi_xml (0.5.5) 38 | multipart-post (2.0.0) 39 | nokogiri (1.6.8) 40 | mini_portile2 (~> 2.1.0) 41 | pkg-config (~> 1.1.7) 42 | nokogiri (1.6.8-java) 43 | oauth2 (1.2.0) 44 | faraday (>= 0.8, < 0.10) 45 | jwt (~> 1.0) 46 | multi_json (~> 1.3) 47 | multi_xml (~> 0.5) 48 | rack (>= 1.2, < 3) 49 | pkg-config (1.1.7) 50 | power_assert (0.3.0) 51 | rack (1.6.4) 52 | rake (11.2.2) 53 | rdoc (4.2.2) 54 | json (~> 1.4) 55 | semver (1.0.1) 56 | shoulda (2.11.3) 57 | test-unit (3.2.1) 58 | power_assert 59 | thread_safe (0.3.5) 60 | thread_safe (0.3.5-java) 61 | 62 | PLATFORMS 63 | java 64 | ruby 65 | 66 | DEPENDENCIES 67 | jeweler (~> 2.1.1) 68 | mocha (~> 1.1.0) 69 | rack (< 2.0) 70 | shoulda (~> 2.11.3) 71 | test-unit (~> 3.2.0) 72 | 73 | BUNDLED WITH 74 | 1.14.6 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2016 Lennart Koopmann, Alexey Palazhchenko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GELF Ruby library 2 | 3 | This is the new GELF gem written by Alexey Palazhchenko. It is based on the old gem by Lennart Koopmann and allows you to send GELF messages to Graylog or Logstash instances. See the [GELF specification](http://docs.graylog.org/en/stable/pages/gelf.html) for more information about GELF and [RDoc](http://rdoc.info/github/graylog-labs/gelf-rb/master/frames) for API documentation. 4 | 5 | Tested with Ruby 1.9, 2.0, 2.1, 2.2, 2.3 and 2.4. 6 | 7 | [![Build Status](https://travis-ci.org/graylog-labs/gelf-rb.svg?branch=master)](https://travis-ci.org/graylog-labs/gelf-rb) 8 | [![Code Climate](https://codeclimate.com/github/graylog-labs/gelf-rb/badges/gpa.svg)](https://codeclimate.com/github/graylog-labs/gelf-rb) 9 | 10 | ## Usage 11 | ### Gelf::Notifier 12 | 13 | This allows you to send arbitary messages via UDP to Graylog. 14 | 15 | n = GELF::Notifier.new("localhost", 12201) 16 | 17 | # Send with custom attributes and an additional parameter "foo" 18 | n.notify!(:short_message => "foo", :full_message => "something here\n\nbacktrace?!", :_foo => "bar") 19 | 20 | # Pass any object that responds to .to_hash 21 | n.notify!(Exception.new) 22 | 23 | The recommended default is to send via UDP but you can choose to send via TCP like this: 24 | 25 | n = GELF::Notifier.new("127.0.0.1", 12201, "LAN", { :protocol => GELF::Protocol::TCP }) 26 | 27 | Note that the `LAN` or `WAN` option is ignored for TCP because no chunking happens. (Read below for more information.) 28 | 29 | ### Gelf::Logger 30 | 31 | The Gelf::Logger is compatible with the standard Ruby Logger interface and can be used interchangeably. 32 | Under the hood it uses Gelf::Notifier to send log messages via UDP to Graylog. 33 | 34 | logger = GELF::Logger.new("localhost", 12201, "WAN", { :facility => "appname" }) 35 | 36 | logger.debug "foobar" 37 | logger.info "foobar" 38 | logger.warn "foobar" 39 | logger.error "foobar" 40 | logger.fatal "foobar" 41 | 42 | logger << "foobar" 43 | 44 | Then `WAN` or `LAN` option influences the UDP chunk size depending on if you send in your own 45 | network (LAN) or on a longer route (i.e. through the internet) and should be set accordingly. 46 | 47 | Since it's compatible with the Logger interface, you can also use it in your Rails application: 48 | 49 | # config/environments/production.rb 50 | config.logger = GELF::Logger.new("localhost", 12201, "WAN", { :facility => "appname" }) 51 | 52 | ### Note on Patches/Pull Requests 53 | 54 | * Fork the project. 55 | * Make your feature addition or bug fix. 56 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 57 | * Commit, do not mess with rakefile, version, or history. 58 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 59 | * Send me a pull request. Bonus points for topic branches. 60 | 61 | ## Copyright 62 | 63 | Copyright (c) 2010-2016 Lennart Koopmann and Alexey Palazhchenko. See LICENSE for details. 64 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | begin 4 | require 'ci/reporter/rake/test_unit' 5 | rescue LoadError 6 | # nothing 7 | end 8 | 9 | begin 10 | require 'jeweler' 11 | 12 | Jeweler::Tasks.new do |gem| 13 | gem.name = "gelf" 14 | gem.summary = 'Library to send GELF messages to Graylog logging server.' 15 | gem.description = 'Library to send GELF messages to Graylog logging server. Supports plain-text, GELF messages and exceptions via UDP and TCP.' 16 | gem.email = "mail@marcusilgner.com" 17 | gem.homepage = "http://github.com/Graylog2/gelf-rb" 18 | gem.authors = ["Alexey Palazhchenko", "Lennart Koopmann", "Zac Sprackett", "Marcus Ilgner"] 19 | gem.add_dependency "json" 20 | gem.licenses = ["MIT"] 21 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 22 | end 23 | rescue LoadError => e 24 | puts e 25 | abort "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 26 | end 27 | 28 | require 'rake/testtask' 29 | Rake::TestTask.new(:test) do |test| 30 | test.libs << 'lib' << 'test' 31 | test.pattern = 'test/**/test_*.rb' 32 | test.verbose = true 33 | end 34 | 35 | task :default => :test 36 | 37 | begin 38 | require 'rcov/rcovtask' 39 | Rcov::RcovTask.new do |test| 40 | test.libs << 'test' 41 | test.pattern = 'test/**/test_*.rb' 42 | test.rcov_opts << '--exclude gem' 43 | test.verbose = true 44 | end 45 | rescue LoadError => e 46 | task :rcov do 47 | puts e 48 | abort "rcov is not available. Run: gem install rcov" 49 | end 50 | end 51 | 52 | #require 'rake/rdoctask' 53 | #Rake::RDocTask.new do |rdoc| 54 | # version = File.exist?('VERSION') ? File.read('VERSION') : "" 55 | # 56 | # rdoc.rdoc_dir = 'rdoc' 57 | # rdoc.title = "gelf #{version}" 58 | # rdoc.rdoc_files.include('README*') 59 | # rdoc.rdoc_files.include('lib/**/*.rb') 60 | #end 61 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 3.1.0 -------------------------------------------------------------------------------- /benchmarks/notifier.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | puts "Loading..." 4 | 5 | require 'benchmark' 6 | require 'rubygems' 7 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 8 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 9 | require 'gelf' 10 | 11 | puts "Generating random data..." 12 | srand(1) 13 | RANDOM_DATA = ('A'..'z').to_a 14 | k3_message = (1..3*1024).map { RANDOM_DATA[rand(RANDOM_DATA.count)] }.join 15 | 16 | TARGET_HOST = 'localhost' 17 | TARGET_PORT = 12201 18 | DEFAULT_OPTIONS = { '_host' => 'localhost' } 19 | TIMES = 5000 20 | 21 | SHORT_HASH = { 'short_message' => 'message' } 22 | LONG_HASH = { 'short_message' => 'message', 'long_message' => k3_message } 23 | 24 | 25 | notifier_lan = GELF::Notifier.new(TARGET_HOST, TARGET_PORT, 'LAN', DEFAULT_OPTIONS) 26 | notifier_wan = GELF::Notifier.new(TARGET_HOST, TARGET_PORT, 'WAN', DEFAULT_OPTIONS) 27 | 28 | # to create mongo collections, etc. 29 | notifier_lan.notify!(LONG_HASH) 30 | sleep(5) 31 | 32 | puts "Sending #{TIMES} notifications...\n" 33 | tms = Benchmark.bm(25) do |b| 34 | b.report('lan, short data, 1 chunk ') { TIMES.times { notifier_lan.notify!(SHORT_HASH) } } 35 | sleep(5) 36 | b.report('lan, long data, 1 chunk ') { TIMES.times { notifier_lan.notify!(LONG_HASH) } } 37 | sleep(5) 38 | b.report('wan, long data, 2 chunks') { TIMES.times { notifier_wan.notify!(LONG_HASH) } } 39 | end 40 | -------------------------------------------------------------------------------- /gelf.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: gelf 3.1.0 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "gelf" 9 | s.version = "3.1.0" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Alexey Palazhchenko", "Lennart Koopmann", "Zac Sprackett", "Marcus Ilgner"] 14 | s.date = "2018-09-05" 15 | s.description = "Library to send GELF messages to Graylog logging server. Supports plain-text, GELF messages and exceptions via UDP and TCP." 16 | s.email = "mail@marcusilgner.com" 17 | s.extra_rdoc_files = [ 18 | "LICENSE", 19 | "README.md" 20 | ] 21 | s.files = [ 22 | ".gemtest", 23 | ".travis.yml", 24 | "CHANGELOG", 25 | "CONTRIBUTING.md", 26 | "Gemfile", 27 | "Gemfile.lock", 28 | "LICENSE", 29 | "README.md", 30 | "Rakefile", 31 | "VERSION", 32 | "benchmarks/notifier.rb", 33 | "gelf.gemspec", 34 | "lib/gelf.rb", 35 | "lib/gelf/logger.rb", 36 | "lib/gelf/notifier.rb", 37 | "lib/gelf/severity.rb", 38 | "lib/gelf/transport/tcp.rb", 39 | "lib/gelf/transport/tcp_tls.rb", 40 | "lib/gelf/transport/udp.rb", 41 | "test/helper.rb", 42 | "test/test_logger.rb", 43 | "test/test_notifier.rb", 44 | "test/test_ruby_sender.rb", 45 | "test/test_severity.rb" 46 | ] 47 | s.homepage = "https://github.com/graylog-labs/gelf-rb" 48 | s.licenses = ["MIT"] 49 | s.rubygems_version = "2.5.1" 50 | s.summary = "Library to send GELF messages to Graylog logging server." 51 | 52 | if s.respond_to? :specification_version then 53 | s.specification_version = 4 54 | 55 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 56 | s.add_development_dependency(%q, ["~> 2.11.3"]) 57 | s.add_development_dependency(%q, ["~> 2.1.1"]) 58 | s.add_development_dependency(%q, ["< 2.0"]) 59 | s.add_development_dependency(%q, ["~> 1.1.0"]) 60 | s.add_development_dependency(%q, ["~> 3.2.0"]) 61 | s.add_runtime_dependency(%q, [">= 0"]) 62 | else 63 | s.add_dependency(%q, ["~> 2.11.3"]) 64 | s.add_dependency(%q, ["~> 2.1.1"]) 65 | s.add_dependency(%q, ["< 2.0"]) 66 | s.add_dependency(%q, ["~> 1.1.0"]) 67 | s.add_dependency(%q, ["~> 3.2.0"]) 68 | s.add_dependency(%q, [">= 0"]) 69 | end 70 | else 71 | s.add_dependency(%q, ["~> 2.11.3"]) 72 | s.add_dependency(%q, ["~> 2.1.1"]) 73 | s.add_dependency(%q, ["< 2.0"]) 74 | s.add_dependency(%q, ["~> 1.1.0"]) 75 | s.add_dependency(%q, ["~> 3.2.0"]) 76 | s.add_dependency(%q, [">= 0"]) 77 | end 78 | end 79 | 80 | -------------------------------------------------------------------------------- /lib/gelf.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'socket' 3 | require 'zlib' 4 | require 'digest/md5' 5 | 6 | module GELF 7 | SPEC_VERSION = '1.0' 8 | module Protocol 9 | UDP = 0 10 | TCP = 1 11 | end 12 | end 13 | 14 | require 'gelf/severity' 15 | require 'gelf/notifier' 16 | require 'gelf/logger' 17 | -------------------------------------------------------------------------------- /lib/gelf/logger.rb: -------------------------------------------------------------------------------- 1 | module GELF 2 | # Methods for compatibility with Ruby Logger. 3 | module LoggerCompatibility 4 | 5 | attr_accessor :formatter 6 | 7 | # Use it like Logger#add... or better not to use at all. 8 | def add(level, message = nil, progname = nil, &block) 9 | progname ||= default_options['facility'] 10 | message ||= block.call unless block.nil? 11 | 12 | if message.nil? 13 | message = progname 14 | progname = default_options['facility'] 15 | end 16 | 17 | message_hash = { 'facility' => progname } 18 | 19 | if message.is_a?(Hash) 20 | message.each do |key, value| 21 | message_hash[key.to_s] = value.to_s 22 | end 23 | else 24 | message_hash['short_message'] = message.to_s 25 | end 26 | 27 | if message.is_a?(Exception) 28 | message_hash.merge!(self.class.extract_hash_from_exception(message)) 29 | end 30 | 31 | if message_hash.key?('short_message') && !message_hash['short_message'].empty? 32 | notify_with_level(level, message_hash) 33 | end 34 | end 35 | 36 | # Redefines methods in +Notifier+. 37 | GELF::Levels.constants.each do |const| 38 | method_name = const.downcase 39 | 40 | define_method(method_name) do |progname=nil, &block| 41 | const_level = GELF.const_get(const) 42 | add(const_level, nil, progname, &block) 43 | end 44 | 45 | define_method("#{method_name}?") do 46 | const_level = GELF.const_get(const) 47 | const_level >= level 48 | end 49 | end 50 | 51 | def <<(message) 52 | notify_with_level(GELF::UNKNOWN, 'short_message' => message) 53 | end 54 | end 55 | 56 | # Graylog2 notifier, compatible with Ruby Logger. 57 | # You can use it with Rails like this: 58 | # config.logger = GELF::Logger.new("localhost", 12201, "WAN", { :facility => "appname" }) 59 | # config.colorize_logging = false 60 | class Logger < Notifier 61 | include LoggerCompatibility 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /lib/gelf/notifier.rb: -------------------------------------------------------------------------------- 1 | require 'gelf/transport/udp' 2 | require 'gelf/transport/tcp' 3 | require 'gelf/transport/tcp_tls' 4 | 5 | # replace JSON and #to_json with Yajl if available 6 | begin 7 | require 'yajl/json_gem' 8 | rescue LoadError 9 | end 10 | 11 | module GELF 12 | # Graylog2 notifier. 13 | class Notifier 14 | # Maximum number of GELF chunks as per GELF spec 15 | MAX_CHUNKS = 128 16 | MAX_CHUNK_SIZE_WAN = 1420 17 | MAX_CHUNK_SIZE_LAN = 8154 18 | 19 | attr_accessor :enabled, :collect_file_and_line, :rescue_network_errors 20 | attr_reader :max_chunk_size, :level, :default_options, :level_mapping 21 | 22 | # +host+ and +port+ are host/ip and port of graylog2-server. 23 | # +max_size+ is passed to max_chunk_size=. 24 | # +default_options+ is used in notify! 25 | def initialize(host = 'localhost', port = 12201, max_size = 'WAN', default_options = {}) 26 | @enabled = true 27 | @collect_file_and_line = true 28 | @random = Random.new 29 | 30 | self.level = GELF::DEBUG 31 | self.max_chunk_size = max_size 32 | self.rescue_network_errors = false 33 | 34 | self.default_options = default_options.dup 35 | self.default_options['version'] = SPEC_VERSION 36 | self.default_options['host'] ||= Socket.gethostname 37 | self.default_options['level'] ||= GELF::UNKNOWN 38 | self.default_options['facility'] ||= 'gelf-rb' 39 | self.default_options['protocol'] ||= GELF::Protocol::UDP 40 | 41 | self.level_mapping = :logger 42 | @sender = create_sender(host, port) 43 | end 44 | 45 | # Get a list of receivers. 46 | # notifier.addresses # => [['localhost', 12201], ['localhost', 12202]] 47 | def addresses 48 | @sender.addresses 49 | end 50 | 51 | # Set a list of receivers. 52 | # notifier.addresses = [['localhost', 12201], ['localhost', 12202]] 53 | def addresses=(addrs) 54 | @sender.addresses = addrs 55 | end 56 | 57 | # +size+ may be a number of bytes, 'WAN' (1420 bytes) or 'LAN' (8154). 58 | # Default (safe) value is 'WAN'. 59 | def max_chunk_size=(size) 60 | case size.to_s.downcase 61 | when 'wan' 62 | @max_chunk_size = MAX_CHUNK_SIZE_WAN 63 | when 'lan' 64 | @max_chunk_size = MAX_CHUNK_SIZE_LAN 65 | else 66 | @max_chunk_size = size.to_int 67 | end 68 | end 69 | 70 | def level=(new_level) 71 | @level = if new_level.is_a?(Integer) 72 | new_level 73 | else 74 | GELF.const_get(new_level.to_s.upcase) 75 | end 76 | end 77 | 78 | def default_options=(options) 79 | @default_options = self.class.stringify_keys(options) 80 | end 81 | 82 | # +mapping+ may be a hash, 'logger' (GELF::LOGGER_MAPPING) or 'direct' (GELF::DIRECT_MAPPING). 83 | # Default (compatible) value is 'logger'. 84 | def level_mapping=(mapping) 85 | case mapping.to_s.downcase 86 | when 'logger' 87 | @level_mapping = GELF::LOGGER_MAPPING 88 | when 'direct' 89 | @level_mapping = GELF::DIRECT_MAPPING 90 | else 91 | @level_mapping = mapping 92 | end 93 | end 94 | 95 | def disable 96 | @enabled = false 97 | end 98 | 99 | def enable 100 | @enabled = true 101 | end 102 | 103 | # Closes sender 104 | def close 105 | @sender.close 106 | end 107 | 108 | # Same as notify!, but rescues all exceptions (including +ArgumentError+) 109 | # and sends them instead. 110 | def notify(*args) 111 | notify_with_level(nil, *args) 112 | end 113 | 114 | # Sends message to Graylog2 server. 115 | # +args+ can be: 116 | # - hash-like object (any object which responds to +to_hash+, including +Hash+ instance): 117 | # notify!(:short_message => 'All your rebase are belong to us', :user => 'AlekSi') 118 | # - exception with optional hash-like object: 119 | # notify!(SecurityError.new('ALARM!'), :trespasser => 'AlekSi') 120 | # - string-like object (anything which responds to +to_s+) with optional hash-like object: 121 | # notify!('Plain olde text message', :scribe => 'AlekSi') 122 | # Resulted fields are merged with +default_options+, the latter will never overwrite the former. 123 | # This method will raise +ArgumentError+ if arguments are wrong. Consider using notify instead. 124 | def notify!(*args) 125 | notify_with_level!(nil, *args) 126 | end 127 | 128 | GELF::Levels.constants.each do |const| 129 | define_method(const.downcase) do |*args| 130 | level = GELF.const_get(const) 131 | notify_with_level(level, *args) 132 | end 133 | end 134 | 135 | private 136 | 137 | def create_sender(host, port) 138 | addresses = [[host, port]] 139 | if default_options['protocol'] == GELF::Protocol::TCP 140 | if default_options.key?('tls') 141 | tls_options = default_options.delete('tls') 142 | GELF::Transport::TCPTLS.new(addresses, tls_options) 143 | else 144 | GELF::Transport::TCP.new(addresses) 145 | end 146 | else 147 | GELF::Transport::UDP.new(addresses) 148 | end 149 | end 150 | 151 | def notify_with_level(message_level, *args) 152 | notify_with_level!(message_level, *args) 153 | rescue SocketError, SystemCallError 154 | raise unless rescue_network_errors 155 | rescue Exception => exception 156 | notify_with_level!(GELF::UNKNOWN, exception) 157 | end 158 | 159 | def notify_with_level!(message_level, *args) 160 | return unless @enabled 161 | hash = extract_hash(*args) 162 | hash['level'] = message_level unless message_level.nil? 163 | if hash['level'] >= level 164 | if default_options['protocol'] == GELF::Protocol::TCP 165 | validate_hash(hash) 166 | @sender.send(hash.to_json + "\0") 167 | else 168 | @sender.send_datagrams(datagrams_from_hash(hash)) 169 | end 170 | end 171 | end 172 | 173 | def extract_hash(object = nil, args = {}) 174 | primary_data = if object.respond_to?(:to_hash) 175 | object.to_hash 176 | elsif object.is_a?(Exception) 177 | args['level'] ||= GELF::ERROR 178 | self.class.extract_hash_from_exception(object) 179 | else 180 | args['level'] ||= GELF::INFO 181 | { 'short_message' => object.to_s } 182 | end 183 | 184 | hash = default_options.merge(self.class.stringify_keys(args.merge(primary_data))) 185 | convert_hoptoad_keys_to_graylog2(hash) 186 | set_file_and_line(hash) if @collect_file_and_line 187 | set_timestamp(hash) 188 | check_presence_of_mandatory_attributes(hash) 189 | hash 190 | end 191 | 192 | def self.extract_hash_from_exception(exception) 193 | bt = exception.backtrace || ["Backtrace is not available."] 194 | { 195 | 'short_message' => "#{exception.class}: #{exception.message}", 196 | 'full_message' => "Backtrace:\n" + bt.join("\n") 197 | } 198 | end 199 | 200 | # Converts Hoptoad-specific keys in +@hash+ to Graylog2-specific. 201 | def convert_hoptoad_keys_to_graylog2(hash) 202 | if hash['short_message'].to_s.empty? 203 | if hash.has_key?('error_class') && hash.has_key?('error_message') 204 | hash['short_message'] = hash.delete('error_class') + ': ' + hash.delete('error_message') 205 | end 206 | end 207 | end 208 | 209 | CALLER_REGEXP = /^(.*):(\d+).*/ 210 | LIB_GELF_PATTERN = File.join('lib', 'gelf') 211 | 212 | def set_file_and_line(hash) 213 | stack = caller 214 | frame = stack.find { |f| !f.include?(LIB_GELF_PATTERN) } 215 | match = CALLER_REGEXP.match(frame) 216 | hash['file'] = match[1] 217 | hash['line'] = match[2].to_i 218 | end 219 | 220 | def set_timestamp(hash) 221 | hash['timestamp'] = Time.now.utc.to_f if hash['timestamp'].nil? 222 | end 223 | 224 | def check_presence_of_mandatory_attributes(hash) 225 | %w(version short_message host).each do |attribute| 226 | if hash[attribute].to_s.empty? 227 | raise ArgumentError.new("#{attribute} is missing. Options version, short_message and host must be set.") 228 | end 229 | end 230 | end 231 | 232 | def datagrams_from_hash(hash) 233 | data = serialize_hash(hash) 234 | datagrams = [] 235 | 236 | # Maximum total size is 8192 byte for UDP datagram. Split to chunks if bigger. (GELF v1.0 supports chunking) 237 | if data.count > @max_chunk_size 238 | id = @random.bytes(8) 239 | msg_id = Digest::MD5.digest("#{Time.now.to_f}-#{id}")[0, 8] 240 | num, count = 0, (data.count.to_f / @max_chunk_size).ceil 241 | if count > MAX_CHUNKS 242 | raise ArgumentError, "Data too big (#{data.count} bytes), would create more than #{MAX_CHUNKS} chunks!" 243 | end 244 | data.each_slice(@max_chunk_size) do |slice| 245 | datagrams << "\x1e\x0f" + msg_id + [num, count, *slice].pack('C*') 246 | num += 1 247 | end 248 | else 249 | datagrams << data.to_a.pack('C*') 250 | end 251 | 252 | datagrams 253 | end 254 | 255 | def validate_hash(hash) 256 | raise ArgumentError.new("Hash is empty.") if hash.nil? || hash.empty? 257 | hash['level'] = @level_mapping[hash['level']] 258 | end 259 | 260 | def serialize_hash(hash) 261 | validate_hash(hash) 262 | 263 | Zlib::Deflate.deflate(hash.to_json).bytes 264 | end 265 | 266 | def self.stringify_keys(data) 267 | return data unless data.is_a? Hash 268 | 269 | data.each_with_object({}) do |(key, value), obj| 270 | key_s = key.to_s 271 | 272 | if (key != key_s) && data.key?(key_s) 273 | raise ArgumentError, "Both #{key.inspect} and #{key_s} are present." 274 | end 275 | 276 | obj[key_s] = value 277 | end 278 | end 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /lib/gelf/severity.rb: -------------------------------------------------------------------------------- 1 | module GELF 2 | # There are two things you should know about log levels/severity: 3 | # - syslog defines levels from 0 (Emergency) to 7 (Debug). 4 | # 0 (Emergency) and 1 (Alert) levels are reserved for OS kernel. 5 | # - Ruby default Logger defines levels from 0 (DEBUG) to 4 (FATAL) and 5 (UNKNOWN). 6 | # Note that order is inverted. 7 | # For compatibility we define our constants as Ruby Logger, and convert values before 8 | # generating GELF message, using defined mapping. 9 | 10 | module Levels 11 | DEBUG = 0 12 | INFO = 1 13 | WARN = 2 14 | ERROR = 3 15 | FATAL = 4 16 | UNKNOWN = 5 17 | # Additional native syslog severities. These will work in direct mapping mode 18 | # only, for compatibility with syslog sources unrelated to Logger. 19 | EMERGENCY = 10 20 | ALERT = 11 21 | CRITICAL = 12 22 | WARNING = 14 23 | NOTICE = 15 24 | INFORMATIONAL = 16 25 | end 26 | 27 | include Levels 28 | 29 | # Maps Ruby Logger levels to syslog levels as SyslogLogger and syslogger gems. This one is default. 30 | LOGGER_MAPPING = {DEBUG => 7, # Debug 31 | INFO => 6, # Informational 32 | WARN => 5, # Notice 33 | ERROR => 4, # Warning 34 | FATAL => 3, # Error 35 | UNKNOWN => 1} # Alert – shouldn't be used 36 | 37 | # Maps Syslog or Ruby Logger levels directly to standard syslog numerical severities. 38 | DIRECT_MAPPING = {DEBUG => 7, # Debug 39 | INFORMATIONAL => 6, # Informational (syslog source) 40 | INFO => 6, # Informational (Logger source) 41 | NOTICE => 5, # Notice 42 | WARNING => 4, # Warning (syslog source) 43 | WARN => 4, # Warning (Logger source) 44 | ERROR => 3, # Error 45 | CRITICAL => 2, # Critical (syslog source) 46 | FATAL => 2, # Critical (Logger source) 47 | ALERT => 1, # Alert (syslog source) 48 | UNKNOWN => 1, # Alert - shouldn't be used (Logger source) 49 | EMERGENCY => 0} # Emergency (syslog source) 50 | end 51 | -------------------------------------------------------------------------------- /lib/gelf/transport/tcp.rb: -------------------------------------------------------------------------------- 1 | module GELF 2 | module Transport 3 | class TCP 4 | attr_reader :addresses 5 | 6 | # `addresses` Array of [host, port] pairs 7 | def initialize(addresses) 8 | @sockets = [] 9 | self.addresses = addresses 10 | end 11 | 12 | def addresses=(addresses) 13 | @addresses = addresses.dup.freeze.tap do |addrs| 14 | @sockets.each(&:close) 15 | @sockets = addrs.map { |peer| connect(*peer) } 16 | end 17 | end 18 | 19 | def send(message) 20 | return if @addresses.empty? 21 | loop do 22 | connected = @sockets.reject(&:closed?) 23 | reconnect_all if connected.empty? 24 | break if write_any(connected, message) 25 | end 26 | end 27 | 28 | private 29 | 30 | def connect(host, port) 31 | socket_class.new(host, port) 32 | end 33 | 34 | def reconnect_all 35 | @sockets = @sockets.each_with_index.map do |old_socket, index| 36 | old_socket.closed? ? connect(*@addresses[index]) : old_socket 37 | end 38 | end 39 | 40 | def socket_class 41 | if defined?(Celluloid::IO::TCPSocket) 42 | Celluloid::IO::TCPSocket 43 | else 44 | ::TCPSocket 45 | end 46 | end 47 | 48 | def write_any(sockets, message) 49 | sockets.shuffle.each do |socket| 50 | return true if write_socket(socket, message) 51 | end 52 | false 53 | end 54 | 55 | def write_socket(socket, message) 56 | unsafe_write_socket(socket, message) 57 | rescue IOError, SystemCallError 58 | socket.close unless socket.closed? 59 | false 60 | end 61 | 62 | def unsafe_write_socket(socket, message) 63 | r,w = IO.select([socket], [socket]) 64 | # Read everything first 65 | while r.any? do 66 | # don't expect any reads, but a readable socket might 67 | # mean the remote end closed, so read it and throw it away. 68 | # we'll get an EOFError if it happens. 69 | socket.sysread(16384) 70 | r = IO.select([socket]) 71 | end 72 | 73 | # Now send the payload 74 | return false unless w.any? 75 | return socket.syswrite(message) > 0 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/gelf/transport/tcp_tls.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module GELF 4 | module Transport 5 | # Provides encryption capabilities for TCP connections 6 | class TCPTLS < TCP 7 | # Supported tls_options: 8 | # 'no_default_ca' [Boolean] prevents OpenSSL from using the systems CA store. 9 | # 'version' [Symbol] any of :TLSv1, :TLSv1_1, :TLSv1_2 (default) 10 | # 'ca' [String] the path to a custom CA store 11 | # 'cert' [String, IO] the client certificate file 12 | # 'key' [String, IO] the key for the client certificate 13 | # 'all_ciphers' [Boolean] allows any ciphers to be used, may be insecure 14 | # 'rescue_ssl_errors' [Boolean] similar to rescue_network_errors in notifier.rb, allows SSL exceptions to be raised 15 | # 'no_verify' [Boolean] disable peer verification 16 | 17 | attr_accessor :rescue_ssl_errors 18 | 19 | def initialize(addresses, tls_options={}) 20 | @tls_options = tls_options 21 | @rescue_ssl_errors = @tls_options['rescue_ssl_errors'] 22 | @rescue_ssl_errors if @rescue_ssl_errors.nil? 23 | super(addresses) 24 | end 25 | 26 | protected 27 | 28 | def write_socket(socket, message) 29 | super(socket, message) 30 | rescue OpenSSL::SSL::SSLError 31 | socket.close unless socket.closed? 32 | raise unless rescue_ssl_errors 33 | false 34 | end 35 | 36 | def connect(host, port) 37 | plain_socket = super(host, port) 38 | start_tls(plain_socket) 39 | rescue OpenSSL::SSL::SSLError 40 | plain_socket.close unless plain_socket.closed? 41 | raise unless rescue_ssl_errors 42 | nil 43 | end 44 | 45 | # Initiates TLS communication on the socket 46 | def start_tls(plain_socket) 47 | ssl_socket_class.new(plain_socket, ssl_context).tap do |ssl_socket| 48 | ssl_socket.sync_close = true 49 | ssl_socket.connect 50 | end 51 | end 52 | 53 | def ssl_socket_class 54 | if defined?(Celluloid::IO::SSLSocket) 55 | Celluloid::IO::SSLSocket 56 | else 57 | OpenSSL::SSL::SSLSocket 58 | end 59 | end 60 | 61 | def ssl_context 62 | @ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |ctx| 63 | ctx.cert_store = ssl_cert_store 64 | ctx.ssl_version = tls_version 65 | ctx.verify_mode = verify_mode 66 | set_certificate_and_key(ctx) 67 | restrict_ciphers(ctx) unless @tls_options['all_ciphers'] 68 | end 69 | end 70 | 71 | def set_certificate_and_key(context) 72 | return unless @tls_options['cert'] && @tls_options['key'] 73 | context.cert = OpenSSL::X509::Certificate.new(resource(@tls_options['cert'])) 74 | context.key = OpenSSL::PKey::RSA.new(resource(@tls_options['key'])) 75 | end 76 | 77 | # checks whether {resource} is a filename and tries to read it 78 | # otherwise treats it as if it already contains certificate/key data 79 | def resource(data) 80 | if data.is_a?(String) && File.exist?(data) 81 | File.read(data) 82 | else 83 | data 84 | end 85 | end 86 | 87 | # Ciphers have to come from the CipherString class, specifically the _TXT_ constants here - https://github.com/jruby/jruby-openssl/blob/master/src/main/java/org/jruby/ext/openssl/CipherStrings.java#L47-L178 88 | def restrict_ciphers(ctx) 89 | # This CipherString is will allow a variety of 'currently' cryptographically secure ciphers, 90 | # while also retaining a broad level of compatibility 91 | ctx.ciphers = "TLSv1_2:TLSv1_1:TLSv1:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:!ADH:!IDEA:!3DES" 92 | end 93 | 94 | def verify_mode 95 | @tls_options['no_verify'] ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER 96 | end 97 | 98 | # SSL v2&3 are insecure, forces at least TLS v1.0 and defaults to v1.2 99 | def tls_version 100 | if @tls_options.key?('version') && 101 | OpenSSL::SSL::SSLContext::METHODS.include?(@tls_options['version']) && 102 | @tls_options['version'] =~ /\ATLSv/ 103 | @tls_options['version'] 104 | else 105 | :TLSv1_2 106 | end 107 | end 108 | 109 | def ssl_cert_store 110 | OpenSSL::X509::Store.new.tap do |store| 111 | unless @tls_options['no_default_ca'] 112 | store.set_default_paths 113 | end 114 | 115 | if @tls_options.key?('ca') 116 | ca = @tls_options['ca'] 117 | if File.directory?(ca) 118 | store.add_path(@tls_options['ca']) 119 | elsif File.file?(ca) 120 | store.add_file(ca) 121 | else 122 | $stderr.puts "No directory or file: #{ca}" 123 | end 124 | end 125 | end 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/gelf/transport/udp.rb: -------------------------------------------------------------------------------- 1 | module GELF 2 | module Transport 3 | class UDP 4 | attr_accessor :addresses 5 | 6 | def initialize(addresses) 7 | @addresses = addresses 8 | end 9 | 10 | def send_datagrams(datagrams) 11 | socket = get_socket 12 | idx = get_address_index 13 | 14 | host, port = @addresses[idx] 15 | set_address_index((idx + 1) % @addresses.length) 16 | datagrams.each do |datagram| 17 | socket.send(datagram, 0, host, port) 18 | end 19 | end 20 | 21 | def close 22 | socket = get_socket 23 | socket.close if socket 24 | end 25 | 26 | private 27 | 28 | def get_socket 29 | Thread.current[:gelf_udp_socket] ||= UDPSocket.open 30 | end 31 | 32 | def get_address_index 33 | Thread.current[:gelf_udp_address_idx] ||= 0 34 | end 35 | 36 | def set_address_index(value) 37 | Thread.current[:gelf_udp_address_idx] = value 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'shoulda' 4 | require 'mocha/setup' 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 8 | require 'gelf' 9 | 10 | class Test::Unit::TestCase 11 | end 12 | -------------------------------------------------------------------------------- /test/test_logger.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestLogger < Test::Unit::TestCase 4 | context "with logger with mocked sender" do 5 | setup do 6 | Socket.stubs(:gethostname).returns('stubbed_hostname') 7 | @logger = GELF::Logger.new 8 | @sender = mock 9 | @logger.instance_variable_set('@sender', @sender) 10 | end 11 | 12 | should "respond to #close" do 13 | assert @logger.respond_to?(:close) 14 | end 15 | 16 | context "#add" do 17 | # logger.add(Logger::INFO, nil) 18 | should 'implement add method with level, message and facility from defaults' do 19 | @logger.expects(:notify_with_level!).with do |level, hash| 20 | level == GELF::INFO && 21 | hash['short_message'] == 'gelf-rb' && 22 | hash['facility'] == 'gelf-rb' 23 | end 24 | @logger.add(GELF::INFO, nil) 25 | end 26 | 27 | # logger.add(Logger::INFO, 'Message') 28 | should "implement add method with level and message from parameters, facility from defaults" do 29 | @logger.expects(:notify_with_level!).with do |level, hash| 30 | level == GELF::INFO && 31 | hash['short_message'] == 'Message' && 32 | hash['facility'] == 'gelf-rb' 33 | end 34 | @logger.add(GELF::INFO, nil, 'Message') 35 | end 36 | 37 | # logger.add(Logger::INFO, RuntimeError.new('Boom!')) 38 | should "implement add method with level and exception from parameters, facility from defaults" do 39 | @logger.expects(:notify_with_level!).with do |level, hash| 40 | level == GELF::INFO && 41 | hash['short_message'] == 'RuntimeError: Boom!' && 42 | hash['full_message'] =~ /^Backtrace/ && 43 | hash['facility'] == 'gelf-rb' 44 | end 45 | @logger.add(GELF::INFO, nil, RuntimeError.new('Boom!')) 46 | end 47 | 48 | # logger.add(Logger::INFO) { 'Message' } 49 | should "implement add method with level from parameter, message from block, facility from defaults" do 50 | @logger.expects(:notify_with_level!).with do |level, hash| 51 | level == GELF::INFO && 52 | hash['short_message'] == 'Message' && 53 | hash['facility'] == 'gelf-rb' 54 | end 55 | @logger.add(GELF::INFO, nil, nil) { 'Message' } 56 | end 57 | 58 | # logger.add(Logger::INFO) { RuntimeError.new('Boom!') } 59 | should "implement add method with level from parameter, exception from block, facility from defaults" do 60 | @logger.expects(:notify_with_level!).with do |level, hash| 61 | level == GELF::INFO && 62 | hash['short_message'] == 'RuntimeError: Boom!' && 63 | hash['full_message'] =~ /^Backtrace/ && 64 | hash['facility'] == 'gelf-rb' 65 | end 66 | @logger.add(GELF::INFO, nil, nil) { RuntimeError.new('Boom!') } 67 | end 68 | 69 | # logger.add(Logger::INFO, 'Message', 'Facility') 70 | should "implement add method with level, message and facility from parameters" do 71 | @logger.expects(:notify_with_level!).with do |level, hash| 72 | level == GELF::INFO && 73 | hash['short_message'] == 'Message' && 74 | hash['facility'] == 'Facility' 75 | end 76 | @logger.add(GELF::INFO, 'Message', 'Facility') 77 | end 78 | 79 | # logger.add(Logger::INFO, 'Message', nil) 80 | should "use facility from initialization if facility is nil" do 81 | logger = GELF::Logger.new('localhost', 12202, 'WAN', :facility => 'foo-bar') 82 | logger.expects(:notify_with_level!).with do |level, hash| 83 | level == GELF::INFO && 84 | hash['short_message'] == 'Message' && 85 | hash['facility'] == 'foo-bar' 86 | end 87 | logger.add(GELF::INFO, 'Message', nil) 88 | end 89 | 90 | # logger.add(Logger::INFO, 'Message', nil) 91 | should "use default facility if facility is nil" do 92 | @logger.expects(:notify_with_level!).with do |level, hash| 93 | level == GELF::INFO && 94 | hash['short_message'] == 'Message' && 95 | hash['facility'] == 'gelf-rb' 96 | end 97 | @logger.add(GELF::INFO, 'Message', nil) 98 | end 99 | 100 | # logger.add(Logger::INFO, RuntimeError.new('Boom!'), 'Facility') 101 | should "implement add method with level, exception and facility from parameters" do 102 | @logger.expects(:notify_with_level!).with do |level, hash| 103 | level == GELF::INFO && 104 | hash['short_message'] == 'RuntimeError: Boom!' && 105 | hash['full_message'] =~ /^Backtrace/ && 106 | hash['facility'] == 'Facility' 107 | end 108 | @logger.add(GELF::INFO, RuntimeError.new('Boom!'), 'Facility') 109 | end 110 | 111 | # logger.add(Logger::INFO, nil, 'Facility') { 'Message' } 112 | should "implement add method with level and facility from parameters, message from block" do 113 | @logger.expects(:notify_with_level!).with do |level, hash| 114 | level == GELF::INFO && 115 | hash['short_message'] == 'Message' && 116 | hash['facility'] == 'Facility' 117 | end 118 | @logger.add(GELF::INFO, nil, 'Facility') { 'Message' } 119 | end 120 | 121 | # logger.add(Logger::INFO, nil, 'Facility') { RuntimeError.new('Boom!') } 122 | should "implement add method with level and facility from parameters, exception from block" do 123 | @logger.expects(:notify_with_level!).with do |level, hash| 124 | level == GELF::INFO && 125 | hash['short_message'] == 'RuntimeError: Boom!' && 126 | hash['full_message'] =~ /^Backtrace/ && 127 | hash['facility'] == 'Facility' 128 | end 129 | @logger.add(GELF::INFO, nil, 'Facility') { RuntimeError.new('Boom!') } 130 | end 131 | 132 | # logger.add(Logger::INFO, { :short_message => "Some message" }) 133 | should "implement add method with level and message from hash, facility from defaults" do 134 | @logger.expects(:notify_with_level!).with do |level, hash| 135 | level == GELF::INFO && 136 | hash['short_message'] == 'Some message' && 137 | hash['facility'] == 'gelf-rb' 138 | end 139 | @logger.add(GELF::INFO, { :short_message => "Some message" }) 140 | end 141 | 142 | # logger.add(Logger::INFO, { :short_message => "Some message", :_foo => "bar", "_zomg" => "wat" }) 143 | should "implement add method with level and message from hash, facility from defaults and some additional fields" do 144 | @logger.expects(:notify_with_level!).with do |level, hash| 145 | level == GELF::INFO && 146 | hash['short_message'] == 'Some message' && 147 | hash['facility'] == 'gelf-rb' && 148 | hash['_foo'] == 'bar' && 149 | hash['_zomg'] == 'wat' 150 | end 151 | @logger.add(GELF::INFO, { :short_message => "Some message", :_foo => "bar", "_zomg" => "wat"}) 152 | end 153 | 154 | # logger.add(Logger::INFO, { :short_message => "Some message", :_foo => "bar", "_zomg" => "wat" }, 'somefac') 155 | should "implement add method with level and message from hash, facility from parameters and some additional fields" do 156 | @logger.expects(:notify_with_level!).with do |level, hash| 157 | level == GELF::INFO && 158 | hash['short_message'] == 'Some message' && 159 | hash['facility'] == 'somefac' && 160 | hash['_foo'] == 'bar' && 161 | hash['_zomg'] == 'wat' 162 | end 163 | @logger.add(GELF::INFO, { :short_message => "Some message", :_foo => "bar", "_zomg" => "wat"}, "somefac") 164 | end 165 | 166 | should 'implement add method with level and ignore zero-length message strings' do 167 | @logger.expects(:notify_with_level!).never 168 | @logger.add(GELF::INFO, '') 169 | end 170 | 171 | should 'implement add method with level and ignore hash without short_message key' do 172 | @logger.expects(:notify_with_level!).never 173 | @logger.add(GELF::INFO, { :message => 'Some message' }) 174 | end 175 | 176 | should 'implement add method with level and ignore hash with zero-length short_message entry' do 177 | @logger.expects(:notify_with_level!).never 178 | @logger.add(GELF::INFO, { :short_message => '' }) 179 | end 180 | 181 | should 'implement add method with level and ignore hash with nil short_message entry' do 182 | @logger.expects(:notify_with_level!).never 183 | @logger.add(GELF::INFO, { :short_message => nil }) 184 | end 185 | end 186 | 187 | GELF::Levels.constants.each do |const| 188 | # logger.error "Argument #{ @foo } mismatch." 189 | should "call add with level #{const} from method name, message from parameter" do 190 | @logger.expects(:notify_with_level!).with do |level, hash| 191 | level == GELF.const_get(const) && 192 | hash['short_message'] == 'message' && 193 | hash['facility'] == 'gelf-rb' 194 | end 195 | @logger.__send__(const.downcase, 'message') 196 | end 197 | 198 | # logger.fatal { "Argument 'foo' not given." } 199 | should "call add with level #{const} from method name, message from block" do 200 | @logger.expects(:notify_with_level!).with do |level, hash| 201 | level == GELF.const_get(const) && 202 | hash['short_message'] == 'message' && 203 | hash['facility'] == 'gelf-rb' 204 | end 205 | @logger.__send__(const.downcase) { 'message' } 206 | end 207 | 208 | # logger.info('initialize') { "Initializing..." } 209 | should "call add with level #{const} from method name, facility from parameter, message from block" do 210 | @logger.expects(:notify_with_level!).with do |level, hash| 211 | level == GELF.const_get(const) && 212 | hash['short_message'] == 'message' && 213 | hash['facility'] == 'facility' 214 | end 215 | @logger.__send__(const.downcase, 'facility') { 'message' } 216 | end 217 | 218 | should "respond to #{const.downcase}?" do 219 | @logger.level = GELF.const_get(const) - 1 220 | assert @logger.__send__(const.to_s.downcase + '?') 221 | @logger.level = GELF.const_get(const) 222 | assert @logger.__send__(const.to_s.downcase + '?') 223 | @logger.level = GELF.const_get(const) + 1 224 | assert !@logger.__send__(const.to_s.downcase + '?') 225 | end 226 | end 227 | 228 | should "support Logger#<<" do 229 | @logger.expects(:notify_with_level!).with do |level, hash| 230 | level == GELF::UNKNOWN && 231 | hash['short_message'] == "Message" 232 | end 233 | @logger << "Message" 234 | end 235 | 236 | should "have formatter attribute" do 237 | @logger.formatter 238 | end 239 | 240 | context "close" do 241 | should "close socket" do 242 | @sender.expects(:close).once 243 | @logger.close 244 | end 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /test/test_notifier.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | RANDOM_DATA = ('A'..'Z').to_a 4 | 5 | class TestNotifier < Test::Unit::TestCase 6 | should "allow access to host, port, max_chunk_size and default_options" do 7 | Socket.expects(:gethostname).returns('default_hostname') 8 | n = GELF::Notifier.new 9 | assert_equal [[['localhost', 12201]], 1420], [n.addresses, n.max_chunk_size] 10 | assert_equal( { 'version' => '1.0', 'level' => GELF::UNKNOWN, 'protocol' => 0, 11 | 'host' => 'default_hostname', 'facility' => 'gelf-rb' }, 12 | n.default_options ) 13 | n.addresses, n.max_chunk_size, n.default_options = [['graylog2.org', 7777]], :lan, {:host => 'grayhost'} 14 | assert_equal [['graylog2.org', 7777]], n.addresses 15 | assert_equal 8154, n.max_chunk_size 16 | assert_equal({'host' => 'grayhost'}, n.default_options) 17 | 18 | n.max_chunk_size = 1337.1 19 | assert_equal 1337, n.max_chunk_size 20 | end 21 | 22 | context "with notifier with mocked sender" do 23 | setup do 24 | Socket.stubs(:gethostname).returns('stubbed_hostname') 25 | @notifier = GELF::Notifier.new('host', 12345) 26 | @sender = mock 27 | @notifier.instance_variable_set('@sender', @sender) 28 | end 29 | 30 | context "extract_hash" do 31 | should "check arguments" do 32 | assert_raise(ArgumentError) { @notifier.__send__(:extract_hash) } 33 | assert_raise(ArgumentError) { @notifier.__send__(:extract_hash, 1, 2, 3) } 34 | end 35 | 36 | should "work with hash" do 37 | hash = @notifier.__send__(:extract_hash, { 'version' => '1.0', 'short_message' => 'message' }) 38 | assert_equal '1.0', hash['version'] 39 | assert_equal 'message', hash['short_message'] 40 | end 41 | 42 | should "work with any object which responds to #to_hash" do 43 | o = Object.new 44 | o.expects(:to_hash).returns({ 'version' => '1.0', 'short_message' => 'message' }) 45 | hash = @notifier.__send__(:extract_hash, o) 46 | assert_equal '1.0', hash['version'] 47 | assert_equal 'message', hash['short_message'] 48 | end 49 | 50 | should "work with exception with backtrace" do 51 | e = RuntimeError.new('message') 52 | e.set_backtrace(caller) 53 | hash = @notifier.__send__(:extract_hash, e) 54 | assert_equal 'RuntimeError: message', hash['short_message'] 55 | assert_match(/Backtrace/, hash['full_message']) 56 | assert_equal GELF::ERROR, hash['level'] 57 | end 58 | 59 | should "work with exception without backtrace" do 60 | e = RuntimeError.new('message') 61 | hash = @notifier.__send__(:extract_hash, e) 62 | assert_match(/Backtrace is not available/, hash['full_message']) 63 | end 64 | 65 | should "work with exception and hash" do 66 | e, h = RuntimeError.new('message'), {'param' => 1, 'level' => GELF::FATAL, 'short_message' => 'will be hidden by exception'} 67 | hash = @notifier.__send__(:extract_hash, e, h) 68 | assert_equal 'RuntimeError: message', hash['short_message'] 69 | assert_equal GELF::FATAL, hash['level'] 70 | assert_equal 1, hash['param'] 71 | end 72 | 73 | should "work with plain text" do 74 | hash = @notifier.__send__(:extract_hash, 'message') 75 | assert_equal 'message', hash['short_message'] 76 | assert_equal GELF::INFO, hash['level'] 77 | end 78 | 79 | should "work with plain text and hash" do 80 | hash = @notifier.__send__(:extract_hash, 'message', 'level' => GELF::WARN) 81 | assert_equal 'message', hash['short_message'] 82 | assert_equal GELF::WARN, hash['level'] 83 | end 84 | 85 | should "covert hash keys to strings" do 86 | hash = @notifier.__send__(:extract_hash, :short_message => :message) 87 | assert hash.has_key?('short_message') 88 | assert !hash.has_key?(:short_message) 89 | end 90 | 91 | should "not overwrite keys on convert" do 92 | assert_raise(ArgumentError) { @notifier.__send__(:extract_hash, :short_message => :message1, 'short_message' => 'message2') } 93 | end 94 | 95 | should "use default_options" do 96 | @notifier.default_options = {:foo => 'bar', 'short_message' => 'will be hidden by explicit argument', 'host' => 'some_host'} 97 | hash = @notifier.__send__(:extract_hash, { 'version' => '1.0', 'short_message' => 'message' }) 98 | assert_equal 'bar', hash['foo'] 99 | assert_equal 'message', hash['short_message'] 100 | end 101 | 102 | should "be compatible with HoptoadNotifier" do 103 | # https://github.com/thoughtbot/hoptoad_notifier/blob/master/README.rdoc, section Going beyond exceptions 104 | hash = @notifier.__send__(:extract_hash, :error_class => 'Class', :error_message => 'Message') 105 | assert_equal 'Class: Message', hash['short_message'] 106 | end 107 | 108 | should "set file and line" do 109 | line = __LINE__ 110 | hash = @notifier.__send__(:extract_hash, { 'version' => '1.0', 'short_message' => 'message' }) 111 | assert_match(/test_notifier.rb/, hash['file']) 112 | assert_equal line + 1, hash['line'] 113 | end 114 | 115 | should "set timestamp to current time if not set" do 116 | hash = @notifier.__send__(:extract_hash, { 'version' => '1.0', 'short_message' => 'message' }) 117 | assert_instance_of Float, hash['timestamp'] 118 | now = Time.now.utc.to_f 119 | assert ((now - 1)..(now + 1)).include?(hash['timestamp']) 120 | end 121 | 122 | should "set timestamp to specified time" do 123 | timestamp = 1319799449.13765 124 | hash = @notifier.__send__(:extract_hash, { 'version' => '1.0', 'short_message' => 'message', 'timestamp' => timestamp }) 125 | assert_equal timestamp, hash['timestamp'] 126 | end 127 | end 128 | 129 | context "serialize_hash" do 130 | setup do 131 | @notifier.level_mapping = :direct 132 | hash = { 'level' => GELF::WARN, 'field' => 'value' } 133 | @data = @notifier.__send__(:serialize_hash, hash) 134 | assert @data.respond_to?(:each) # Enumerable::Enumerator in 1.8, ::Enumerator in 1.9, so... 135 | @deserialized_hash = JSON.parse(Zlib::Inflate.inflate(@data.to_a.pack('C*'))) 136 | assert_instance_of Hash, @deserialized_hash 137 | end 138 | 139 | should "map level using mapping" do 140 | assert_not_equal GELF::WARN, @deserialized_hash['level'] 141 | assert_not_equal GELF::LOGGER_MAPPING[GELF::WARN], @deserialized_hash['level'] 142 | assert_equal GELF::DIRECT_MAPPING[GELF::WARN], @deserialized_hash['level'] 143 | end 144 | end 145 | 146 | context "datagrams_from_hash" do 147 | should "not split short data" do 148 | hash = { 'version' => '1.0', 'short_message' => 'message' } 149 | datagrams = @notifier.__send__(:datagrams_from_hash, hash) 150 | assert_equal 1, datagrams.count 151 | assert_instance_of String, datagrams[0] 152 | 153 | asserted = "\x78\x9c" 154 | if RUBY_VERSION[0,1].to_i >= 2 155 | puts "I'm a Ruby > 2.0.0. Enforcing ASCII-8BIT. (#{RUBY_VERSION}/#{RUBY_VERSION[0,1].to_i})" 156 | # lol well yeah, Rubby. 157 | # http://stackoverflow.com/questions/15843684/binary-string-literals-in-ruby-2-0 158 | asserted = asserted.b 159 | end 160 | 161 | assert_equal asserted, datagrams[0][0..1] # zlib header 162 | end 163 | 164 | should "split long data" do 165 | srand(1) # for stable tests 166 | hash = { 'version' => '1.0', 'short_message' => 'message' } 167 | hash.merge!('something' => (0..3000).map { RANDOM_DATA[rand(RANDOM_DATA.count)] }.join) # or it will be compressed too good 168 | datagrams = @notifier.__send__(:datagrams_from_hash, hash) 169 | assert_equal 2, datagrams.count 170 | datagrams.each_index do |i| 171 | datagram = datagrams[i] 172 | assert_instance_of String, datagram 173 | assert datagram[0..1] == "\x1e\x0f" # chunked GELF magic number 174 | # datagram[2..9] is a message id 175 | assert_equal i, datagram[10].ord 176 | assert_equal datagrams.count, datagram[11].ord 177 | end 178 | end 179 | 180 | should "split long data when subclassed" do 181 | class MyNotifier < GELF::Notifier; end 182 | 183 | @notifier = MyNotifier.new('host', 1234) 184 | @sender = mock 185 | @notifier.instance_variable_set('@sender', @sender) 186 | 187 | srand(1) # for stable tests 188 | hash = { 'version' => '1.0', 'short_message' => 'message' } 189 | hash.merge!('something' => (0..3000).map { RANDOM_DATA[rand(RANDOM_DATA.count)] }.join) # or it will be compressed too good 190 | datagrams = @notifier.__send__(:datagrams_from_hash, hash) 191 | assert_equal 2, datagrams.count 192 | datagrams.each_index do |i| 193 | datagram = datagrams[i] 194 | assert_instance_of String, datagram 195 | assert datagram[0..1] == "\x1e\x0f" # chunked GELF magic number 196 | # datagram[2..9] is a message id 197 | assert_equal i, datagram[10].ord 198 | assert_equal datagrams.count, datagram[11].ord 199 | end 200 | end 201 | 202 | should "throw an error if more than MAX_CHUNKS will be created" do 203 | srand(1) # for stable tests 204 | hash = { 'version' => '1.0', 'short_message' => 'message' } 205 | hash.merge!('something' => (0..3000).map { RANDOM_DATA[rand(RANDOM_DATA.count)] }.join) # or it will be compressed too good 206 | @notifier.max_chunk_size = 10 207 | @notifier.instance_variable_set('@hash', hash) 208 | assert_raise(ArgumentError) do 209 | @notifier.__send__(:datagrams_from_hash) 210 | end 211 | end 212 | end 213 | 214 | context "level threshold" do 215 | setup do 216 | @notifier.level = GELF::WARN 217 | @hash = { 'version' => '1.0', 'short_message' => 'message' } 218 | end 219 | 220 | ['debug', 'DEBUG', :debug].each do |l| 221 | should "allow to set threshold as #{l.inspect}" do 222 | @notifier.level = l 223 | assert_equal GELF::DEBUG, @notifier.level 224 | end 225 | end 226 | 227 | should "not send notifications with level below threshold" do 228 | @sender.expects(:send_datagrams).never 229 | @notifier.notify!(@hash.merge('level' => GELF::DEBUG)) 230 | end 231 | 232 | should "not notifications with level equal or above threshold" do 233 | @sender.expects(:send_datagrams).once 234 | @notifier.notify!(@hash.merge('level' => GELF::WARN)) 235 | end 236 | end 237 | 238 | context "close" do 239 | should "close sender" do 240 | @sender.expects(:close).once 241 | @notifier.close 242 | end 243 | end 244 | 245 | context "when disabled" do 246 | setup do 247 | @notifier.disable 248 | end 249 | 250 | should "not send datagrams" do 251 | @sender.expects(:send_datagrams).never 252 | @notifier.expects(:extract_hash).never 253 | @notifier.notify!({ 'version' => '1.0', 'short_message' => 'message' }) 254 | end 255 | 256 | context "and enabled again" do 257 | setup do 258 | @notifier.enable 259 | end 260 | 261 | should "send datagrams" do 262 | @sender.expects(:send_datagrams) 263 | @notifier.notify!({ 'version' => '1.0', 'short_message' => 'message' }) 264 | end 265 | end 266 | end 267 | 268 | should "pass valid data to sender" do 269 | @sender.expects(:send_datagrams).with do |datagrams| 270 | datagrams.is_a?(Array) && datagrams[0].is_a?(String) 271 | end 272 | @notifier.notify!({ 'version' => '1.0', 'short_message' => 'message' }) 273 | end 274 | 275 | should "not mutate arguments" do 276 | data = { 'version' => '1.0', 'short_message' => 'message', foo: { bar: "BAZ" } } 277 | original_hash = data.hash 278 | 279 | @sender.expects(:send_datagrams) 280 | @notifier.notify!(data) 281 | 282 | assert_equal(data.hash, original_hash) 283 | end 284 | 285 | GELF::Levels.constants.each do |const| 286 | should "call notify with level #{const} from method name" do 287 | @notifier.expects(:notify_with_level).with(GELF.const_get(const), { 'version' => '1.0', 'short_message' => 'message' }) 288 | @notifier.__send__(const.downcase, { 'version' => '1.0', 'short_message' => 'message' }) 289 | end 290 | end 291 | 292 | should "not rescue from invalid invocation of #notify!" do 293 | assert_raise(ArgumentError) { @notifier.notify!(:no_short_message => 'too bad') } 294 | end 295 | 296 | should "rescue from invalid invocation of #notify" do 297 | @notifier.expects(:notify_with_level!).with(nil, instance_of(Hash)).raises(ArgumentError) 298 | @notifier.expects(:notify_with_level!).with(GELF::UNKNOWN, instance_of(ArgumentError)) 299 | assert_nothing_raised { @notifier.notify(:no_short_message => 'too bad') } 300 | end 301 | end 302 | 303 | context "with notifier with real sender" do 304 | setup do 305 | @notifier = GELF::Notifier.new('no_such_host_321') 306 | end 307 | 308 | should "raise exception" do 309 | assert_raise(SocketError) { @notifier.notify('Hello!') } 310 | end 311 | 312 | should "not raise exception if asked" do 313 | @notifier.rescue_network_errors = true 314 | assert_nothing_raised { @notifier.notify('Hello!') } 315 | end 316 | end 317 | end 318 | -------------------------------------------------------------------------------- /test/test_ruby_sender.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestRubyUdpSender < Test::Unit::TestCase 4 | context "with ruby sender" do 5 | setup do 6 | @addresses = [['localhost', 12201], ['localhost', 12202]] 7 | @sender = GELF::Transport::UDP.new(@addresses) 8 | @datagrams1 = %w(d1 d2 d3) 9 | @datagrams2 = %w(e1 e2 e3) 10 | end 11 | 12 | context "send_datagrams" do 13 | setup do 14 | @sender.send_datagrams(@datagrams1) 15 | @sender.send_datagrams(@datagrams2) 16 | end 17 | 18 | before_should "be called 3 times with 1st and 2nd address" do 19 | UDPSocket.any_instance.expects(:send).times(3).with do |datagram, _, host, port| 20 | datagram.start_with?('d') && host == 'localhost' && port == 12201 21 | end 22 | UDPSocket.any_instance.expects(:send).times(3).with do |datagram, _, host, port| 23 | datagram.start_with?('e') && host == 'localhost' && port == 12202 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/test_severity.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestSeverity < Test::Unit::TestCase 4 | should "map Ruby Logger levels to syslog levels as SyslogLogger" do 5 | GELF::LOGGER_MAPPING.each do |ruby_level, syslog_level| 6 | assert_not_equal syslog_level, ruby_level 7 | end 8 | end 9 | end 10 | --------------------------------------------------------------------------------