gelf 1.3.2 → 1.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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.