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.
- 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
|