gelf.fitterpen 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,287 @@
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
+ #---------------------------------------------------------------------------------------
185
+ #NOTE:@fitterpen
186
+ #hash = default_options.merge(self.class.stringify_keys(args.merge(primary_data)))
187
+ option_hash = default_options.dup
188
+ option_hash.delete('facility')
189
+ option_hash.delete('protocol')
190
+ #---------------------------------------------------------------------------------------
191
+ hash = self.class.stringify_keys(args.merge(primary_data)).merge(option_hash)
192
+ convert_hoptoad_keys_to_graylog2(hash)
193
+ set_file_and_line(hash) if @collect_file_and_line
194
+ set_timestamp(hash)
195
+ check_presence_of_mandatory_attributes(hash)
196
+ hash
197
+ end
198
+
199
+ def self.extract_hash_from_exception(exception)
200
+ bt = exception.backtrace || ["Backtrace is not available."]
201
+ {
202
+ 'short_message' => "#{exception.class}: #{exception.message}",
203
+ 'full_message' => "Backtrace:\n" + bt.join("\n")
204
+ }
205
+ end
206
+
207
+ # Converts Hoptoad-specific keys in +@hash+ to Graylog2-specific.
208
+ def convert_hoptoad_keys_to_graylog2(hash)
209
+ if hash['short_message'].to_s.empty?
210
+ if hash.has_key?('error_class') && hash.has_key?('error_message')
211
+ hash['short_message'] = hash.delete('error_class') + ': ' + hash.delete('error_message')
212
+ end
213
+ end
214
+ end
215
+
216
+ CALLER_REGEXP = /^(.*):(\d+).*/
217
+ LIB_GELF_PATTERN = File.join('lib', 'gelf')
218
+
219
+ def set_file_and_line(hash)
220
+ stack = caller
221
+ frame = stack.find { |f| !f.include?(LIB_GELF_PATTERN) }
222
+ match = CALLER_REGEXP.match(frame)
223
+ hash['file'] = match[1]
224
+ hash['line'] = match[2].to_i
225
+ end
226
+
227
+ def set_timestamp(hash)
228
+ hash['timestamp'] = Time.now.utc.to_f if hash['timestamp'].nil?
229
+ end
230
+
231
+ def check_presence_of_mandatory_attributes(hash)
232
+ %w(version short_message host).each do |attribute|
233
+ if hash[attribute].to_s.empty?
234
+ raise ArgumentError.new("#{attribute} is missing. Options version, short_message and host must be set.")
235
+ end
236
+ end
237
+ end
238
+
239
+ def datagrams_from_hash(hash)
240
+ data = serialize_hash(hash)
241
+ datagrams = []
242
+
243
+ # Maximum total size is 8192 byte for UDP datagram. Split to chunks if bigger. (GELF v1.0 supports chunking)
244
+ if data.count > @max_chunk_size
245
+ id = @random.bytes(8)
246
+ msg_id = Digest::MD5.digest("#{Time.now.to_f}-#{id}")[0, 8]
247
+ num, count = 0, (data.count.to_f / @max_chunk_size).ceil
248
+ if count > MAX_CHUNKS
249
+ raise ArgumentError, "Data too big (#{data.count} bytes), would create more than #{MAX_CHUNKS} chunks!"
250
+ end
251
+ data.each_slice(@max_chunk_size) do |slice|
252
+ datagrams << "\x1e\x0f" + msg_id + [num, count, *slice].pack('C*')
253
+ num += 1
254
+ end
255
+ else
256
+ datagrams << data.to_a.pack('C*')
257
+ end
258
+
259
+ datagrams
260
+ end
261
+
262
+ def validate_hash(hash)
263
+ raise ArgumentError.new("Hash is empty.") if hash.nil? || hash.empty?
264
+ hash['level'] = @level_mapping[hash['level']]
265
+ end
266
+
267
+ def serialize_hash(hash)
268
+ validate_hash(hash)
269
+
270
+ Zlib::Deflate.deflate(hash.to_json).bytes
271
+ end
272
+
273
+ def self.stringify_keys(data)
274
+ return data unless data.is_a? Hash
275
+
276
+ data.each_with_object({}) do |(key, value), obj|
277
+ key_s = key.to_s
278
+
279
+ if (key != key_s) && data.key?(key_s)
280
+ raise ArgumentError, "Both #{key.inspect} and #{key_s} are present."
281
+ end
282
+
283
+ obj[key_s] = value
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,79 @@
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
@@ -0,0 +1,129 @@
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
@@ -0,0 +1,41 @@
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
data/lib/gelf.rb ADDED
@@ -0,0 +1,16 @@
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'
data/test/helper.rb ADDED
@@ -0,0 +1,11 @@
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