typhon 0.1.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/bin/typhon +63 -0
- data/lib/typhon.rb +82 -0
- data/lib/typhon/config.rb +48 -0
- data/lib/typhon/heads.rb +160 -0
- data/lib/typhon/log.rb +55 -0
- data/lib/typhon/stompclient.rb +49 -0
- metadata +72 -0
data/bin/typhon
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'typhon'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
configdir = "/etc/typhon"
|
7
|
+
daemon = false
|
8
|
+
pidfile = nil
|
9
|
+
|
10
|
+
opt = OptionParser.new
|
11
|
+
|
12
|
+
opt.on("--config [DIR]", "Directory for configuration file and heads") do |v|
|
13
|
+
configdir = v
|
14
|
+
end
|
15
|
+
|
16
|
+
opt.on("--daemonize", "-d", "Daemonize the process") do |v|
|
17
|
+
daemon = true
|
18
|
+
end
|
19
|
+
|
20
|
+
opt.on("--pid [PIDFILE]", "-p", "Write a pidfile") do |v|
|
21
|
+
pidfile = v
|
22
|
+
end
|
23
|
+
|
24
|
+
opt.parse!
|
25
|
+
|
26
|
+
raise "The directory #{configdir} does not exist" unless File.directory?(configdir)
|
27
|
+
|
28
|
+
def stop_and_exit(daemon, pidfile, signal)
|
29
|
+
Typhon::Log.info("Exiting after signal #{signal}")
|
30
|
+
|
31
|
+
if daemon && pidfile
|
32
|
+
File.unlink(pidfile)
|
33
|
+
end
|
34
|
+
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
Signal.trap('INT') { stop_and_exit(daemon, pidfile, :int) }
|
39
|
+
Signal.trap('TERM') { stop_and_exit(daemon, pidfile, :term) }
|
40
|
+
|
41
|
+
typhon = Typhon.new(configdir)
|
42
|
+
|
43
|
+
Typhon.files.each do |f|
|
44
|
+
Typhon::Log.info("Tailing log #{f}")
|
45
|
+
end
|
46
|
+
|
47
|
+
if daemon
|
48
|
+
raise "Pidfile #{pidfile} exist" if pidfile && File.exist?(pidfile)
|
49
|
+
|
50
|
+
Typhon.daemonize do
|
51
|
+
if pidfile
|
52
|
+
begin
|
53
|
+
File.open(pidfile, 'w') {|f| f.write(Process.pid) }
|
54
|
+
rescue
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Typhon::Log.info("Running in the background as pid #{Process.pid}")
|
59
|
+
typhon.tail
|
60
|
+
end
|
61
|
+
else
|
62
|
+
typhon.tail
|
63
|
+
end
|
data/lib/typhon.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
class Typhon
|
2
|
+
require 'rubygems'
|
3
|
+
require 'yaml'
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'eventmachine-tail'
|
6
|
+
require 'typhon/heads'
|
7
|
+
require 'typhon/log'
|
8
|
+
require 'typhon/config'
|
9
|
+
require 'typhon/stompclient'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def heads
|
13
|
+
Heads.heads
|
14
|
+
end
|
15
|
+
|
16
|
+
def files
|
17
|
+
Heads.heads.keys
|
18
|
+
end
|
19
|
+
|
20
|
+
def grow(options, &blk)
|
21
|
+
raise "Heads need a name" unless options[:name]
|
22
|
+
raise "Heads need files" unless options[:files]
|
23
|
+
|
24
|
+
Heads.register_head(options[:name], options[:files], blk)
|
25
|
+
end
|
26
|
+
|
27
|
+
def stomp=(stomp)
|
28
|
+
@stomp = stomp
|
29
|
+
end
|
30
|
+
|
31
|
+
def stomp
|
32
|
+
@stomp
|
33
|
+
end
|
34
|
+
|
35
|
+
def daemonize
|
36
|
+
fork do
|
37
|
+
Process.setsid
|
38
|
+
exit if fork
|
39
|
+
Dir.chdir('/tmp')
|
40
|
+
STDIN.reopen('/dev/null')
|
41
|
+
STDOUT.reopen('/dev/null', 'a')
|
42
|
+
STDERR.reopen('/dev/null', 'a')
|
43
|
+
|
44
|
+
yield
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :heads
|
50
|
+
|
51
|
+
def initialize(path="/etc/typhon")
|
52
|
+
@configdir = path
|
53
|
+
|
54
|
+
Config[:configdir] = path
|
55
|
+
Config.loadconfig
|
56
|
+
|
57
|
+
@heads = Heads.new
|
58
|
+
@stomp = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def tail
|
62
|
+
EM.run do
|
63
|
+
@heads.loadheads
|
64
|
+
|
65
|
+
if Config[:stomp]
|
66
|
+
Log.debug("Connecting to Stomp Server %s:%d" % [ Config[:stomp][:server], Config[:stomp][:port] ])
|
67
|
+
@stomp = EM.connect Config[:stomp][:server], Config[:stomp][:port], Typhon::StompClient, {:auto_reconnect => true, :timeout => 2}
|
68
|
+
Typhon.stomp = @stomp
|
69
|
+
end
|
70
|
+
|
71
|
+
EM.add_periodic_timer(10) do
|
72
|
+
@heads.loadheads
|
73
|
+
end
|
74
|
+
|
75
|
+
if Config[:stat_log_frequency] > 0
|
76
|
+
EM.add_periodic_timer(Config[:stat_log_frequency]) do
|
77
|
+
@heads.log_stats
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Typhon
|
2
|
+
class Config
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
@settings = {:loglevel => :info, :stomp => false, :stat_log_frequency => 3600}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :settings
|
9
|
+
|
10
|
+
def []=(key,val)
|
11
|
+
@settings[key] = val
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
@settings[key]
|
16
|
+
end
|
17
|
+
|
18
|
+
def include?(key)
|
19
|
+
@settings.include?(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def each
|
23
|
+
@settings.each_pair do |k, v|
|
24
|
+
yield({k => v})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def loadconfig
|
29
|
+
raise "Set configdir" unless @settings.include?(:configdir)
|
30
|
+
|
31
|
+
file = File.join([@settings[:configdir], "typhon.yaml"])
|
32
|
+
|
33
|
+
raise "Cannot find file #{file}" unless File.exist?(file)
|
34
|
+
@settings.merge!(YAML.load_file(file))
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(k, *args, &block)
|
38
|
+
return @settings[k] if @settings.include?(k)
|
39
|
+
|
40
|
+
k = k.to_s.gsub("_", ".")
|
41
|
+
return @settings[k] if @settings.include?(k)
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
data/lib/typhon/heads.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
class Typhon
|
2
|
+
class Heads
|
3
|
+
# these methods are here to help the DSL have somewhere to
|
4
|
+
# store instances of the heads and some utilities to manage
|
5
|
+
# them. This is effectively a global named scope that just
|
6
|
+
# holds blocks of codes
|
7
|
+
class << self
|
8
|
+
def register_head(name, files, head)
|
9
|
+
@heads ||= {}
|
10
|
+
|
11
|
+
[files].flatten.each do |file|
|
12
|
+
@heads[file] ||= {}
|
13
|
+
|
14
|
+
raise "Already have a head called #{name} for file #{file}" if @heads[file].include?(name)
|
15
|
+
|
16
|
+
@heads[file][name] = head
|
17
|
+
|
18
|
+
Log.debug("Registered a new head: #{name} for file #{file}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear!
|
23
|
+
Log.debug("Clearing previously loaded heads")
|
24
|
+
@heads = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def heads
|
28
|
+
@heads || {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def files
|
32
|
+
@heads.keys
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@dir = File.join(Config.configdir, "heads")
|
38
|
+
@tails = {}
|
39
|
+
@linecount = 0
|
40
|
+
@starttime = Time.now
|
41
|
+
end
|
42
|
+
|
43
|
+
def log_stats
|
44
|
+
uptime = seconds_to_human((Time.now - @starttime).to_i)
|
45
|
+
|
46
|
+
Log.info("Up for #{uptime} read #{@linecount} lines")
|
47
|
+
end
|
48
|
+
|
49
|
+
# Handles a line of text from a log file by finding the
|
50
|
+
# heads associated with that file and calling them all
|
51
|
+
def feed(file, pos, text)
|
52
|
+
return unless Heads.heads.include?(file)
|
53
|
+
|
54
|
+
Heads.heads[file].each_pair do |name, head|
|
55
|
+
begin
|
56
|
+
head.call(file, pos, text)
|
57
|
+
@linecount += 1
|
58
|
+
rescue Exception => e
|
59
|
+
Log.error("Failed to handle line from #{file}##{pos} with head #{name}: #{e.class}: #{e}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Loads/Reload all the heads from disk, a trigger file is used that
|
65
|
+
# the user can touch the trigger and it will initiate a complete reload
|
66
|
+
def loadheads
|
67
|
+
if File.exist?(triggerfile)
|
68
|
+
triggerage = File::Stat.new(triggerfile).mtime.to_f
|
69
|
+
else
|
70
|
+
triggerage = 0
|
71
|
+
end
|
72
|
+
|
73
|
+
@loaded ||= 0
|
74
|
+
|
75
|
+
if (@loaded < triggerage) || @loaded == 0
|
76
|
+
Heads.clear!
|
77
|
+
headfiles.each do |head|
|
78
|
+
loadhead(head)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
starttails
|
83
|
+
|
84
|
+
@loaded = Time.now.to_f
|
85
|
+
end
|
86
|
+
|
87
|
+
# Start EM tailers for each known file. If a file has become orphaned
|
88
|
+
# by all its heads being removed then close the tail
|
89
|
+
def starttails
|
90
|
+
# for all the files that have interested heads start tailers
|
91
|
+
Typhon.files.each do |file|
|
92
|
+
unless @tails.include?(file)
|
93
|
+
Log.debug("Starting a new tailer for #{file}")
|
94
|
+
@tails[file] = EventMachine::file_tail(file) do |ft, line|
|
95
|
+
self.feed(ft.path, ft.position, line)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# for all the tailers make sure there are files, else close the tailer
|
101
|
+
@tails.keys.each do |file|
|
102
|
+
unless Typhon.files.include?(file)
|
103
|
+
Log.debug("Closing tailer for #{file} there are no heads attached")
|
104
|
+
|
105
|
+
begin
|
106
|
+
@tails[file].close
|
107
|
+
rescue
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def loadhead(head)
|
114
|
+
Log.debug("Loading head #{head}")
|
115
|
+
load head
|
116
|
+
rescue Exception => e
|
117
|
+
puts "Failed to load #{head}: #{e.class}: #{e}"
|
118
|
+
p e.backtrace
|
119
|
+
end
|
120
|
+
|
121
|
+
def headfiles
|
122
|
+
if File.directory?(@dir)
|
123
|
+
Dir.entries(@dir).grep(/head.rb$/).map do |f|
|
124
|
+
File.join([@dir, f])
|
125
|
+
end
|
126
|
+
else
|
127
|
+
raise "#{@dir} is not a directory"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def triggerfile
|
132
|
+
File.join([@dir, "reload.txt"])
|
133
|
+
end
|
134
|
+
|
135
|
+
# borrowed from ohai, thanks Adam.
|
136
|
+
def seconds_to_human(seconds)
|
137
|
+
days = seconds.to_i / 86400
|
138
|
+
seconds -= 86400 * days
|
139
|
+
|
140
|
+
hours = seconds.to_i / 3600
|
141
|
+
seconds -= 3600 * hours
|
142
|
+
|
143
|
+
minutes = seconds.to_i / 60
|
144
|
+
seconds -= 60 * minutes
|
145
|
+
|
146
|
+
if days > 1
|
147
|
+
return sprintf("%d days %02d hours %02d minutes %02d seconds", days, hours, minutes, seconds)
|
148
|
+
elsif days == 1
|
149
|
+
return sprintf("%d day %02d hours %02d minutes %02d seconds", days, hours, minutes, seconds)
|
150
|
+
elsif hours > 0
|
151
|
+
return sprintf("%d hours %02d minutes %02d seconds", hours, minutes, seconds)
|
152
|
+
elsif minutes > 0
|
153
|
+
return sprintf("%d minutes %02d seconds", minutes, seconds)
|
154
|
+
else
|
155
|
+
return sprintf("%02d seconds", seconds)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
data/lib/typhon/log.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
class Typhon
|
2
|
+
class Log
|
3
|
+
require 'syslog'
|
4
|
+
|
5
|
+
include Syslog::Constants
|
6
|
+
|
7
|
+
@configured = false
|
8
|
+
|
9
|
+
@known_levels = [:debug, :info, :warn, :error, :fatal]
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def log(msg, severity=:debug)
|
13
|
+
configure unless @configured
|
14
|
+
|
15
|
+
if @known_levels.index(severity) >= @known_levels.index(@active_level)
|
16
|
+
Syslog.send(valid_levels[severity.to_sym], "#{from} #{msg}")
|
17
|
+
end
|
18
|
+
rescue Exception => e
|
19
|
+
STDERR.puts("Failed to log: #{e.class}: #{e}: original log message: #{severity}: #{msg}")
|
20
|
+
STDERR.puts(e.backtrace.join("\n\t"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def configure
|
24
|
+
Syslog.close if Syslog.opened?
|
25
|
+
Syslog.open(File.basename($0))
|
26
|
+
|
27
|
+
@active_level = Config[:loglevel]
|
28
|
+
|
29
|
+
raise "Unknown log level #{@active_level} specified" unless valid_levels.include?(@active_level)
|
30
|
+
|
31
|
+
@configured = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# figures out the filename that called us
|
35
|
+
def from
|
36
|
+
from = File.basename(caller[4])
|
37
|
+
end
|
38
|
+
|
39
|
+
def valid_levels
|
40
|
+
{:info => :info,
|
41
|
+
:warn => :warning,
|
42
|
+
:debug => :debug,
|
43
|
+
:fatal => :crit,
|
44
|
+
:error => :err}
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_missing(level, *args, &block)
|
48
|
+
super unless [:info, :warn, :debug, :fatal, :error].include?(level)
|
49
|
+
|
50
|
+
log(args[0], level)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Typhon
|
2
|
+
class StompClient < EM::Connection
|
3
|
+
include EM::Protocols::Stomp
|
4
|
+
|
5
|
+
def initialize(params={})
|
6
|
+
@connected = false
|
7
|
+
@options = {:auto_reconnect => true, :timeout => 2, :max_queue_size => 500}
|
8
|
+
@queue = EM::Queue.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection_completed
|
12
|
+
connect :login => Config[:stomp][:user], :passcode => Config[:stomp][:pass]
|
13
|
+
|
14
|
+
Log.debug("Authenticated to %s:%d" % [ Config[:stomp][:server], Config[:stomp][:port] ])
|
15
|
+
@connected = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def unbind
|
19
|
+
Log.error("Connection to %s:%d failed" % [ Config[:stomp][:server], Config[:stomp][:port] ])
|
20
|
+
@connected = false
|
21
|
+
|
22
|
+
EM.add_timer(@options[:timeout]) do
|
23
|
+
Log.debug("Connecting to Stomp Server %s:%d" % [ Config[:stomp][:server], Config[:stomp][:port] ])
|
24
|
+
reconnect Config[:stomp][:server], Config[:stomp][:port]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def connected?
|
29
|
+
(@connected && !error?)
|
30
|
+
end
|
31
|
+
|
32
|
+
def publish(topic, message, param={})
|
33
|
+
if connected?
|
34
|
+
send(topic, message, param)
|
35
|
+
|
36
|
+
until @queue.empty? do
|
37
|
+
@queue.pop do |msg|
|
38
|
+
send(msg[:topic], msg[:message], msg[:param])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
if @queue.size < @options[:max_queue_size]
|
43
|
+
@queue.push({:topic => topic, :message => message, :param => param})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: typhon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- R.I.Pienaar
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-29 00:00:00 +01:00
|
19
|
+
default_executable: typhon
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Single daemon that tails many files and route lines through your own logic
|
23
|
+
email: rip@devco.net
|
24
|
+
executables:
|
25
|
+
- typhon
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- bin/typhon
|
32
|
+
- lib/typhon/config.rb
|
33
|
+
- lib/typhon/stompclient.rb
|
34
|
+
- lib/typhon/heads.rb
|
35
|
+
- lib/typhon/log.rb
|
36
|
+
- lib/typhon.rb
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: https://github.com/ripienaar/typhon/
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.3.7
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Wrapper around eventmachine-tail to make writing custom logtailers easy
|
71
|
+
test_files: []
|
72
|
+
|