gelf 1.3.2 → 1.4.0.beta1

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.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.2
1
+ 1.4.0.beta1
@@ -0,0 +1,17 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require 'gelf'
7
+
8
+ TARGET_HOST = 'localhost'
9
+ TARGET_PORT = 5140
10
+ RANGE = 35000...36000
11
+
12
+ n = GELF::Notifier.new(TARGET_HOST, TARGET_PORT, 'WAN')
13
+ RANGE.each do |size|
14
+ n.notify!('a' * size)
15
+ puts size if (size % 100) == 0
16
+ sleep 0.01
17
+ end
@@ -1,13 +1,15 @@
1
1
  #! /usr/bin/env ruby
2
2
 
3
- puts "Loading..."
4
-
5
3
  require 'benchmark'
4
+ require 'thread'
6
5
  require 'rubygems'
7
6
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
7
  $LOAD_PATH.unshift(File.dirname(__FILE__))
9
8
  require 'gelf'
10
9
 
10
+ Thread.abort_on_exception = true
11
+
12
+ puts "Running on #{RUBY_DESCRIPTION}"
11
13
  puts "Generating random data..."
12
14
  srand(1)
13
15
  RANDOM_DATA = ('A'..'z').to_a
@@ -16,11 +18,21 @@ k3_message = (1..3*1024).map { RANDOM_DATA[rand(RANDOM_DATA.count)] }.join
16
18
  TARGET_HOST = 'localhost'
17
19
  TARGET_PORT = 12201
18
20
  DEFAULT_OPTIONS = { '_host' => 'localhost' }
19
- TIMES = 5000
21
+ MESSAGES_COUNT = 5000
22
+ THREADS_COUNT = 50
20
23
 
21
24
  SHORT_HASH = { 'short_message' => 'message' }
22
25
  LONG_HASH = { 'short_message' => 'message', 'long_message' => k3_message }
23
26
 
27
+ def notify(n, hash)
28
+ threads = []
29
+ THREADS_COUNT.times do
30
+ threads << Thread.new do
31
+ (MESSAGES_COUNT / THREADS_COUNT).times { n.notify!(hash) }
32
+ end
33
+ end
34
+ threads.each { |t| t.join }
35
+ end
24
36
 
25
37
  notifier_lan = GELF::Notifier.new(TARGET_HOST, TARGET_PORT, 'LAN', DEFAULT_OPTIONS)
26
38
  notifier_wan = GELF::Notifier.new(TARGET_HOST, TARGET_PORT, 'WAN', DEFAULT_OPTIONS)
@@ -29,11 +41,11 @@ notifier_wan = GELF::Notifier.new(TARGET_HOST, TARGET_PORT, 'WAN', DEFAULT_OPTIO
29
41
  notifier_lan.notify!(LONG_HASH)
30
42
  sleep(5)
31
43
 
32
- puts "Sending #{TIMES} notifications...\n"
44
+ puts "Sending #{MESSAGES_COUNT} notifications...\n"
33
45
  tms = Benchmark.bm(25) do |b|
34
- b.report('lan, short data, 1 chunk ') { TIMES.times { notifier_lan.notify!(SHORT_HASH) } }
46
+ b.report('lan, short data, 1 chunk ') { notify(notifier_lan, SHORT_HASH) }
35
47
  sleep(5)
36
- b.report('lan, long data, 1 chunk ') { TIMES.times { notifier_lan.notify!(LONG_HASH) } }
48
+ b.report('lan, long data, 1 chunk ') { notify(notifier_lan, LONG_HASH) }
37
49
  sleep(5)
38
- b.report('wan, long data, 2 chunks') { TIMES.times { notifier_wan.notify!(LONG_HASH) } }
50
+ b.report('wan, long data, 2 chunks') { notify(notifier_wan, LONG_HASH) }
39
51
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "gelf"
8
- s.version = "1.3.2"
8
+ s.version = "1.4.0.beta1"
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Alexey Palazhchenko", "Lennart Koopmann"]
12
- s.date = "2011-12-02"
12
+ s.date = "2012-01-17"
13
13
  s.description = "Library to send GELF messages to Graylog2 logging server. Supports plain-text, GELF messages and exceptions."
14
14
  s.email = "alexey.palazhchenko@gmail.com"
15
15
  s.extra_rdoc_files = [
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  "README.rdoc",
25
25
  "Rakefile",
26
26
  "VERSION",
27
+ "benchmarks/incremental.rb",
27
28
  "benchmarks/notifier.rb",
28
29
  "gelf.gemspec",
29
30
  "lib/gelf.rb",
@@ -39,7 +40,7 @@ Gem::Specification.new do |s|
39
40
  ]
40
41
  s.homepage = "http://github.com/Graylog2/gelf-rb"
41
42
  s.require_paths = ["lib"]
42
- s.rubygems_version = "1.8.11"
43
+ s.rubygems_version = "1.8.15"
43
44
  s.summary = "Library to send GELF messages to Graylog2 logging server."
44
45
 
45
46
  if s.respond_to? :specification_version then
@@ -2,6 +2,7 @@ require 'json'
2
2
  require 'socket'
3
3
  require 'zlib'
4
4
  require 'digest/md5'
5
+ require 'thread'
5
6
 
6
7
  module GELF
7
8
  SPEC_VERSION = '1.0'
@@ -5,7 +5,7 @@ module GELF
5
5
  def close
6
6
  end
7
7
 
8
- # Use it like Logger#add... or better not to use at all.
8
+ # Use it like Logger#add... or better not use directly at all.
9
9
  def add(level, *args)
10
10
  raise ArgumentError.new('Wrong arguments.') unless (0..2).include?(args.count)
11
11
 
@@ -50,6 +50,5 @@ module GELF
50
50
  # config.colorize_logging = false
51
51
  class Logger < Notifier
52
52
  include LoggerCompatibility
53
- @last_chunk_id = 0
54
53
  end
55
54
  end
@@ -1,11 +1,6 @@
1
1
  module GELF
2
2
  # Graylog2 notifier.
3
3
  class Notifier
4
- @last_chunk_id = 0
5
- class << self
6
- attr_accessor :last_chunk_id
7
- end
8
-
9
4
  attr_accessor :enabled, :collect_file_and_line, :rescue_network_errors
10
5
  attr_reader :max_chunk_size, :level, :default_options, :level_mapping
11
6
 
@@ -137,14 +132,16 @@ module GELF
137
132
 
138
133
  def notify_with_level!(message_level, *args)
139
134
  return unless @enabled
140
- extract_hash(*args)
141
- @hash['level'] = message_level unless message_level.nil?
142
- if @hash['level'] >= level
143
- @sender.send_datagrams(datagrams_from_hash)
135
+ hash = extract_hash(*args)
136
+ hash['level'] = message_level unless message_level.nil?
137
+ if hash['level'] >= level
138
+ @sender.send_datagrams(datagrams_from_hash(hash))
144
139
  end
145
140
  end
146
141
 
147
142
  def extract_hash(object = nil, args = {})
143
+ args = self.class.stringify_keys(args)
144
+
148
145
  primary_data = if object.respond_to?(:to_hash)
149
146
  object.to_hash
150
147
  elsif object.is_a?(Exception)
@@ -155,61 +152,94 @@ module GELF
155
152
  { 'short_message' => object.to_s }
156
153
  end
157
154
 
158
- @hash = default_options.merge(self.class.stringify_keys(args.merge(primary_data)))
159
- convert_hoptoad_keys_to_graylog2
160
- set_file_and_line if @collect_file_and_line
161
- set_timestamp
162
- check_presence_of_mandatory_attributes
163
- @hash
155
+ hash = self.class.stringify_keys(primary_data)
156
+ hash = default_options.merge(args.merge(hash))
157
+ hash = convert_airbrake_keys_to_graylog2(hash)
158
+ hash = set_file_and_line(hash)
159
+ hash = set_timestamp(hash)
160
+ check_presence_of_mandatory_attributes(hash)
161
+ hash
164
162
  end
165
163
 
164
+ CALLER_REGEXP = /^(.*):(\d+).*/
165
+
166
166
  def self.extract_hash_from_exception(exception)
167
- bt = exception.backtrace || ["Backtrace is not available."]
168
- { 'short_message' => "#{exception.class}: #{exception.message}", 'full_message' => "Backtrace:\n" + bt.join("\n") }
167
+ error_class = exception.class.name
168
+ error_message = exception.message
169
+
170
+ # always collect file and line there (ignore @collect_file_and_line)
171
+ # since we already know them, no need to call `caller`
172
+ file, line = nil, nil
173
+ bt = exception.backtrace
174
+ if bt
175
+ match = CALLER_REGEXP.match(bt[0])
176
+ if match
177
+ file = match[1]
178
+ line = match[2].to_i
179
+ end
180
+ else
181
+ bt = ["Backtrace is not available."]
182
+ end
183
+
184
+ { 'short_message' => "#{error_class}: #{error_message}", 'full_message' => "Backtrace:\n" + bt.join("\n"),
185
+ 'error_class' => error_class, 'error_message' => error_message,
186
+ 'file' => file, 'line' => line }
169
187
  end
170
188
 
171
- # Converts Hoptoad-specific keys in +@hash+ to Graylog2-specific.
172
- def convert_hoptoad_keys_to_graylog2
173
- if @hash['short_message'].to_s.empty?
174
- if @hash.has_key?('error_class') && @hash.has_key?('error_message')
175
- @hash['short_message'] = @hash.delete('error_class') + ': ' + @hash.delete('error_message')
189
+ # Converts Airbrake-specific keys in +hash+ to Graylog2-specific.
190
+ def convert_airbrake_keys_to_graylog2(hash)
191
+ if hash['short_message'].to_s.empty?
192
+ if hash.has_key?('error_class') && hash.has_key?('error_message')
193
+ hash['short_message'] = hash['error_class'] + ': ' + hash['error_message']
176
194
  end
177
195
  end
196
+ hash
178
197
  end
179
198
 
180
- CALLER_REGEXP = /^(.*):(\d+).*/
181
199
  LIB_GELF_PATTERN = File.join('lib', 'gelf')
182
200
 
183
- def set_file_and_line
184
- stack = caller
185
- begin
186
- frame = stack.shift
187
- end while frame.include?(LIB_GELF_PATTERN)
188
- match = CALLER_REGEXP.match(frame)
189
- @hash['file'] = match[1]
190
- @hash['line'] = match[2].to_i
201
+ def set_file_and_line(hash)
202
+ return hash unless hash['file'].nil? || hash['line'].nil?
203
+
204
+ if @collect_file_and_line
205
+ stack = caller
206
+ begin
207
+ frame = stack.shift
208
+ end while frame.include?(LIB_GELF_PATTERN)
209
+
210
+ match = CALLER_REGEXP.match(frame)
211
+ if match
212
+ hash['file'] = match[1]
213
+ hash['line'] = match[2].to_i
214
+ else
215
+ hash['file'] = 'unknown'
216
+ hash['line'] = -1
217
+ end
218
+ end
219
+
220
+ hash
191
221
  end
192
222
 
193
- def set_timestamp
194
- @hash['timestamp'] = Time.now.utc.to_f if @hash['timestamp'].nil?
223
+ def set_timestamp(hash)
224
+ hash['timestamp'] ||= Time.now.utc.to_f
225
+ hash
195
226
  end
196
227
 
197
- def check_presence_of_mandatory_attributes
228
+ def check_presence_of_mandatory_attributes(hash)
198
229
  %w(version short_message host).each do |attribute|
199
- if @hash[attribute].to_s.empty?
230
+ if hash[attribute].to_s.empty?
200
231
  raise ArgumentError.new("#{attribute} is missing. Options version, short_message and host must be set.")
201
232
  end
202
233
  end
203
234
  end
204
235
 
205
- def datagrams_from_hash
206
- data = serialize_hash
236
+ def datagrams_from_hash(hash)
237
+ data = serialize_hash(hash)
207
238
  datagrams = []
208
239
 
209
240
  # Maximum total size is 8192 byte for UDP datagram. Split to chunks if bigger. (GELF v1.0 supports chunking)
210
241
  if data.count > @max_chunk_size
211
- id = self.class.last_chunk_id += 1
212
- msg_id = Digest::MD5.digest("#{Time.now.to_f}-#{id}")[0, 8]
242
+ msg_id = Digest::MD5.digest("#{Time.now.to_f}-#{hash.object_id}")[0, 8]
213
243
  num, count = 0, (data.count.to_f / @max_chunk_size).ceil
214
244
  data.each_slice(@max_chunk_size) do |slice|
215
245
  datagrams << "\x1e\x0f" + msg_id + [num, count, *slice].pack('C*')
@@ -222,12 +252,12 @@ module GELF
222
252
  datagrams
223
253
  end
224
254
 
225
- def serialize_hash
226
- raise ArgumentError.new("Hash is empty.") if @hash.nil? || @hash.empty?
255
+ def serialize_hash(hash)
256
+ raise ArgumentError.new("Hash is empty.") if hash.nil? || hash.empty?
227
257
 
228
- @hash['level'] = @level_mapping[@hash['level']]
258
+ hash['level'] = @level_mapping[hash['level']]
229
259
 
230
- Zlib::Deflate.deflate(@hash.to_json).bytes
260
+ Zlib::Deflate.deflate(hash.to_json).bytes
231
261
  end
232
262
 
233
263
  def self.stringify_keys(hash)
@@ -1,17 +1,32 @@
1
1
  module GELF
2
2
  # Plain Ruby UDP sender.
3
3
  class RubyUdpSender
4
- attr_accessor :addresses
5
-
6
- def initialize(addresses)
7
- @addresses = addresses
8
- @i = 0
4
+ def initialize(addrs)
5
+ @mutex = ::Mutex.new
9
6
  @socket = UDPSocket.open
7
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 65507) # 65535 - 20 (ip header) - 8 (udp header)
8
+ self.addresses = addrs
9
+ end
10
+
11
+ def addresses
12
+ @mutex.synchronize do
13
+ @i = 0
14
+ @addresses
15
+ end
16
+ end
17
+
18
+ def addresses=(addrs)
19
+ @mutex.synchronize do
20
+ @i = 0
21
+ @addresses = addrs
22
+ end
10
23
  end
11
24
 
12
25
  def send_datagrams(datagrams)
26
+ # not thread-safe, but we don't care if round-robin algo fails sometimes
13
27
  host, port = @addresses[@i]
14
28
  @i = (@i + 1) % @addresses.length
29
+
15
30
  datagrams.each do |datagram|
16
31
  @socket.send(datagram, 0, host, port)
17
32
  end
@@ -52,20 +52,30 @@ class TestNotifier < Test::Unit::TestCase
52
52
  e.set_backtrace(caller)
53
53
  hash = @notifier.__send__(:extract_hash, e)
54
54
  assert_equal 'RuntimeError: message', hash['short_message']
55
+ assert_equal 'RuntimeError', hash['error_class']
56
+ assert_equal 'message', hash['error_message']
57
+ assert_match /shoulda/, hash['file']
58
+ assert hash['line'] > 300 # 382 in shoulda 2.11.3
55
59
  assert_match /Backtrace/, hash['full_message']
56
60
  assert_equal GELF::ERROR, hash['level']
57
61
  end
58
62
 
59
63
  should "work with exception without backtrace" do
60
64
  e = RuntimeError.new('message')
61
- hash = @notifier.__send__(:extract_hash, e)
65
+ hash, line = @notifier.__send__(:extract_hash, e), __LINE__
66
+ assert_equal __FILE__, hash['file']
67
+ assert_equal line, hash['line']
62
68
  assert_match /Backtrace is not available/, hash['full_message']
63
69
  end
64
70
 
65
71
  should "work with exception and hash" do
66
72
  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)
73
+ hash, line = @notifier.__send__(:extract_hash, e, h), __LINE__
68
74
  assert_equal 'RuntimeError: message', hash['short_message']
75
+ assert_equal 'RuntimeError', hash['error_class']
76
+ assert_equal 'message', hash['error_message']
77
+ assert_equal __FILE__, hash['file']
78
+ assert_equal line, hash['line']
69
79
  assert_equal GELF::FATAL, hash['level']
70
80
  assert_equal 1, hash['param']
71
81
  end
@@ -80,6 +90,10 @@ class TestNotifier < Test::Unit::TestCase
80
90
  hash = @notifier.__send__(:extract_hash, 'message', 'level' => GELF::WARN)
81
91
  assert_equal 'message', hash['short_message']
82
92
  assert_equal GELF::WARN, hash['level']
93
+
94
+ hash = @notifier.__send__(:extract_hash, 'message', :level => GELF::WARN)
95
+ assert_equal 'message', hash['short_message']
96
+ assert_equal GELF::WARN, hash['level']
83
97
  end
84
98
 
85
99
  should "covert hash keys to strings" do
@@ -99,10 +113,12 @@ class TestNotifier < Test::Unit::TestCase
99
113
  assert_equal 'message', hash['short_message']
100
114
  end
101
115
 
102
- should "be compatible with HoptoadNotifier" do
103
- # https://github.com/thoughtbot/hoptoad_notifier/blob/master/README.rdoc, section Going beyond exceptions
116
+ should "be compatible with Airbrake" do
117
+ # https://github.com/airbrake/airbrake/blob/master/README.md, section Going beyond exceptions
104
118
  hash = @notifier.__send__(:extract_hash, :error_class => 'Class', :error_message => 'Message')
105
119
  assert_equal 'Class: Message', hash['short_message']
120
+ assert_equal 'Class', hash['error_class']
121
+ assert_equal 'Message', hash['error_message']
106
122
  end
107
123
 
108
124
  should "set file and line" do
@@ -129,8 +145,7 @@ class TestNotifier < Test::Unit::TestCase
129
145
  context "serialize_hash" do
130
146
  setup do
131
147
  @notifier.level_mapping = :direct
132
- @notifier.instance_variable_set('@hash', { 'level' => GELF::WARN, 'field' => 'value' })
133
- @data = @notifier.__send__(:serialize_hash)
148
+ @data = @notifier.__send__(:serialize_hash, { 'level' => GELF::WARN, 'field' => 'value' })
134
149
  assert @data.respond_to?(:each) # Enumerable::Enumerator in 1.8, ::Enumerator in 1.9, so...
135
150
  @deserialized_hash = JSON.parse(Zlib::Inflate.inflate(@data.to_a.pack('C*')))
136
151
  assert_instance_of Hash, @deserialized_hash
@@ -145,8 +160,7 @@ class TestNotifier < Test::Unit::TestCase
145
160
 
146
161
  context "datagrams_from_hash" do
147
162
  should "not split short data" do
148
- @notifier.instance_variable_set('@hash', { 'version' => '1.0', 'short_message' => 'message' })
149
- datagrams = @notifier.__send__(:datagrams_from_hash)
163
+ datagrams = @notifier.__send__(:datagrams_from_hash, { 'version' => '1.0', 'short_message' => 'message' })
150
164
  assert_equal 1, datagrams.count
151
165
  assert_instance_of String, datagrams[0]
152
166
  assert_equal "\x78\x9c", datagrams[0][0..1] # zlib header
@@ -156,8 +170,7 @@ class TestNotifier < Test::Unit::TestCase
156
170
  srand(1) # for stable tests
157
171
  hash = { 'version' => '1.0', 'short_message' => 'message' }
158
172
  hash.merge!('something' => (0..3000).map { RANDOM_DATA[rand(RANDOM_DATA.count)] }.join) # or it will be compressed too good
159
- @notifier.instance_variable_set('@hash', hash)
160
- datagrams = @notifier.__send__(:datagrams_from_hash)
173
+ datagrams = @notifier.__send__(:datagrams_from_hash, hash)
161
174
  assert_equal 2, datagrams.count
162
175
  datagrams.each_index do |i|
163
176
  datagram = datagrams[i]
metadata CHANGED
@@ -1,13 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gelf
3
3
  version: !ruby/object:Gem::Version
4
- hash: 31
5
- prerelease:
4
+ hash: -3453770742
5
+ prerelease: 6
6
6
  segments:
7
7
  - 1
8
- - 3
9
- - 2
10
- version: 1.3.2
8
+ - 4
9
+ - 0
10
+ - beta
11
+ - 1
12
+ version: 1.4.0.beta1
11
13
  platform: ruby
12
14
  authors:
13
15
  - Alexey Palazhchenko
@@ -16,7 +18,7 @@ autorequire:
16
18
  bindir: bin
17
19
  cert_chain: []
18
20
 
19
- date: 2011-12-02 00:00:00 Z
21
+ date: 2012-01-17 00:00:00 Z
20
22
  dependencies:
21
23
  - !ruby/object:Gem::Dependency
22
24
  name: json
@@ -77,6 +79,7 @@ files:
77
79
  - README.rdoc
78
80
  - Rakefile
79
81
  - VERSION
82
+ - benchmarks/incremental.rb
80
83
  - benchmarks/notifier.rb
81
84
  - gelf.gemspec
82
85
  - lib/gelf.rb
@@ -109,16 +112,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
109
112
  required_rubygems_version: !ruby/object:Gem::Requirement
110
113
  none: false
111
114
  requirements:
112
- - - ">="
115
+ - - ">"
113
116
  - !ruby/object:Gem::Version
114
- hash: 3
117
+ hash: 25
115
118
  segments:
116
- - 0
117
- version: "0"
119
+ - 1
120
+ - 3
121
+ - 1
122
+ version: 1.3.1
118
123
  requirements: []
119
124
 
120
125
  rubyforge_project:
121
- rubygems_version: 1.8.11
126
+ rubygems_version: 1.8.15
122
127
  signing_key:
123
128
  specification_version: 3
124
129
  summary: Library to send GELF messages to Graylog2 logging server.