analogger 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/INSTALL +60 -0
- data/README +19 -0
- data/analogger.gemspec +41 -0
- data/bin/analogger +94 -0
- data/external/package.rb +672 -0
- data/external/test_support.rb +58 -0
- data/setup.rb +26 -0
- data/src/swiftcore/Analogger.rb +293 -0
- data/src/swiftcore/Analogger/Client.rb +103 -0
- data/test/TC_Analogger.rb +151 -0
- data/test/analogger.cnf +22 -0
- data/test/analogger2.cnf +9 -0
- data/test/tc_template.rb +15 -0
- metadata +78 -0
@@ -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
|
data/setup.rb
ADDED
@@ -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
|