logtwuncator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.1.0 / 2007-10-10
2
+
3
+ * Its twuncating season!
4
+
data/Manifest.txt ADDED
@@ -0,0 +1,19 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ example.yml
6
+ bin/log_twuncator_service
7
+ bin/log_twuncator_daemon
8
+ lib/log_twuncator.rb
9
+ lib/log_twuncator/truncator.rb
10
+ lib/log_twuncator/options.rb
11
+ lib/log_twuncator/service.rb
12
+ lib/log_twuncator/daemon.rb
13
+ lib/log_twuncator/win32_file.rb
14
+ test/test_config.yml
15
+ test/test_daemon.rb
16
+ test/test_truncator.rb
17
+ test/test_win32_file.rb
18
+ test/test_service.rb
19
+ test/test_helper.rb
data/README.txt ADDED
@@ -0,0 +1,186 @@
1
+ log_twuncator
2
+ by Adam Meehan (adam.meehan@gmail.com)
3
+ http://log_twuncator
4
+
5
+ == DESCRIPTION:
6
+
7
+ A log truncator for win32 originally to truncate rails log files but
8
+ generalised to any log files needing truncation and archiving. There
9
+ didn't seem be something easy, free and open out there so I thought
10
+ I would hack up a gem.
11
+
12
+ It comes with a windows service to install and leave running to monitor
13
+ the directories you specify in the config file. The config file allows the
14
+ specification of many directories to monitor for log file age and size.
15
+
16
+ File age is monitored by using the file creation date. In *nix programs like
17
+ logrotate the log file can be moved and recreated using POSIX signalling to
18
+ restart the process, and hence starting with a new file and creation date. On
19
+ windows this is not possible without getting really intimate with the running
20
+ process, so to get around it this program uses win32 API call to change the
21
+ created date after arching and truncating the file, leaving it in plave.
22
+ This seems to work fine with Rails and Apache but may cause problems
23
+ depending on how the running application has opened the log file to append
24
+ to it.
25
+
26
+ Please report any problems/suggestions you have.
27
+
28
+ Credits:
29
+
30
+ Herryanto Siatono (http://www.pluitsolutions.com/) for his
31
+ win32 Ferret service which I based some of the service structure on.
32
+
33
+ pdumpfs project (http://0xcc.net/pdumpfs/index.html.en) where I learned
34
+ the win32 API stuff in Ruby to do the file creation date changes.
35
+
36
+
37
+ == FEATURES/PROBLEMS:
38
+
39
+ * Multiple log truncation and archiving
40
+ * Nominate specific file or directory to monitor for truncation
41
+ * Use name formatting for archived file name
42
+ * Set defaults for settings to use in multiple monitored locations
43
+ * Set file or folder specific size and/or age limit
44
+
45
+ Future:
46
+ * Optional formats for the timestamp
47
+ * More archive name options
48
+ * Compress archived logs
49
+ * Email archived logs
50
+
51
+ == REQUIREMENTS:
52
+
53
+ win32-service gem required to run as service
54
+
55
+ == INSTALL:
56
+
57
+ gem install log_twuncator
58
+
59
+ == USAGE:
60
+
61
+ log_truncator_service <command> -N service_name <options>
62
+
63
+ For help
64
+
65
+ log_truncator_service -h
66
+
67
+ To install service:
68
+
69
+ log_truncator_service install -N log_twuncator -c c:/twuncator_config.yml -d 1 -l c:/log_twuncator.log
70
+
71
+ where:
72
+ -N : is the name of the service to install
73
+ -c : is the config file with the settings for various log monitoring paths
74
+ -d : is the number of minutes between checking the log files for truncation. Default is 5.
75
+ -l : is the log file for the truncator. Default is 'log_twuncator.log' is same folder as config file.
76
+
77
+ To remove service
78
+
79
+ log_truncator_service remove -N log_twuncator
80
+
81
+ To start/stop service
82
+
83
+ log_truncator_service start -N log_twuncator
84
+
85
+ log_truncator_service stop -N log_twuncator
86
+
87
+ And remember kids the service creation does not set the startup-type to 'Automatic',
88
+ so if you it want it to start with every windows startup then change this setting in
89
+ the Services management console ('services.msc' on the command prompt).
90
+
91
+ --- Config File
92
+
93
+ The config file allows for multiple monitored source directories for log files. The
94
+ setting options are as follows:
95
+
96
+ source_dir:
97
+ This is the root folder to monitor
98
+
99
+ filter:
100
+ This is used filter the files in the folder and takes the allowed settings used in
101
+ the ruby Dir.glob method. You could specify '*.log' for all files with log extension.
102
+ Or a full filename like 'production.log' to limit to just one file. See Dir.glob
103
+ method for more fancy possibilites including files in subdirectories.
104
+
105
+ age:
106
+ The age in days of the file before it should be archived and truncated. If set to
107
+ zero then age is never used as the trigger to truncate.
108
+
109
+ size:
110
+ The size of the log file in kilobytes above which the file should be truncated.
111
+ If set to 0 then size is never used as the trigger to truncate
112
+
113
+ archive_dir:
114
+ The full path directory to store the archived log file
115
+
116
+ archive_name:
117
+ The format of the archived log file name. You can use three components mixed with
118
+ any other characters you wish to insert into the file name when its archived. The
119
+ components are
120
+
121
+ [set_name] - the value of key in the config file for the set of options
122
+ [basename] - the original log filename without its extension
123
+ [ext] - the file extension of the log file
124
+ [timestamp] - the timestamp in the format yyyymmddHHMMSS
125
+
126
+ These can be combined to make the archived file name eg.
127
+
128
+ [basename]_[timestamp].[ext] - a log file called production.log archived at
129
+ 2007-01-01 12:12:35 would become production_20070101121235.log
130
+
131
+ The config file allows for a 'defaults' section where you can set the defaults for
132
+ all subsequent monitored directories. Any setting specified in a monitoring set
133
+ will override the defaults for the same setting.
134
+
135
+ Best shown with an example:
136
+
137
+ Below is config settings in yaml format
138
+ ---------------------------------------------------------------------------
139
+ defaults:
140
+ age: 7
141
+ size: 5000
142
+ filter: '*.log'
143
+ archive_dir: 'c:/logs'
144
+ archive_name: '[set_name]_[basename]_[date].[ext]'
145
+
146
+ app_name:
147
+ source_dir: 'c:/web/app/logs'
148
+ filter: 'production.log'
149
+ age: 0
150
+ archive_dir: 'c:/web/app/logs/archive'
151
+ ---------------------------------------------------------------------------
152
+
153
+ This config defaults section defines all the settings, but keep in mind that a
154
+ defaults section alone does not constitute a monitored set. If you only have a
155
+ defaults section then nothing will be monitored. In the example the 'app_name'
156
+ monitored set will override the default source directory, filter, age and
157
+ archive_dir. The age is set to 0 so files will not be truncated based on age.
158
+ It will inherit the default size of 5000 (5000 KB or 5 MB) as the threshold for
159
+ truncation above which the file will be archived in c:/web/app/logs/archive
160
+ and truncated.
161
+
162
+
163
+ == LICENSE:
164
+
165
+ (The MIT License)
166
+
167
+ Copyright (c) 2007 Adam Meehan
168
+
169
+ Permission is hereby granted, free of charge, to any person obtaining
170
+ a copy of this software and associated documentation files (the
171
+ 'Software'), to deal in the Software without restriction, including
172
+ without limitation the rights to use, copy, modify, merge, publish,
173
+ distribute, sublicense, and/or sell copies of the Software, and to
174
+ permit persons to whom the Software is furnished to do so, subject to
175
+ the following conditions:
176
+
177
+ The above copyright notice and this permission notice shall be
178
+ included in all copies or substantial portions of the Software.
179
+
180
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
181
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
182
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
183
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
184
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
185
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
186
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/log_twuncator.rb'
6
+
7
+ Hoe.new('logtwuncator', LogTwuncator::VERSION) do |p|
8
+ p.rubyforge_name = 'logtwuncator'
9
+ p.author = 'Adam Meehan'
10
+ p.email = 'adam.meehan@gmail.com'
11
+ p.summary = 'A win32 log file truncator and archiver with windows service'
12
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
13
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
14
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
15
+ p.remote_rdoc_dir = ''
16
+ p.extra_deps << ['win32-service', '>= 0.5.2']
17
+ end
18
+
19
+ # vim: syntax=Ruby
@@ -0,0 +1,6 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'log_twuncator/daemon'
4
+
5
+ d = LogTwuncator::Daemon.run(ARGV)
6
+ d.mainloop
@@ -0,0 +1,5 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ require 'log_twuncator/service'
4
+
5
+ LogTwuncator::Service.new.run(ARGV)
data/example.yml ADDED
@@ -0,0 +1,14 @@
1
+ defaults:
2
+ age: 7
3
+ size: 5000
4
+ filter: '*.log'
5
+ archive_dir: 'w:/logs'
6
+ archive_name: '[set_name]_[basename]_[timestamp].[ext]'
7
+
8
+ app_name:
9
+ source_dir:
10
+ filter:
11
+ age:
12
+ size:
13
+ archive_dir:
14
+ archive_name:
@@ -0,0 +1,3 @@
1
+ class LogTwuncator
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,88 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'logger'
4
+ require 'win32/service'
5
+ require 'log_twuncator/truncator'
6
+ require 'log_twuncator/options'
7
+
8
+ module LogTwuncator
9
+
10
+ class ConfigFileNotFound < StandardError; end
11
+
12
+ class Daemon < Win32::Daemon
13
+ attr_reader :config, :delay, :truncators, :logger
14
+
15
+ def self.run(args = ARGV)
16
+ new LogTwuncator::Options.parse(args)
17
+ end
18
+
19
+ def initialize(options)
20
+ @config, @delay, @log = options[:config], options[:delay], options[:log]
21
+ @truncators = []
22
+ @log = File.join(File.dirname(@config), 'log_twuncator.log') unless options[:log]
23
+ @logger = Logger.new(@log)
24
+ end
25
+
26
+ def service_init
27
+ log "starting service"
28
+ load_config
29
+ end
30
+
31
+ def service_main
32
+ while running?
33
+ @truncators.each {|truncator| truncator.truncate }
34
+
35
+ # sleep for 1 second for number of seconds in delay minutes which prevents process blocking
36
+ (@delay * 60).times {sleep 1}
37
+ end
38
+ end
39
+
40
+ def service_stop
41
+ log "stopping service"
42
+ end
43
+
44
+ # Loads config yaml file. Sets the truncator class defaults from the config section if one is labelled 'defaults'.
45
+ # Create truncators for each config section, which are run in the service_main loop.
46
+ # Config file can have ERB tags embedded for power and convenience
47
+ def load_config
48
+ raise LogTwuncator::ConfigFileNotFound unless File.exists?(@config)
49
+ config = YAML::load(ERB.new(IO.read(@config)).result)
50
+ set_defaults(symbolize_keys(config.delete('defaults'))) if config['defaults']
51
+ config.map do |key, options|
52
+ options[:logger] = @logger
53
+ options[:name] = key
54
+ begin
55
+ @truncators << LogTwuncator::Truncator.new(symbolize_keys(options))
56
+ rescue InvalidTruncatorOptions => e
57
+ log "Config set '#{key}' was skipped due to the following errors:\n#{e.message}"
58
+ end
59
+ end
60
+ rescue ConfigFileNotFound
61
+ log "Config file not found"
62
+ rescue => e
63
+ log "Unknown error occurred: #{e}"
64
+ end
65
+
66
+ private
67
+ # Sets the class defaults for the truncator class
68
+ def set_defaults(options)
69
+ LogTwuncator::Truncator.defaults = options
70
+ end
71
+
72
+ def log(msg)
73
+ if @logger
74
+ @logger.info msg
75
+ else
76
+ puts msg
77
+ end
78
+ end
79
+
80
+ # Taken from rails
81
+ def symbolize_keys(hash)
82
+ hash.inject({}) do |options, (key, value)|
83
+ options[key.to_sym] = value
84
+ options
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,47 @@
1
+ require 'optparse'
2
+
3
+ module LogTwuncator
4
+ class Options
5
+
6
+ def self.parse(args = ARGV)
7
+ options = {}
8
+ options[:delay] = 5
9
+ opts = OptionParser.new do |opts|
10
+ opts.banner = "Usage: log_twuncator_daemon.rb [options]"
11
+ opts.separator ""
12
+ opts.separator "Specific options:"
13
+
14
+ opts.on("-d", "--delay N", Integer, "Check files for truncation every N minutes (default: 5)") do |v|
15
+ options[:delay] = v
16
+ end
17
+
18
+ opts.on("-c", "--config FILE", String, "Config file with projects session files to check") do |v|
19
+ options[:config] = v
20
+ end
21
+
22
+ opts.on("-l", "--log FILE", String, "Log file for truncator") do |v|
23
+ options[:log] = v
24
+ end
25
+
26
+ opts.on_tail("-h", "--help", "Show this message") do
27
+ puts opts
28
+ exit
29
+ end
30
+ end
31
+ opts.parse!(args)
32
+ validate_options options
33
+ options
34
+ end
35
+
36
+ def self.validate_options(options)
37
+ unless options[:delay]
38
+ puts "Delay in seconds required [-d N]"
39
+ exit
40
+ end
41
+ unless options[:config]
42
+ puts "Config file required [-c FILE]"
43
+ exit
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,135 @@
1
+ require 'optparse'
2
+ require 'win32/service'
3
+
4
+ module LogTwuncator
5
+ class ServiceCommand
6
+ attr_reader :command, :options
7
+
8
+ def initialize
9
+ @options = {}
10
+ @options[:delay] = 5
11
+ end
12
+
13
+ def run(args)
14
+ @command = args.shift unless args[0] == '-h'
15
+ @command = @command.downcase if @command
16
+
17
+ parse(args)
18
+ @options
19
+ end
20
+
21
+ def parse(args)
22
+ begin
23
+ opts = OptionParser.new do |opts|
24
+ opts.separator ""
25
+ opts.banner = "Usage: log_twuncator_service <command> [options]"
26
+ opts.separator ""
27
+ opts.separator "Specific options:"
28
+ opts.separator ""
29
+ opts.separator "Commands : install, remove, start and stop"
30
+ opts.separator ""
31
+ opts.on("-N", "--name SERVICE_NAME", String, "Name of windows service") do |v|
32
+ @options[:name] = v
33
+ end
34
+
35
+ opts.on("-c", "--config FILE", String, "Config file with log file truncation settings") do |v|
36
+ @options[:config] = v
37
+ end
38
+
39
+ opts.on("-d", "--delay N", Integer, "Check files for truncation every N minutes") do |v|
40
+ @options[:delay] = v
41
+ end
42
+
43
+ opts.on("-l", "--log FILE", String, "Log file for truncator") do |v|
44
+ @options[:log] = v
45
+ end
46
+
47
+ opts.on_tail("-h", "--help", "Show this message") do
48
+ puts opts.help
49
+ exit
50
+ end
51
+ end
52
+ opts.parse!(args)
53
+ validate_options
54
+ rescue OptionParser::ParseError => e
55
+ puts e
56
+ puts opts
57
+ end
58
+ end
59
+
60
+ def validate_options
61
+ errors = []
62
+ errors << "Service name is required (switch -N)" unless @options[:name]
63
+ errors << "Delay is required (switch -d)" if !@options[:delay] && @command == 'install'
64
+ errors << "Config file is required (switch -c)" if !@options[:config] && @command == 'install'
65
+ errors << "Config file does not exist" if @options[:config] && !FileTest.exists?(@options[:config])
66
+
67
+ if errors.size > 0
68
+ puts "Error found."
69
+ puts errors.join("\n")
70
+ exit
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ class Service
77
+
78
+ def install
79
+ svc = Win32::Service.new
80
+ if Win32::Service.exists?(@options[:name])
81
+ puts "Service name '#{@options[:name]}' already exists."
82
+ return
83
+ end
84
+
85
+ svc.create_service do |s|
86
+ s.service_name = @options[:name]
87
+ s.display_name = @options[:name]
88
+ s.binary_path_name = binary_path_name
89
+ end
90
+
91
+ svc.close
92
+ puts "Log Twuncator service '#{@options[:name]}' installed."
93
+ rescue Win32::ServiceError => e
94
+ puts "Service '#{@options[:name]}' failed to install due to error: #{e.message}"
95
+ end
96
+
97
+ def start
98
+ Win32::Service.start(@options[:name])
99
+ puts "'#{@options[:name]}' service started."
100
+ rescue Win32::ServiceError => e
101
+ puts "Service '#{@options[:name]}' failed to start due to error: #{e.message}"
102
+ end
103
+
104
+ def stop
105
+ Win32::Service.stop(@options[:name])
106
+ puts "'#{@options[:name]}' service stopped."
107
+ rescue Win32::ServiceError => e
108
+ puts "Service '#{@options[:name]}' failed to stop due to error: #{e.message}"
109
+ end
110
+
111
+ def remove
112
+ begin
113
+ Win32::Service.stop(@options[:name])
114
+ rescue
115
+ end
116
+
117
+ Win32::Service.delete(@options[:name])
118
+ puts "'#{@options[:name]}' service removed."
119
+ rescue Win32::ServiceError => e
120
+ puts "Service '#{@options[:name]}' failed to be removed due to error: #{e.message}"
121
+ end
122
+
123
+ def binary_path_name
124
+ path = "#{Config::CONFIG['bindir']}/rubyw #{Config::CONFIG['bindir']}/log_twuncator_daemon"
125
+ path << " -c #{@options[:config]}"
126
+ path << " -d #{@options[:delay]}"
127
+ end
128
+
129
+ def run(args)
130
+ command = LogTwuncator::ServiceCommand.new
131
+ @options = command.run(args)
132
+ self.send(command.command.to_sym)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,112 @@
1
+ require 'fileutils'
2
+ require 'log_twuncator/win32_file'
3
+
4
+ module LogTwuncator
5
+
6
+ class InvalidTruncatorOptions < StandardError; end
7
+
8
+ class Truncator
9
+ @@defaults = {
10
+ :age => 7,
11
+ :size => 1_000,
12
+ :archive_name => "[basename]_[timestamp].[ext]"
13
+ }
14
+
15
+ TIMESTAMP_FORMAT = "%Y%m%d%H%M%S"
16
+
17
+ attr_reader :name, :age, :size, :source_dir, :filter, :archive_dir, :archive_name, :logger
18
+
19
+ def initialize(options)
20
+ @options = @@defaults.merge(options)
21
+ [:name, :age, :size, :source_dir, :filter, :archive_dir, :archive_name, :logger].each {|sym| instance_variable_set("@#{sym}".to_sym, @options[sym]) }
22
+ if errors = self.class.validate_options(@options)
23
+ raise InvalidTruncatorOptions, errors.join("\n")
24
+ end
25
+ end
26
+
27
+ # Find files using source directory and filter and then loop of files checking for files
28
+ # large than _size_ in kilobytes or older than _age_ in days.
29
+ def truncate
30
+ path_filter = File.join(@source_dir, @filter)
31
+ truncated = 0
32
+ time = Time.now - age_in_seconds
33
+ Dir[path_filter].each do |file|
34
+ next unless (@size > 0 && File.stat(file).size > size_in_bytes) || (@age > 0 && File.stat(file).ctime < time)
35
+ archive_time = Time.now
36
+ archive file, archive_time
37
+ File.truncate(file, 0)
38
+ set_truncated_time(file, archive_time)
39
+ truncated += 1
40
+ log "Truncated #{file}"
41
+ end
42
+ truncated
43
+ rescue => e
44
+ log "Unknown error occurred: #{e}"
45
+ end
46
+
47
+ private
48
+
49
+ # Archive _file_name_ to archive directory specified by _archive_dir_
50
+ def archive(file_name, time = Time.now)
51
+ archive_file = archive_filename(file_name)
52
+ FileUtils.makedirs(File.dirname(archive_file))
53
+ FileUtils.copy(file_name, archive_file)
54
+ log "Archived file #{file_name} to #{archive_file}"
55
+ end
56
+
57
+ # Contruct the archived filename from the archive_name setting. The filename components are allowed which are [basename], [ext] and [timestamp].
58
+ # The archived filname is then constructed by substituting the values for the file in specified in archive_name
59
+ def archive_filename(file_name, time = Time.now)
60
+ basename = File.basename(file_name, ".*")
61
+ ext = File.extname(file_name)
62
+ time_str = time.strftime(TIMESTAMP_FORMAT)
63
+ new_name = @archive_name.sub("[set_name]", @name.to_s).sub("[basename]", basename).sub(".[ext]", ext).sub("[timestamp]", time_str)
64
+ File.join(@archive_dir, new_name)
65
+ end
66
+
67
+ def set_truncated_time(file, time)
68
+ LogTwuncator::Win32File.set_file_time(file, time, nil, nil)
69
+ end
70
+
71
+ def log(msg)
72
+ if @logger
73
+ @logger.info(msg)
74
+ else
75
+ puts msg
76
+ end
77
+ end
78
+
79
+ # File size converted from kilobytes to bytes
80
+ def size_in_bytes
81
+ @size * 1024
82
+ end
83
+
84
+ # Age converted from days into seconds
85
+ def age_in_seconds
86
+ @age * 86400
87
+ end
88
+
89
+ def self.validate_options(options)
90
+ errors = []
91
+ errors << "No source directory defined" unless options[:source_dir]
92
+ errors << "Source directory is not a valid path" unless options[:source_dir] && FileTest.exists?(options[:source_dir])
93
+ errors << "Archive directory is not a valid path" unless options[:archive_dir] && FileTest.exists?(options[:archive_dir])
94
+ errors << "No file filter defined" unless options[:filter]
95
+ errors << "Size is not a valid number" unless options[:size] && options[:size].is_a?(Fixnum)
96
+ errors << "Age is not a valid number" unless options[:age] && options[:age].is_a?(Fixnum)
97
+ errors << "Either age or size must be non-zero" if options[:size].is_a?(Fixnum) && (options[:age] == 0 && options[:size] == 0)
98
+
99
+ return errors.empty? ? false : errors
100
+ end
101
+
102
+ # Allows the class defaults to be set
103
+ def self.defaults=(value)
104
+ @@defaults = value
105
+ end
106
+
107
+ def self.defaults
108
+ @@defaults
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,48 @@
1
+ require 'Win32API'
2
+
3
+ # This code has been mostly taken from pdumpfs project http://raa.ruby-lang.org/project/pdumpfs/
4
+ # Uses the Win32API to do win32 system calls to change file dates
5
+ module LogTwuncator
6
+ class Win32File
7
+ GENERIC_WRITE = 0x40000000
8
+ GENERIC_EXECUTE = 0x20000000
9
+ GENERIC_ALL = 0x10000000
10
+ FILE_SHARE_WRITE = 2
11
+ OPEN_EXISTING = 3
12
+ FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
13
+
14
+ GetLocaltime = Win32API.new("kernel32", "GetLocalTime", "P", 'V')
15
+ SystemTimeToFileTime = Win32API.new("kernel32", "SystemTimeToFileTime", "PP", 'I')
16
+ CreateFile = Win32API.new("kernel32", "CreateFileA", "PLLLLLL", "L")
17
+ SetFileTime = Win32API.new("kernel32", "SetFileTime", "LPPP", "I")
18
+ CloseHandle = Win32API.new("kernel32", "CloseHandle", "L", "I")
19
+
20
+ # Convert Ruby time into win32 FileTime
21
+ def self.get_file_time(time)
22
+ pSYSTEMTIME = ' ' * 2 * 8 # 2byte x 8
23
+ pFILETIME = ' ' * 2 * 8 # 2byte x 8
24
+ GetLocaltime.call(pSYSTEMTIME)
25
+ time_arr = pSYSTEMTIME.unpack("S8")
26
+ time_arr[0..1] = time.year, time.month
27
+ time_arr[3..6] = time.day, time.hour, time.min, time.sec
28
+ SystemTimeToFileTime.call(time_arr.pack("S8"), pFILETIME)
29
+ pFILETIME
30
+ end
31
+
32
+ # Set created, accessed and modified dates for a file
33
+ def self.set_file_time(file, ctime=nil, atime=nil, mtime=nil)
34
+ hFile = 0
35
+
36
+ times = [ctime, atime, mtime].collect do |time|
37
+ next( 0 ) if time.nil?
38
+ get_file_time(time.dup.utc)
39
+ end
40
+
41
+ hFile = CreateFile.call(file.dup, GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0)
42
+ res = SetFileTime.call(hFile, times[0], times[1], times[2])
43
+ CloseHandle.call(hFile)
44
+ return res
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ defaults:
2
+ age: 7
3
+ size: 5000
4
+ filter: '*.log'
5
+ archive_dir: <%= File.dirname(File.expand_path(__FILE__)) + '/test/test_archive' %>
6
+ archive_name: '[basename]_[timestamp].[ext]'
7
+
8
+ app_name:
9
+ source_dir: <%= File.dirname(File.expand_path(__FILE__)) + '/test/test_log' %>
10
+ filter: '*.log'
11
+ age: 2
12
+ size: 1
@@ -0,0 +1,44 @@
1
+ require 'test/test_helper'
2
+ require 'log_twuncator/daemon'
3
+
4
+ class TestDaemon < Test::Unit::TestCase
5
+
6
+ def setup
7
+ setup_paths_and_files
8
+ @defaults = LogTwuncator::Truncator.defaults
9
+ end
10
+
11
+ def test_create_daemon_with_args
12
+ d = create
13
+ assert_equal @test_config, d.config
14
+ assert_equal 1, d.delay
15
+ end
16
+
17
+ def test_load_config
18
+ d = create
19
+ d.load_config
20
+ assert_equal 1, d.truncators.size
21
+ assert_equal @log_path, d.truncators[0].source_dir
22
+ assert_equal 'app_name', d.truncators.first.name
23
+ end
24
+
25
+ def test_truncator_class_defaults_loaded
26
+ d = create
27
+ d.load_config
28
+ assert_equal 5000, LogTwuncator::Truncator.defaults[:size]
29
+ assert_equal 7, LogTwuncator::Truncator.defaults[:age]
30
+ end
31
+
32
+ def test_log_file
33
+ d = create
34
+ d.load_config
35
+ end
36
+
37
+ def create
38
+ LogTwuncator::Daemon.run( %w{-d 1 -c } << @test_config << '-l' << @log_file)
39
+ end
40
+
41
+ def teardown
42
+ LogTwuncator::Truncator.defaults = @defaults
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ require 'test/unit'
2
+
3
+ class Test::Unit::TestCase
4
+
5
+ def setup_paths_and_files
6
+ @base_path = File.dirname(File.expand_path(__FILE__))
7
+
8
+ @archive_path = File.join(@base_path, 'test_archive')
9
+ @log_path = File.join(@base_path, 'test_log')
10
+
11
+ Dir.mkdir(@archive_path) unless FileTest.exist?(@archive_path)
12
+ Dir.mkdir(@log_path) unless FileTest.exist?(@log_path)
13
+
14
+ FileUtils.rm Dir[@log_path + "/*.*"]
15
+ FileUtils.rm Dir[@archive_path + "/*.*"]
16
+
17
+ @test_config = File.join(@base_path, 'test_config.yml')
18
+ @test_log = File.join(@log_path, 'test.log')
19
+ @log_file = File.join(@base_path, 'log_twuncator.log')
20
+ end
21
+
22
+ end
@@ -0,0 +1,40 @@
1
+ require 'test/test_helper'
2
+ require 'log_twuncator/service'
3
+ require 'win32/service'
4
+
5
+ class TestService < Test::Unit::TestCase
6
+
7
+ def setup
8
+ setup_paths_and_files
9
+ @cmd = LogTwuncator::ServiceCommand.new
10
+ end
11
+
12
+ def test_install_args
13
+ options = create_command
14
+ assert_equal 'install', @cmd.command
15
+ assert_equal 'test_log_truncator', options[:name]
16
+ assert_equal 1, options[:delay]
17
+ assert_equal @test_config, options[:config]
18
+ end
19
+
20
+ def test_create_service
21
+ assert_nothing_raised { create_service }
22
+ end
23
+
24
+ def create_command
25
+ @cmd.run( %w{install -N test_log_truncator -d 1 -c } << @test_config )
26
+ end
27
+
28
+ def create_service
29
+ name = 'test_log_truncator'
30
+ LogTwuncator::Service.new.run(%w{install -N test_log_truncator -d 1 -c } << @test_config)
31
+ assert Win32::Service.exists?(name)
32
+ yield(name) if block_given?
33
+ sleep 2
34
+ Win32::Service.delete(name)
35
+ end
36
+
37
+ def teardown
38
+ Win32::Service.delete('test_log_truncator') if Win32::Service.exists?('test_log_truncator')
39
+ end
40
+ end
@@ -0,0 +1,93 @@
1
+ require 'test/test_helper'
2
+ require 'log_twuncator/truncator'
3
+
4
+ class TestTruncator < Test::Unit::TestCase
5
+
6
+ DAY_IN_SECONDS = 86400
7
+
8
+ def setup
9
+ setup_paths_and_files
10
+ end
11
+
12
+ def test_defaults
13
+ lt = LogTwuncator::Truncator.new({:source_dir => @log_path, :archive_dir => @archive_path, :filter => '*.log'})
14
+ assert_equal 1_000, lt.size
15
+ assert_equal 7, lt.age
16
+ assert_equal '*.log', lt.filter
17
+ end
18
+
19
+ def test_defaults_overridden_with_create_options
20
+ lt = create({ :size => 2_000, :age => 30 })
21
+ assert_equal 30, lt.age
22
+ assert_equal 2_000, lt.size
23
+ end
24
+
25
+ def test_archive_name_format
26
+ file = create_dummy_log @test_log
27
+ lt = create(:filter => 'test.log', :archive_dir => @archive_path, :archive_name => '[set_name]_[basename]_[timestamp].[ext]')
28
+ time = Time.new
29
+ assert_equal File.join(@archive_path, "app_name_test_#{time.strftime("%Y%m%d%H%M%S")}.log"), lt.send(:archive_filename, @test_log, time)
30
+ end
31
+
32
+ def test_single_file_truncation
33
+ file = create_dummy_log @test_log
34
+ lt = create(:filter => 'test.log', :archive_dir => @archive_path)
35
+ lt.truncate
36
+ assert FileTest::zero?(file)
37
+ end
38
+
39
+ def test_multiple_file_truncation
40
+ file1 = create_dummy_log(File.dirname(@test_log) + '/test1.log')
41
+ file2 = create_dummy_log(File.dirname(@test_log) + '/test2.log')
42
+ lt = create(:filter => 'test[1-2]*.log', :archive_dir => @archive_path)
43
+ assert_equal 2, lt.truncate
44
+ assert FileTest::zero?(file1)
45
+ assert FileTest::zero?(file2)
46
+ assert_equal 2, Dir[@archive_path + '/*.log'].size
47
+ end
48
+
49
+ def test_file_skipped_when_size_is_zero
50
+ file = create_dummy_log @test_log
51
+ lt = create :size => 0, :age => 7
52
+ assert_equal 0, lt.truncate
53
+ end
54
+
55
+ def test_file_skipped_when_age_is_zero
56
+ file = create_dummy_log @test_log
57
+ lt = create :size => 2, :age => 0
58
+ assert_equal 0, lt.truncate
59
+ end
60
+
61
+ def test_file_skipped_when_below_size_and_age
62
+ file = create_dummy_log @test_log
63
+ lt = create :size => 2, :age => 7
64
+ assert_equal 0, lt.truncate
65
+ end
66
+
67
+ def test_file_archived_when_below_size_but_over_age
68
+ file = create_dummy_log @test_log
69
+ LogTwuncator::Win32File.set_file_time(file, Time.now - (2 * DAY_IN_SECONDS))
70
+ lt = create :size => 2, :age => 1
71
+ assert_equal 1, lt.truncate
72
+ end
73
+
74
+ def test_file_archived_when_below_age_but_over_size
75
+ file = create_dummy_log @test_log, 3
76
+ lt = create :size => 2, :age => 1
77
+ assert_equal 1, lt.truncate
78
+ end
79
+
80
+ def create(options={})
81
+ LogTwuncator::Truncator.new({:name => 'app_name', :source_dir => @log_path, :archive_dir => @archive_path, :filter => '*.log', :size => 1}.merge(options))
82
+ end
83
+
84
+ # size is in kilobytes
85
+ def create_dummy_log(name, size=1)
86
+ filesize = 0
87
+ bytes = size * 1024
88
+ File.open(name, 'w') do |f|
89
+ f.puts "A" * bytes
90
+ end
91
+ name
92
+ end
93
+ end
@@ -0,0 +1,45 @@
1
+ require 'test/test_helper'
2
+ require 'log_twuncator/win32_file'
3
+
4
+ class TestWin32File < Test::Unit::TestCase
5
+
6
+ def setup
7
+ setup_paths_and_files
8
+ @test_file = File.join(@log_path, 'test.txt')
9
+ @yesterday = yesterday = Time.now - 86400
10
+ end
11
+
12
+ def test_set_file_time_with_no_changes
13
+ f = File.new(@test_file, File::CREAT)
14
+ f.close
15
+ mtime = File.stat(@test_file).mtime
16
+ LogTwuncator::Win32File.set_file_time(@test_file, nil, nil, nil)
17
+ assert_equal mtime, File.stat(@test_file).mtime
18
+ end
19
+
20
+ def test_set_file_times_on_closed_file
21
+ file = File.open(@test_file, File::CREAT)
22
+ file.close
23
+ LogTwuncator::Win32File.set_file_time(file.path, @yesterday, nil, nil)
24
+ assert_equal @yesterday.to_s, File.stat(file.path).ctime.to_s
25
+ LogTwuncator::Win32File.set_file_time(file.path, nil, @yesterday, nil)
26
+ assert_equal @yesterday.to_s, File.stat(file.path).atime.to_s
27
+ LogTwuncator::Win32File.set_file_time(file.path, nil, nil, @yesterday)
28
+ assert_equal @yesterday.to_s, File.stat(file.path).mtime.to_s
29
+ end
30
+
31
+ def test_set_file_times_on_open_file
32
+ File.open(@test_file, File::CREAT) do |file|
33
+ LogTwuncator::Win32File.set_file_time(file.path, @yesterday, nil, nil)
34
+ assert_equal @yesterday.to_s, File.stat(file.path).ctime.to_s
35
+
36
+ # FIXME: these fail and I think its because of the file interaction which changes the dates
37
+ # LogTwuncator::Win32File.set_file_time(file.path, nil, @yesterday, nil)
38
+ # assert_equal @yesterday.to_s, File.stat(file.path).atime.to_s
39
+ # LogTwuncator::Win32File.set_file_time(file.path, nil, nil, @yesterday)
40
+ # assert_equal @yesterday.to_s, File.stat(file.path).mtime.to_s
41
+ end
42
+ end
43
+
44
+ end
45
+
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: logtwuncator
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-10-17 00:00:00 +10:00
8
+ summary: A win32 log file truncator and archiver with windows service
9
+ require_paths:
10
+ - lib
11
+ email: adam.meehan@gmail.com
12
+ homepage: " by Adam Meehan (adam.meehan@gmail.com)"
13
+ rubyforge_project: logtwuncator
14
+ description: "It comes with a windows service to install and leave running to monitor the directories you specify in the config file. The config file allows the specification of many directories to monitor for log file age and size. File age is monitored by using the file creation date. In *nix programs like logrotate the log file can be moved and recreated using POSIX signalling to restart the process, and hence starting with a new file and creation date. On windows this is not possible without getting really intimate with the running process, so to get around it this program uses win32 API call to change the created date after arching and truncating the file, leaving it in plave. This seems to work fine with Rails and Apache but may cause problems depending on how the running application has opened the log file to append to it. Please report any problems/suggestions you have. Credits:"
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Adam Meehan
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - example.yml
37
+ - bin/log_twuncator_service
38
+ - bin/log_twuncator_daemon
39
+ - lib/log_twuncator.rb
40
+ - lib/log_twuncator/truncator.rb
41
+ - lib/log_twuncator/options.rb
42
+ - lib/log_twuncator/service.rb
43
+ - lib/log_twuncator/daemon.rb
44
+ - lib/log_twuncator/win32_file.rb
45
+ - test/test_config.yml
46
+ - test/test_daemon.rb
47
+ - test/test_truncator.rb
48
+ - test/test_win32_file.rb
49
+ - test/test_service.rb
50
+ - test/test_helper.rb
51
+ test_files:
52
+ - test/test_daemon.rb
53
+ - test/test_helper.rb
54
+ - test/test_service.rb
55
+ - test/test_truncator.rb
56
+ - test/test_win32_file.rb
57
+ rdoc_options:
58
+ - --main
59
+ - README.txt
60
+ extra_rdoc_files:
61
+ - History.txt
62
+ - Manifest.txt
63
+ - README.txt
64
+ executables:
65
+ - log_twuncator_service
66
+ - log_twuncator_daemon
67
+ extensions: []
68
+
69
+ requirements: []
70
+
71
+ dependencies:
72
+ - !ruby/object:Gem::Dependency
73
+ name: win32-service
74
+ version_requirement:
75
+ version_requirements: !ruby/object:Gem::Version::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 0.5.2
80
+ version:
81
+ - !ruby/object:Gem::Dependency
82
+ name: hoe
83
+ version_requirement:
84
+ version_requirements: !ruby/object:Gem::Version::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.0
89
+ version: