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 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
+
@@ -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
+