log2mail 0.0.1.pre2

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