log2mail 0.0.1.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,216 @@
1
+ module Log2mail
2
+ class Config
3
+
4
+ class <<self
5
+ def parse_config(config_path)
6
+ Log2mail::Config.new(config_path)
7
+ # pp config.config
8
+ # config.files.each do |f|
9
+ # puts "File: #{f}"
10
+ # config.patterns_for_file(f).each do |pattern|
11
+ # puts " Pattern: #{pattern}; mailto: " + config.mailtos_for_pattern( f, pattern ).join(', ')
12
+ # end
13
+ # end
14
+ end
15
+ end
16
+
17
+ # attr_reader :config
18
+
19
+ INT_OPTIONS = [:sendtime, :resendtime, :maxlines]
20
+ STR_OPTIONS = [:fromaddr, :sendmail]
21
+ PATH_OPTIONS = [:template]
22
+
23
+ def initialize(config_paths)
24
+ $logger.debug "Reading configuration from #{config_paths}"
25
+ @config_paths = Array(config_paths)
26
+ expand_paths
27
+ read_configuration
28
+ validate_configuration
29
+ end
30
+
31
+ # returns all the paths of all files needed to be watched
32
+ def files
33
+ @config.keys - [:defaults]
34
+ end
35
+
36
+ def file_patterns
37
+ h = {}
38
+ files.each do |file|
39
+ h[file] = patterns_for_file(file)
40
+ end
41
+ h
42
+ end
43
+
44
+ def patterns_for_file( file )
45
+ Hash(@config[file][:patterns]).keys + \
46
+ Hash(defaults[:patterns]).keys
47
+ end
48
+
49
+ def mailtos_for_pattern( file, pattern )
50
+ m = []
51
+ m.concat Hash( config_file_pattern(file, pattern)[:mailtos] ).keys
52
+ m.concat Hash(Hash(Hash(defaults[:patterns])[pattern])[:mailtos]).keys
53
+ m.concat Array(defaults[:mailtos]) if m.empty?
54
+ m.uniq
55
+ end
56
+
57
+ def settings_for_mailto( file, pattern, mailto )
58
+ h = defaults.reject {|k,v| k==:mailtos}
59
+ h.merge config_file_mailto(file, pattern, mailto)
60
+ end
61
+
62
+ def defaults
63
+ Hash(@config[:defaults])
64
+ end
65
+
66
+ def formatted( show_effective )
67
+ Terminal::Table.new do |t|
68
+ settings_header = show_effective ? 'Effective Settings' : 'Settings'
69
+ t << ['File', 'Pattern', 'Recipient', settings_header]
70
+ t << :separator
71
+ files.each do |file|
72
+ patterns_for_file(file).each do |pattern|
73
+ mailtos_for_pattern(file, pattern).each do |mailto|
74
+ settings = []
75
+ if show_effective
76
+ settings_for_mailto(file, pattern, mailto).each_pair \
77
+ { |k,v| settings << '%s=%s' % [k,v] }
78
+ else
79
+ config_file_mailto(file, pattern, mailto).each_pair \
80
+ { |k,v| settings << '%s=%s' % [k,v] }
81
+ end
82
+ t.add_row [file, pattern, mailto, settings.join($/)]
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def config_file(file)
92
+ Hash(@config[file])
93
+ end
94
+
95
+ def config_file_pattern(file, pattern)
96
+ Hash( Hash( config_file(file)[:patterns] )[pattern] )
97
+ end
98
+
99
+ def config_file_mailtos(file, pattern)
100
+ Hash( config_file_pattern(file, pattern)[:mailtos] )
101
+ end
102
+
103
+ def config_file_mailto(file, pattern, mailto)
104
+ Hash( config_file_mailtos(file, pattern)[mailto] )
105
+ end
106
+
107
+ def expand_paths
108
+ expanded_paths = []
109
+ @config_paths.each do |path|
110
+ if ::File.directory?(path)
111
+ expanded_paths.concat Dir.glob( ::File.join( path, '*[^~#]' ) )
112
+ else
113
+ expanded_paths << path
114
+ end
115
+ end
116
+ @config_paths = expanded_paths
117
+ end
118
+
119
+ # tries to follow original code at https://github.com/lordlamer/log2mail/blob/master/config.cc#L192
120
+ def read_configuration
121
+ @config = {}
122
+ @config_paths.each do |file|
123
+ @section = nil; @pattern = nil; @mailto = nil
124
+ # section, pattern, mailto are reset for every file (but not when included by 'include')
125
+ parse_file( file )
126
+ end
127
+ end
128
+
129
+ def parse_file( filename )
130
+ IO.readlines(filename).each_with_index do |line, lineno|
131
+ parse(filename, line, lineno + 1)
132
+ end
133
+ rescue Errno::ENOENT
134
+ fail Error, "Configuration file or directory not found (or not readable): #{filename}"
135
+ end
136
+
137
+ def parse(file, line, lineno)
138
+ line.strip!
139
+ return if line =~ /^#/
140
+ return if line =~ /^$/
141
+ line =~ /^(\S+)\s*=?\s*"?(.*?)"?(\s*#.*)?$/ # drop double quotes on right hand side; drop comments
142
+ key, value = $1.to_sym, $2.strip
143
+ if key == :include # include shall work everywhere
144
+ parse_file( ::File.join(Pathname(file).parent, value) )
145
+ return
146
+ end
147
+ if key == :defaults and value.empty? # section: specifies top level; must be 'defaults' or 'file'
148
+ @section = key
149
+ @pattern = nil; @mailto = nil
150
+ fail Error, "Invalid section. Section 'defaults' already specified." if @config[@section]
151
+ @config[@section] = {}
152
+ elsif key == :file
153
+ @section = value
154
+ @pattern = nil; @mailto = nil
155
+ @config[@section] ||= {}
156
+ elsif key == :pattern # must come inside 'file' (or 'defaults')
157
+ # fail "Invalid section. All statements must appear after 'defaults' or 'file=...'" unless @section
158
+ @pattern = value; @mailto = nil
159
+ @config[@section][:patterns] ||= {}
160
+ warning { "Redefining pattern section '#{value}' which has been defined already for '#{@section}'." } \
161
+ if @config[@section][:patterns][value]
162
+ @config[@section][:patterns][value] = {}
163
+ elsif key == :mailto and @section != :defaults # must come inside 'pattern' (or 'defaults')
164
+ fail Error, "'mailto' statements only allowed inside 'pattern' or 'defaults'." unless @pattern
165
+ @mailto = value
166
+ @config[@section][:patterns][@pattern][:mailtos] ||= {}
167
+ warning { "Redefining mailto section '#{value}' which has been defined already for '#{@section}'." } \
168
+ if @config[@section][:patterns][@pattern][:mailtos][value]
169
+ @config[@section][:patterns][@pattern][:mailtos][value] = {}
170
+ else # everything else must come inside 'defaults' or 'mailto'
171
+ fail Error, "'#{key}' must be set within 'defaults' or 'mailto'." unless @section == :defaults or @mailto
172
+ if INT_OPTIONS.include?(key)
173
+ value = value.to_i
174
+ elsif STR_OPTIONS.include?(key)
175
+ elsif PATH_OPTIONS.include?(key)
176
+ value = ::File.expand_path( value, Pathname(file).parent ) unless Pathname(value).absolute?
177
+ elsif key == :mailto # special handling for 'mailto' in 'defaults' section
178
+ @config[:defaults][:mailtos] ||= []
179
+ @config[:defaults][:mailtos] << value
180
+ return # skip the 'mailto' entry itself
181
+ else
182
+ fail Error, "'#{key}' is an unknown configuration statement."
183
+ end
184
+ if @section == :defaults and !@pattern and !@mailto
185
+ warning { "Redefining value for '#{key}'." } if @config[@section][key]
186
+ @config[@section][key] = value
187
+ else
188
+ warning { "Redefining value for '#{key}'." } \
189
+ if @config[@section][:patterns][@pattern][:mailtos][@mailto][key]
190
+ @config[@section][:patterns][@pattern][:mailtos][@mailto][key] = value
191
+ end
192
+ end
193
+ rescue
194
+ fail Error, "#{file} (line #{lineno}): #{$!.message}#$/[#{$!.class} at #{$!.backtrace.first}]"
195
+ end
196
+
197
+ def validate_configuration
198
+ files.each do |file|
199
+ patterns_for_file(file).each do |pattern|
200
+ mailtos = mailtos_for_pattern(file, pattern)
201
+ $logger.warn "Pattern #{file}:#{pattern} has no recipients." if mailtos.empty?
202
+ end
203
+ end
204
+ # FIXME: empty configuration should cause FATAL error
205
+ # TODO: illegal regexp pattern should cause ERROR
206
+ end
207
+
208
+ def warning(&block)
209
+ file = block.binding.eval('file')
210
+ lineno = block.binding.eval('lineno')
211
+ message = block.call
212
+ $logger.warn "#{file} (line #{lineno}): #{message}#$/[at #{caller.first}]"
213
+ end
214
+
215
+ end
216
+ end
@@ -0,0 +1,56 @@
1
+ module Log2mail
2
+
3
+ module Console::Commands
4
+
5
+ class <<self
6
+
7
+ def desc(description)
8
+ @desc = description
9
+ end
10
+
11
+ def method_added(meth)
12
+ @@meths ||= []
13
+ meth = meth.to_s
14
+ @@meths << [meth.gsub('_', ' '), @desc] if @desc
15
+ # puts "Added method #{meth}. Desc: #{@desc.inspect}."
16
+ # puts @@meths.inspect
17
+ # meth
18
+ # super
19
+ end
20
+
21
+ end
22
+
23
+ protected
24
+
25
+ def commands
26
+ @@meths.map { |m| m.first.gsub(' ', '_') }
27
+ end
28
+
29
+ public
30
+
31
+ desc "show list of available commands"
32
+ def help
33
+ @command_table ||= Terminal::Table.new :headings => ['command','description'], :rows => @@meths.sort
34
+ puts @command_table
35
+ true
36
+ end
37
+
38
+ desc "end console session"
39
+ def quit; end
40
+ def exit; end
41
+
42
+ desc "show configuration"
43
+ def config
44
+ config_path = ask('configuration path? ').chomp
45
+ config = Log2mail::Config.parse_config config_path
46
+ puts "Defaults:"
47
+ puts config.defaults
48
+ puts "Settings:"
49
+ puts config.formatted(false)
50
+ puts "Effective settings:"
51
+ puts config.formatted(true)
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,23 @@
1
+ module Log2mail
2
+
3
+ module Console::Logger
4
+
5
+ def info(msg)
6
+ puts msg
7
+ end
8
+
9
+ def fatal(msg)
10
+ puts "FATAL: " + msg
11
+ end
12
+
13
+ def warn(msg)
14
+ puts "WARNING: " + msg
15
+ end
16
+
17
+ def debug(msg)
18
+ puts "DEBUG: " + msg
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,35 @@
1
+ class Log2mail::Console
2
+
3
+ require_relative 'console/logger'
4
+ require_relative 'console/commands'
5
+ include Log2mail::Console::Commands
6
+ require 'highline/import'
7
+
8
+ def run
9
+
10
+ # PFUSCH!!!
11
+ # Log2mail::Config.extend Log2mail::Console::Logger
12
+ # Log2mail::Config.include Log2mail::Console::Logger
13
+
14
+ loop do
15
+ input = ask('log2mail.rb % ').chomp
16
+ # command, *params = input.split /\s/
17
+ command = input
18
+ next if command.empty?
19
+ command.gsub!(' ', '_')
20
+ (quit; return) if ['quit', 'exit'].include?(command)
21
+ if self.commands.include?(command)
22
+ send(command)
23
+ else
24
+ puts "Unknown command. Use 'help' for more information."
25
+ end
26
+ end
27
+ rescue EOFError
28
+ quit; return
29
+ end
30
+
31
+ def quit
32
+ puts "quitting..."
33
+ end
34
+
35
+ end
@@ -0,0 +1,5 @@
1
+ module Log2mail
2
+
3
+ class Error < RuntimeError; end
4
+
5
+ end
@@ -0,0 +1,49 @@
1
+ module Log2mail
2
+
3
+ module File::Parser
4
+
5
+ def parse(multiline_text)
6
+ return [] unless multiline_text
7
+ empty_buf! unless @buf
8
+ # add new text to parse buffer
9
+ @buf = @buf << multiline_text
10
+ hits = []
11
+ @patterns.each do |pattern|
12
+ matches = []
13
+ if pattern.from_string?
14
+ matches.concat @buf.lines.find_all{ |line| line.match(pattern) }
15
+ matches.map!(&:chomp)
16
+ else
17
+ matches.concat @buf.gsub(pattern).collect.to_a
18
+ end
19
+ hits.concat matches.map { |match| Hit.new( match, pattern, @path ) }
20
+ end
21
+ unless hits.empty?
22
+ log 'pattern match: ' + hits.inspect
23
+ empty_buf! # match -> clear buffer
24
+ else
25
+ cleanup_buf! # no match -> just keep what's necessary
26
+ end
27
+ hits
28
+ end
29
+
30
+ # ---------------------
31
+ # - Buffer management -
32
+ # ---------------------
33
+
34
+ def empty_buf!
35
+ @buf = ''
36
+ end
37
+
38
+ def cleanup_buf!
39
+ # FIXME: cleanup should clean up to latest match only OR upto last line (if from_string true)
40
+ if @patterns.all? {|p| p.from_string? }
41
+ empty_buf!
42
+ else
43
+ @buf = @buf.byteslice(-@maxbufsize/32,@maxbufsize) if @buf.bytesize > @maxbufsize
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,95 @@
1
+
2
+ module Log2mail
3
+
4
+ class File
5
+
6
+ require_relative 'file/parser'
7
+ include Parser
8
+
9
+
10
+ # FIXME: redundant
11
+ def log(msg, sev = ::Logger::DEBUG)
12
+ $logger.log sev, '%s: %s%s [%s]' % [@path, msg, $/, caller.first]
13
+ end
14
+
15
+ def warn(msg)
16
+ log(msg, ::Logger::WARN)
17
+ end
18
+
19
+ attr_reader :path, :patterns
20
+
21
+ def initialize( path, patterns, maxbufsize = 65536 )
22
+ @path = path
23
+ self.patterns = patterns
24
+ @maxbufsize = maxbufsize
25
+ log "Maximum buffer size: #{@maxbufsize}"
26
+ log "Patterns: #{patterns.inspect}"
27
+ end
28
+
29
+ def patterns=(patterns)
30
+ @patterns = patterns.map(&:to_r)
31
+ end
32
+
33
+ # ----------------------------
34
+ # - File management (public) -
35
+ # ----------------------------
36
+
37
+ def open
38
+ @f = ::File.open(@path, 'r', :encoding => "BINARY")
39
+ log "file opened"
40
+ @ino = @f.stat.ino
41
+ @size = 0
42
+ @f
43
+ rescue Errno::ENOENT
44
+ warn "does not exist"
45
+ false
46
+ end
47
+
48
+ def seek_to_end
49
+ @f.seek(0, IO::SEEK_END)
50
+ @size = @f.stat.size
51
+ @f
52
+ end
53
+
54
+ def read_to_end
55
+ return unless @f
56
+ s = @f.gets(nil)
57
+ @size += s.length
58
+ s
59
+ end
60
+
61
+ def eof?
62
+ !@f or @f.eof?
63
+ end
64
+
65
+ def rotated?
66
+ if inum_changed?
67
+ log "inode number changed"
68
+ true
69
+ elsif file_size_changed?
70
+ log "file size changed; probably truncated"
71
+ true
72
+ else
73
+ false
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # -------------------
80
+ # - File management -
81
+ # -------------------
82
+
83
+ def inum_changed?
84
+ ::File.stat(@path).ino != @ino
85
+ rescue Errno::ENOENT
86
+ true
87
+ end
88
+
89
+ def file_size_changed?
90
+ @f.stat.size != @size
91
+ end
92
+
93
+ end
94
+
95
+ end
@@ -0,0 +1,13 @@
1
+ module Log2mail
2
+ class Hit
3
+
4
+ attr_reader :matched_text, :pattern, :file
5
+
6
+ def initialize( text, pattern, file )
7
+ @matched_text = text
8
+ @pattern = pattern
9
+ @file = file
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # provides a formatter to be used on TTY
2
+ module Log2mail::LoggerFormatter
3
+
4
+ class <<self
5
+
6
+ LEVELS = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL", "UNKNOWN"]
7
+
8
+ # http://blog.bigbinary.com/2014/03/03/logger-formatting-in-rails.html
9
+ def msg2str(msg)
10
+ case msg
11
+ when ::String
12
+ msg
13
+ when ::Exception
14
+ "#{ msg.message } (#{ msg.class })\n [" <<
15
+ ( $verbose ? \
16
+ (msg.backtrace || []).join("#$/ ") : \
17
+ (msg.backtrace || []).first
18
+ ) << ']'
19
+ else
20
+ msg.inspect
21
+ end
22
+ end
23
+
24
+ def call(severity, datetime, progname, msg)
25
+ sev = severity.instance_of?(Fixnum) ? LEVELS[severity] : severity
26
+ '%s: %s' % [sev, msg2str(msg)] + $/
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,148 @@
1
+ Main {
2
+
3
+ CONFIG_DEFAULT = '/etc/log2mail/config'
4
+ PID_DEFAULT = '/var/run/log2mail.rb.pid'
5
+ LOG_DEFAULT = '/var/log/log2mail.rb'
6
+
7
+ version Log2mail::VERSION
8
+
9
+ ### daemonizes! DOES NOT WORK CORRECTLY => implementing own forking
10
+ # daemonizes!
11
+
12
+ $logger = logger
13
+ logger_level Logger::INFO
14
+ logger.formatter = Log2mail::LoggerFormatter if logger.tty?
15
+
16
+ environment('LOG2MAIL_CONF') {
17
+ argument_required
18
+ defaults CONFIG_DEFAULT
19
+ description 'the configuration file or directory'
20
+ synopsis 'env LOG2MAIL_CONF=config_path'
21
+ }
22
+ option('config', 'c') {
23
+ argument_required
24
+ defaults CONFIG_DEFAULT
25
+ # synopsis '--config=config_path, -c'
26
+ description 'path of configuration file or directory'
27
+ }
28
+ # option('pidfile', 'p') {
29
+ # description 'path of PID file'
30
+ # }
31
+ option('verbose', 'v')
32
+
33
+ usage['MAN PAGE'] = "type 'gem man log2mail' for more information"
34
+
35
+ # usage['EXAMPLE USAGE'] = <<-EXAMPLES
36
+ # env LOG2MAIL_CONF=/usr/local/etc/log2mail.conf #{$0} start
37
+ # starts as daemon using configuration from '/usr/local/etc/log2mail.conf'
38
+ # EXAMPLES
39
+ #
40
+ # usage['CONFIGURATION FILE EXAMPLE'] = <<-EXAMPLES
41
+ # defaults
42
+ # mailto = your@mail.address
43
+ # file = /var/log/mail.log
44
+ # pattern = status=bounced # report bounced mail
45
+ # file = /var/log/syslog
46
+ # pattern = /(warning|error)/i # report warnings and errors from syslog
47
+ # EXAMPLES
48
+
49
+ def after_parse_parameters
50
+ if params['verbose'].given?
51
+ logger.level = Logger::DEBUG
52
+ $verbose = true
53
+ else
54
+ logger.level = logger_level
55
+ end
56
+ @config_path = params['config'].given? ? params['config'].value : params['LOG2MAIL_CONF'].value
57
+ @config_path ||= CONFIG_DEFAULT
58
+ end
59
+
60
+ # returns the pid(s) of the daemon
61
+ def daemon_pids
62
+ prog_name = Log2mail::PROGNAME
63
+ own_pid = Process.pid
64
+ # FIXME: finding daemon pids by using pgrep is NOT 'optimal' :-)
65
+ `pgrep -f #{prog_name}`.split.map(&:to_i) - [own_pid]
66
+ end
67
+
68
+ def daemon_running?
69
+ !daemon_pids.empty?
70
+ end
71
+
72
+ mode 'start' do
73
+ # option('daemonize', '-D') { description 'daemonize into background'}
74
+ option('nofork', '-N') { description 'no daemonizing, stay in foreground'}
75
+ option('sleeptime') {
76
+ argument_required
77
+ description 'polling interval [seconds]'
78
+ cast :int
79
+ defaults 60
80
+ }
81
+ option('maxbufsize') { argument_required; cast :int; default 65536 }
82
+ def run
83
+ fail "Not starting. Daemon running." if daemon_running? and !params['nofork'].value
84
+ config = Log2mail::Config.parse_config @config_path
85
+ unless params['nofork'].value
86
+ Process.daemon
87
+ $PROGRAM_NAME = Log2mail::PROGNAME
88
+ $logger = Syslog::Logger.new(Log2mail::PROGNAME)
89
+ $logger.formatter = Log2mail::LoggerFormatter
90
+ def $logger.log(*a,&b)
91
+ add(*a,&b)
92
+ end
93
+ logger $logger
94
+ logger.info{'Deamon started.'}
95
+ trap(:INT) do
96
+ # for whatever reason, SIGINT is NOT LOGGED automatically like other signals (SIGTERM)
97
+ fatal "SIGINT"
98
+ exit(1)
99
+ end
100
+ end
101
+ Log2mail::Watcher.new(config, params['sleeptime'].value).run
102
+ end
103
+ end
104
+
105
+ mode 'stop' do
106
+ def run
107
+ if daemon_running?
108
+ daemon_pids.each do |pid|
109
+ info "Interrupting pid #{pid}..."
110
+ Process.kill(:INT, pid)
111
+ end
112
+ else
113
+ warn "No running daemon found."
114
+ end
115
+ rescue Errno::ENOENT
116
+ fail "Require 'pgrep' on path."
117
+ end
118
+ end
119
+
120
+ # TODO: add restart mode
121
+
122
+ mode 'status' do
123
+ def run
124
+ unless daemon_running?
125
+ warn "No running daemon found."
126
+ exit 1
127
+ else
128
+ info 'Daemon running. PID: %s' % daemon_pids.join(', ')
129
+ end
130
+ end
131
+ end
132
+
133
+ mode 'configtest' do
134
+ option('effective', 'e') { description 'show effective settings' }
135
+ def run
136
+ config = Log2mail::Config.parse_config @config_path
137
+ puts config.formatted(params['effective'].value)
138
+ end
139
+ end
140
+
141
+ mode 'console' do
142
+ def run
143
+ Log2mail::Console.new.run
144
+ end
145
+ end if Log2mail.const_defined?(:Console)
146
+
147
+ alias_method :run, :help!
148
+ }