file_series 0.2.0 → 0.3.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/VERSION +1 -1
- data/file_series.gemspec +2 -2
- data/lib/file_series.rb +69 -67
- data/spec/file_series_spec.rb +169 -168
- metadata +3 -3
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/file_series.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "file_series"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Alex Dean"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-10-26"
|
13
13
|
s.description = "Automatically start writing to a new file every X seconds without any locking or file moving/renaming."
|
14
14
|
s.email = "alex@crackpot.org"
|
15
15
|
s.extra_rdoc_files = [
|
data/lib/file_series.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
# Writes to this logger will be directed to new files at a configurable frequency.
|
2
4
|
#
|
3
5
|
# => logger = FileSeries.new('.', :prefix=>'test', :rotate_every=>60)
|
@@ -17,84 +19,84 @@
|
|
17
19
|
#
|
18
20
|
|
19
21
|
class FileSeries
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
DEFAULT_DIR = '.'
|
23
|
+
DEFAULT_PREFIX = 'log'
|
24
|
+
DEFAULT_FREQ = 60
|
25
|
+
DEFAULT_SEPARATOR = "\n"
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
attr_accessor :separator
|
28
|
+
attr_accessor :dir
|
29
|
+
attr_accessor :file
|
30
|
+
attr_accessor :current_ts
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
def initialize(options={})
|
33
|
+
@dir = options[:dir] || DEFAULT_DIR
|
34
|
+
@file = nil
|
35
|
+
@current_ts = nil
|
36
|
+
@filename_prefix = options[:prefix] || DEFAULT_PREFIX
|
37
|
+
@rotate_freq = options[:rotate_every] || DEFAULT_FREQ #seconds
|
38
|
+
@binary_mode = options[:binary]
|
39
|
+
@separator = options[:separator] || DEFAULT_SEPARATOR
|
40
|
+
end
|
39
41
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
# write something to the current log file.
|
43
|
+
def write(message)
|
44
|
+
log_file.write(message.to_s + @separator)
|
45
|
+
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
-
|
47
|
+
# return a File object for the current log file.
|
48
|
+
def log_file
|
49
|
+
ts = this_period
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
# if we're in a new time period, start writing to new file.
|
52
|
+
if (! file) || (ts != current_ts)
|
53
|
+
rotate(ts)
|
54
|
+
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
+
file
|
57
|
+
end
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
# compute the current time period.
|
60
|
+
def this_period
|
61
|
+
t = Time.now.to_i
|
62
|
+
t - (t % @rotate_freq)
|
63
|
+
end
|
62
64
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
65
|
+
# close current file handle and open a new one for a new logging period.
|
66
|
+
# ts defaults to the current time period.
|
67
|
+
def rotate(ts=nil)
|
68
|
+
ts ||= this_period
|
69
|
+
@file.close if @file
|
70
|
+
@file = File.open(filename(ts), "a#{'b' if @binary_mode}")
|
71
|
+
@current_ts = ts
|
72
|
+
end
|
71
73
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
# return a string filename for the logfile for the supplied timestamp.
|
75
|
+
# defaults to current time period.
|
76
|
+
def filename(ts=nil)
|
77
|
+
ts ||= this_period
|
78
|
+
File.join(@dir, "#{@filename_prefix}-#{Time.at(ts).utc.strftime('%Y%m%d-%H%M%SZ')}-#{@rotate_freq}.log")
|
79
|
+
end
|
78
80
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
# get all files which match our pattern which are not current.
|
82
|
+
# (safe for consumption. no longer being written to.)
|
83
|
+
def complete_files
|
84
|
+
current_file = filename
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
Dir.glob(
|
87
|
+
File.join(@dir, "#{@filename_prefix}-*-#{@rotate_freq}.log")
|
88
|
+
).select do |name|
|
89
|
+
name != current_file
|
90
|
+
end
|
91
|
+
end
|
90
92
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
93
|
+
# enumerate over all the writes in a series, across all files.
|
94
|
+
def each
|
95
|
+
complete_files.sort.each do |file|
|
96
|
+
File.open(file,"r#{'b' if @binary_mode}").each_line(@separator) do |raw|
|
97
|
+
yield raw
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
99
101
|
|
100
102
|
end
|
data/spec/file_series_spec.rb
CHANGED
@@ -1,181 +1,182 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'time'
|
2
3
|
require 'timecop'
|
3
4
|
|
4
5
|
def test_dir
|
5
|
-
|
6
|
+
'/tmp/file_series_tests'
|
6
7
|
end
|
7
8
|
|
8
9
|
describe "FileSeries" do
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
11
|
+
before(:all) do
|
12
|
+
`mkdir -p #{test_dir}`
|
13
|
+
end
|
14
|
+
after(:all) do
|
15
|
+
`rm -Rf #{test_dir}`
|
16
|
+
end
|
17
|
+
before(:each) do
|
18
|
+
`rm -f #{test_dir}/*`
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should write to a new file if we are in a new time period" do
|
22
|
+
|
23
|
+
Timecop.freeze(Time.parse('1970-01-01 00:01:00Z'))
|
24
|
+
fs = FileSeries.new(:dir=>test_dir, :rotate_every => 10)
|
25
|
+
fs.write('foo')
|
26
|
+
|
27
|
+
name = File.join(test_dir,'log-19700101-000100Z-10.log')
|
28
|
+
File.exist?(name).should eq true
|
29
|
+
fs.file.flush
|
30
|
+
IO.read(name).should eq "foo\n"
|
31
|
+
|
32
|
+
Timecop.freeze(Time.parse('1970-01-01 00:01:15Z'))
|
33
|
+
fs.write('foo again')
|
34
|
+
|
35
|
+
name = File.join(test_dir,'log-19700101-000110Z-10.log')
|
36
|
+
File.exist?(name).should eq true
|
37
|
+
fs.file.flush
|
38
|
+
IO.read(name).should == "foo again\n"
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should support binary file writing" do
|
43
|
+
# TODO: need to come up with some sequence which can't be written in non-binary mode.
|
44
|
+
# Trying to write a Marshal'd version of
|
45
|
+
# https://github.com/tedconf/videometrics/blob/83ad528b013ce591ac2500d7317e0904270356f9/spec/controllers/imports_controller_spec.rb#L92
|
46
|
+
# in non-binary mode fails with
|
47
|
+
# Failure/Error: post :record_event, :event => @event
|
47
48
|
# Encoding::UndefinedConversionError:
|
48
49
|
# "\x87" from ASCII-8BIT to UTF-8
|
49
50
|
# Writing in binary mode works fine. How to reproduce that more simply for a test?
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#write" do
|
54
|
+
it "should call log_file.write with message and separator" do
|
55
|
+
fs = FileSeries.new(:separator=>'...')
|
56
|
+
|
57
|
+
fs.should_receive(:log_file) {
|
58
|
+
d = double('log_file')
|
59
|
+
d.should_receive(:write).with("foo...")
|
60
|
+
d
|
61
|
+
}
|
62
|
+
|
63
|
+
fs.write('foo')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#log_file" do
|
68
|
+
it "should call rotate if no file is open" do
|
69
|
+
fs = FileSeries.new
|
70
|
+
fs.should_receive(:rotate)
|
71
|
+
fs.log_file
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#this_period" do
|
76
|
+
it "should floor timestamp to the beginning of the current period" do
|
77
|
+
fs = FileSeries.new( :rotate_every=>20 )
|
78
|
+
now = Time.now.to_i
|
79
|
+
|
80
|
+
fs.this_period.should == (now - (now%20))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#filename" do
|
85
|
+
it "should accept a timestamp argument" do
|
86
|
+
fs = FileSeries.new( :dir=>'/tmp', :prefix=>'test', :rotate_every=>60)
|
87
|
+
fs.filename(Time.parse('1970-01-01 00:20:34Z').to_i).should eq "/tmp/test-19700101-002034Z-60.log"
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should use this_period when no timestamp is supplied" do
|
91
|
+
fs = FileSeries.new( :dir=>'/tmp', :prefix=>'test', :rotate_every=>3600)
|
92
|
+
fs.should_receive(:this_period) { Time.parse('1970-01-01 00:20:00Z').to_i }
|
93
|
+
fs.filename.should == "/tmp/test-19700101-002000Z-3600.log"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#complete_files" do
|
98
|
+
it "should find files in our series which are not in use" do
|
99
|
+
list = [
|
100
|
+
'/tmp/prefix-19700101-000200Z-60.log',
|
101
|
+
'/tmp/prefix-19700101-000300Z-60.log',
|
102
|
+
'/tmp/prefix-19700101-000400Z-60.log',
|
103
|
+
'/tmp/prefix-19700101-000500Z-60.log',
|
104
|
+
]
|
105
|
+
|
106
|
+
Dir.should_receive(:glob).with('/tmp/prefix-*-60.log') {list}
|
107
|
+
|
108
|
+
Timecop.freeze(Time.parse('1970-01-01 00:05:05Z')) do
|
109
|
+
fs = FileSeries.new(:dir=>'/tmp', :prefix=>'prefix', :rotate_every=>60)
|
110
|
+
fs.complete_files.should == (list - ['/tmp/prefix-19700101-000500Z-60.log'])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "#each" do
|
116
|
+
it "should enumerate all lines in all files in a series" do
|
117
|
+
fs = FileSeries.new(:dir=>test_dir, :rotate_every=>60, :prefix=>'events')
|
118
|
+
|
119
|
+
# write 3 files with consecutive integers.
|
120
|
+
Timecop.freeze(Time.parse('1970-01-01 01:00:00')) do
|
121
|
+
(0..9).each do |i|
|
122
|
+
fs.write i
|
123
|
+
end
|
124
|
+
end
|
125
|
+
Timecop.freeze(Time.parse('1970-01-01 01:01:00')) do
|
126
|
+
(10..19).each do |i|
|
127
|
+
fs.write i
|
128
|
+
end
|
129
|
+
end
|
130
|
+
Timecop.freeze(Time.parse('1970-01-01 01:02:00')) do
|
131
|
+
(20..29).each do |i|
|
132
|
+
fs.write i
|
133
|
+
end
|
134
|
+
end
|
135
|
+
fs.file.flush
|
136
|
+
|
137
|
+
out = []
|
138
|
+
fs.each do |line|
|
139
|
+
out << line
|
140
|
+
end
|
141
|
+
|
142
|
+
out.should == (0..29).to_a.map {|i| i.to_s+"\n" }
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should enumerate all entries in a binary file series" do
|
146
|
+
fs = FileSeries.new(
|
147
|
+
:binary=>true,
|
148
|
+
:separator=>'!~!~!',
|
149
|
+
:dir=>test_dir,
|
150
|
+
:prefix=>'bin',
|
151
|
+
:rotate_every=>60
|
152
|
+
)
|
153
|
+
|
154
|
+
Timecop.freeze(Time.parse('1970-01-01 01:00:00')) do
|
155
|
+
(0..9).each do |i|
|
156
|
+
fs.write Marshal.dump(i)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
Timecop.freeze(Time.parse('1970-01-01 01:01:00')) do
|
160
|
+
(10..19).each do |i|
|
161
|
+
fs.write Marshal.dump(i)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
Timecop.freeze(Time.parse('1970-01-01 01:02:00')) do
|
165
|
+
(20..29).each do |i|
|
166
|
+
fs.write Marshal.dump(i)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
fs.file.flush
|
170
|
+
|
171
|
+
out = []
|
172
|
+
fs.each do |line|
|
173
|
+
out << Marshal.load(line)
|
174
|
+
end
|
175
|
+
|
176
|
+
# note that we don't get the separator, and they're Fixnum not String
|
177
|
+
out.should == (0..29).to_a.map {|i| i }
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
180
181
|
|
181
182
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: file_series
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-26 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: autotest-standalone
|
@@ -176,7 +176,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
176
|
version: '0'
|
177
177
|
segments:
|
178
178
|
- 0
|
179
|
-
hash:
|
179
|
+
hash: 2396804050019200525
|
180
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
181
|
none: false
|
182
182
|
requirements:
|