analogger 0.5.0

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.
@@ -0,0 +1,58 @@
1
+ # A support module for the test suite. This provides a win32 aware
2
+ # mechanism for doing fork/exec operations. It requires win32/process
3
+ # to be installed, however.
4
+ #
5
+ module SwiftcoreTestSupport
6
+ @run_modes = []
7
+
8
+ def self.create_process(args)
9
+ @fork_ok = true unless @fork_ok == false
10
+ pid = nil
11
+ begin
12
+ raise NotImplementedError unless @fork_ok
13
+ unless pid = fork
14
+ Dir.chdir args[:dir]
15
+ exec(*args[:cmd])
16
+ end
17
+ rescue NotImplementedError
18
+ @fork_ok = false
19
+ begin
20
+ require 'rubygems'
21
+ rescue LoadError
22
+ end
23
+
24
+ begin
25
+ require 'win32/process'
26
+ rescue LoadError
27
+ raise "Please install win32-process to run all tests on a Win32 platform. 'gem install win32-process' or http://rubyforge.org/projects/win32utils"
28
+ end
29
+ cwd = Dir.pwd
30
+ Dir.chdir args[:dir]
31
+ pid = Process.create(:app_name => args[:cmd].join(' '))
32
+ Dir.chdir cwd
33
+ end
34
+ pid
35
+ end
36
+
37
+ def self.test_dir(dir)
38
+ File.dirname(File.expand_path(dir))
39
+ end
40
+
41
+ def self.cd_to_test_dir(dir)
42
+ Dir.chdir(File.dirname(File.expand_path(dir)))
43
+ end
44
+
45
+ def self.set_src_dir
46
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__),'../src'))
47
+ end
48
+
49
+ @announcements = {}
50
+ def self.announce(section,msg)
51
+ unless @announcements.has_key?(section)
52
+ puts "\n\n"
53
+ puts msg,"#{'=' * msg.length}\n\n"
54
+ @announcements[section] = true
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,26 @@
1
+ #!ruby
2
+
3
+ basedir = File.dirname(__FILE__)
4
+ $:.push(basedir)
5
+
6
+ require 'external/package'
7
+ require 'rbconfig'
8
+ begin
9
+ require 'rubygems'
10
+ rescue LoadError
11
+ end
12
+
13
+ Dir.chdir(basedir)
14
+ Package.setup("1.0") {
15
+ name "Swiftcore Analogger"
16
+
17
+ translate(:lib, 'src/' => '')
18
+ translate(:bin, 'bin/' => '')
19
+ lib(*Dir["src/swiftcore/**/*.rb"])
20
+ ri(*Dir["src/swiftcore/**/*.rb"])
21
+ bin "bin/analogger"
22
+
23
+ unit_test "test/TC_Analogger.rb"
24
+
25
+ true
26
+ }
@@ -0,0 +1,293 @@
1
+ require 'socket'
2
+ begin
3
+ load_attempted ||= false
4
+ require 'eventmachine'
5
+ rescue LoadError => e
6
+ unless load_attempted
7
+ load_attempted = true
8
+ require 'rubygems'
9
+ retry
10
+ end
11
+ raise e
12
+ end
13
+
14
+ module Swiftcore
15
+ class Analogger
16
+ C_colon = ':'.freeze
17
+ C_bar = '|'.freeze
18
+ Ccull = 'cull'.freeze
19
+ Cdaemonize = 'daemonize'.freeze
20
+ Cdefault = 'default'.freeze
21
+ Cdefault_log = 'default_log'.freeze
22
+ Chost = 'host'.freeze
23
+ Cinterval = 'interval'.freeze
24
+ Ckey = 'key'.freeze
25
+ Clogfile = 'logfile'.freeze
26
+ Clogs = 'logs'.freeze
27
+ Cport = 'port'.freeze
28
+ Csecret = 'secret'.freeze
29
+ Cservice = 'service'.freeze
30
+ Clevels = 'levels'.freeze
31
+ Csyncinterval = 'syncinterval'.freeze
32
+ Cpidfile = 'pidfile'.freeze
33
+ DefaultSeverityLevels = ['debug','info','warn','error','fatal'].inject({}){|h,k|h[k]=true;h}
34
+
35
+ class NoPortProvided < Exception; def to_s; "The port to bind to was not provided."; end; end
36
+ class BadPort < Exception
37
+ def initialize(port)
38
+ @port = port
39
+ end
40
+
41
+ def to_s; "The port provided (#{@port}) is invalid."; end
42
+ end
43
+
44
+ class << self
45
+ def start(config,protocol = AnaloggerProtocol)
46
+ @config = config
47
+ daemonize if @config[Cdaemonize]
48
+ File.open(@config[Cpidfile],'w+') {|fh| fh.puts $$} if @config[Cpidfile]
49
+ @logs = Hash.new {|h,k| h[k] = new_log(k)}
50
+ @queue = Hash.new {|h,k| h[k] = []}
51
+ postprocess_config_load
52
+ check_config_settings
53
+ populate_logs
54
+ set_config_defaults
55
+ @rcount = 0
56
+ @wcount = 0
57
+ trap("INT") {cleanup;exit}
58
+ trap("TERM") {cleanup;exit}
59
+ trap("HUP") {cleanup;throw :hup}
60
+ EventMachine.run {
61
+ EventMachine.start_server @config[Chost], @config[Cport], protocol
62
+ EventMachine.add_periodic_timer(1) {Analogger.update_now}
63
+ EventMachine.add_periodic_timer(@config[Cinterval]) {write_queue}
64
+ EventMachine.add_periodic_timer(@config[Csyncinterval]) {flush_queue}
65
+ }
66
+ end
67
+
68
+ def daemonize
69
+ if (child_pid = fork)
70
+ puts "PID #{child_pid}" unless @config[Cpidfile]
71
+ exit!
72
+ end
73
+ Process.setsid
74
+
75
+ rescue Exception
76
+ puts "Platform (#{RUBY_PLATFORM}) does not appear to support fork/setsid; skipping"
77
+ end
78
+
79
+ def new_log(facility = Cdefault, levels = @config[Clevels] || DefaultSeverityLevels, log = @config[Cdefault_log], cull = true)
80
+ Log.new({Cservice => facility, Clevels => levels, Clogfile => log, Ccull => cull})
81
+ end
82
+
83
+ def cleanup
84
+ @logs.each do |service,l|
85
+ l.logfile.fsync if !l.logfile.closed? and l.logfile.fileno > 2
86
+ l.logfile.close unless l.logfile.closed? or l.logfile.fileno < 3
87
+ end
88
+ end
89
+
90
+ def update_now
91
+ @now = Time.now.strftime('%Y/%m/%d %H:%M:%S')
92
+ end
93
+
94
+ def config
95
+ @config
96
+ end
97
+
98
+ def config=(conf)
99
+ @config = conf
100
+ end
101
+
102
+ def populate_logs
103
+ @config[Clogs].each do |log|
104
+ next unless log[Cservice]
105
+ if Array === log[Cservice]
106
+ log[Cservice].each do |loglog|
107
+ @logs[loglog] = new_log(loglog,log[Clevels],logfile_destination(log[Clogfile]),log[Ccull])
108
+ end
109
+ else
110
+ @logs[log[Cservice]] = new_log(log[Cservice],log[Clevels],logfile_destination(log[Clogfile]),log[Ccull])
111
+ end
112
+ end
113
+ end
114
+
115
+ def postprocess_config_load
116
+ @config[Clogs] ||= []
117
+ if @config[Clevels]
118
+ @config[Clevels] = normalize_levels(@config[Clevels])
119
+ end
120
+
121
+ @config[Clogs].each do |log|
122
+ log[Clevels] = normalize_levels(log[Clevels])
123
+ end
124
+ end
125
+
126
+ def normalize_levels(levels)
127
+ if String === levels and levels =~ /,/
128
+ levels.split(/,/).inject({}) {|h,k| h[k.to_s] = true; h}
129
+ elsif Array === levels
130
+ levels.inject({}) {|h,k| h[k.to_s] = true; h}
131
+ elsif levels.nil?
132
+ DefaultSeverityLevels
133
+ elsif !(Hash === levels)
134
+ [levels.to_s => true]
135
+ else
136
+ levels
137
+ end
138
+ end
139
+
140
+ def check_config_settings
141
+ raise NoPortProvided unless @config[Cport]
142
+ raise BadPort.new(@config[Cport]) unless @config[Cport].to_i > 0
143
+ end
144
+
145
+ def set_config_defaults
146
+ @config[Chost] ||= '127.0.0.1'
147
+ @config[Cinterval] ||= 1
148
+ @config[Csyncinterval] ||= 60
149
+ @config[Csyncinterval] = nil if @config[Csyncinterval] == 0
150
+ @config[Cdefault_log] = @config[Cdefault_log].nil? || @config[Cdefault_log] == '-' ? 'STDOUT' : @config[Cdefault_log]
151
+ @config[Cdefault_log] = logfile_destination(@config[Cdefault_log])
152
+ @logs['default'] = new_log
153
+ end
154
+
155
+ def logfile_destination(logfile)
156
+ if logfile =~ /^STDOUT$/i
157
+ $stdout
158
+ elsif logfile =~ /^STDERR$/i
159
+ $stderr
160
+ else
161
+ File.open(logfile,'ab+')
162
+ end
163
+ end
164
+
165
+ def add_log(log)
166
+ @queue[log.first] << log
167
+ @rcount += 1
168
+ end
169
+
170
+ def write_queue
171
+ @queue.each do |service,q|
172
+ last_sv = nil
173
+ last_m = nil
174
+ last_count = 0
175
+ next unless log = @logs[service]
176
+ lf = log.logfile
177
+ cull = log.cull
178
+ levels = log.levels
179
+ q.each do |m|
180
+ next unless levels.has_key?(m[1])
181
+ if cull
182
+ if m.last == last_m and m[0..1] == last_sv
183
+ last_count += 1
184
+ next
185
+ elsif last_count > 0
186
+ lf.puts "#{@now}|#{last_sv.join(C_bar)}|Last message repeated #{last_count} times"
187
+ last_sv = last_m = nil
188
+ last_count = 0
189
+ end
190
+ lf.puts "#{@now}|#{m.join(C_bar)}"
191
+ last_m = m.last
192
+ last_sv = m[0..1]
193
+ else
194
+ lf.puts "#{@now}|#{m.join(C_bar)}"
195
+ end
196
+ @wcount += 1
197
+ end
198
+ lf.puts "#{@now}|#{last_sv.join(C_bar)}|Last message repeated #{last_count} times" if cull and last_count > 0
199
+ end
200
+ @queue.each {|service,q| q.clear}
201
+ end
202
+
203
+ def flush_queue
204
+ @logs.each_value {|l| l.logfile.fsync if l.logfile.fileno > 2}
205
+ end
206
+
207
+ def key
208
+ @config[Ckey].to_s
209
+ end
210
+
211
+ end
212
+
213
+ end
214
+
215
+ class Log
216
+ attr_reader :service, :levels, :logfile, :cull
217
+
218
+ def initialize(spec)
219
+ @service = spec[Analogger::Cservice]
220
+ @levels = spec[Analogger::Clevels]
221
+ @logfile = spec[Analogger::Clogfile]
222
+ @cull = spec[Analogger::Ccull]
223
+ end
224
+
225
+ def to_s
226
+ "service: #{@service}\nlevels: #{@levels.inspect}\nlogfile: #{@logfile}\ncull: #{@cull}\n"
227
+ end
228
+ end
229
+
230
+ class AnaloggerProtocol < EventMachine::Connection
231
+ Ci = 'i'.freeze
232
+ Rcolon = /:/
233
+ MaxMessageLength = 8192
234
+
235
+ LoggerClass = Analogger
236
+
237
+ def post_init
238
+ setup
239
+ end
240
+
241
+ def setup
242
+ @length = nil
243
+ @logchunk = ''
244
+ @authenticated = nil
245
+ end
246
+
247
+ def receive_data data
248
+ @logchunk << data
249
+ decompose = true
250
+ while decompose
251
+ unless @length
252
+ if @logchunk.length > 7
253
+ l = @logchunk[0..3].unpack(Ci).first
254
+ ck = @logchunk[4..7].unpack(Ci).first
255
+ if l == ck and l < MaxMessageLength
256
+ @length = l + 7
257
+ else
258
+ decompose = false
259
+ peer = get_peername
260
+ peer = peer ? ::Socket.unpack_sockaddr_in(peer)[1] : 'UNK'
261
+ if l == ck
262
+ LoggerClass.add_log([:default,:error,"Max Length Exceeded from #{peer} -- #{l}/#{MaxMessageLength}"])
263
+ close_connection
264
+ else
265
+ LoggerClass.add_log([:default,:error,"checksum failed from #{peer} -- #{l}/#{ck}"])
266
+ close_connection
267
+ end
268
+ end
269
+ end
270
+ end
271
+
272
+ if @length and @logchunk.length > @length
273
+ msg = @logchunk.slice!(0..@length).split(Rcolon,4)
274
+ unless @authenticated
275
+ if msg.last == LoggerClass.key
276
+ @authenticated = true
277
+ else
278
+ close_connection
279
+ end
280
+ else
281
+ msg[0] = nil
282
+ msg.shift
283
+ LoggerClass.add_log(msg)
284
+ end
285
+ @length = nil
286
+ else
287
+ decompose = false
288
+ end
289
+ end
290
+ end
291
+
292
+ end
293
+ end
@@ -0,0 +1,103 @@
1
+ require 'socket'
2
+ include Socket::Constants
3
+
4
+ module Swiftcore
5
+ module Analogger
6
+
7
+ # Swift::Analogger::Client is the client library for writing logging
8
+ # messages to the Swift Analogger asynchronous logging server.
9
+ #
10
+ # To use the Analogger client, instantiate an instance of the Client
11
+ # class.
12
+ #
13
+ # logger = Swift::Analogger::Client.new(:myapplog,'127.0.0.1',12345)
14
+ #
15
+ # Four arguments are accepted when a new Client is created. The first
16
+ # is the name of the logging facility that this Client will write to.
17
+ # The second is the hostname where the Analogger process is running,
18
+ # and the third is the port number that it is listening on for
19
+ # connections.
20
+ #
21
+ # The fourth argument is optional. Analogger can require an
22
+ # authentication key before it will allow logging clients to use its
23
+ # facilities. If the Analogger that one is connecting to requires
24
+ # an authentication key, it must be passed to the new() call as the
25
+ # fourth argument. If the key is incorrect, the connection will be
26
+ # closed.
27
+ #
28
+ # If a Client connects to the Analogger using a facility that is
29
+ # undefined in the Analogger, the log messages will still be accepted,
30
+ # but they will be dumped to the default logging destination.
31
+ #
32
+ # Once connected, the Client is ready to deliver messages to the
33
+ # Analogger. To send a messagine, the log() method is used:
34
+ #
35
+ # logger.log(:debug,"The logging client is now connected.")
36
+ #
37
+ # The log() method takes two arguments. The first is the severity of
38
+ # the message, and the second is the message itself. The default
39
+ # Analogger severity levels are the same as in the standard Ruby
40
+ #
41
+ class Client
42
+ Cauthentication = 'authentication'.freeze
43
+ Ci = 'i'.freeze
44
+
45
+ def initialize(service = 'default', host = '127.0.0.1' , port = 6766, key = nil)
46
+ @service = service.to_s
47
+ @key = key
48
+ @host = host
49
+ @port = port
50
+ connect(host,port)
51
+ end
52
+
53
+ def connect(host,port)
54
+ tries ||= 0
55
+ @socket = Socket.new(AF_INET,SOCK_STREAM,0)
56
+ sockaddr = Socket.pack_sockaddr_in(port,host)
57
+ @socket.connect(sockaddr)
58
+ log(Cauthentication,"#{@key}")
59
+ rescue Exception => e
60
+ if tries < 3
61
+ tries += 1
62
+ @socket.close unless @socket.closed?
63
+ @socket = nil
64
+ select(nil,nil,nil,tries * 0.2) if tries > 0
65
+ retry
66
+ else
67
+ raise e
68
+ end
69
+ end
70
+
71
+ def reconnect
72
+ connect(@host,@port)
73
+ end
74
+
75
+ def log(severity,msg)
76
+ tries ||= 0
77
+ fullmsg = ":#{@service}:#{severity}:#{msg}"
78
+ len = [fullmsg.length].pack(Ci)
79
+ @socket.write "#{len}#{len}#{fullmsg}"
80
+ rescue Exception => e
81
+ if tries < 3
82
+ tries += 1
83
+ @socket.close unless @socket.closed?
84
+ @socket = nil
85
+ select(nil,nil,nil,tries) if tries > 0
86
+ reconnect
87
+ retry
88
+ else
89
+ raise e
90
+ end
91
+ end
92
+
93
+ def close
94
+ @socket.close
95
+ end
96
+
97
+ def closed?
98
+ @socket.closed?
99
+ end
100
+
101
+ end
102
+ end
103
+ end