typhon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+