evanescent 1.0.3 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +87 -0
- data/lib/evanescent.rb +84 -62
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f14da7d6ca2902263af5f1beb2ed7e5dcf1292b
|
4
|
+
data.tar.gz: fd56792706e49bf8b2a6b4bae1462cdfc07748cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14749bb864d45f29e2149d99f34ff6570172ac1d7063f57995047a9665a88429bd0c1f8df352cbde8a534c39e95511464f7a05b4c8da427997723024febe1362
|
7
|
+
data.tar.gz: 4d587a7ebed997339cb32c668d4e1074db764fa78387a1c4e27c2ed26a9d1141f1d89740b423baa418f028df72f0a169b9961c002c83c8c6bb574e628813b38d
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# evanescent
|
2
|
+
|
3
|
+
[](https://travis-ci.org/fornellas/evanescent)
|
4
|
+
[](http://badge.fury.io/rb/evanescent)
|
5
|
+
[](https://github.com/fornellas/evanescent/issues)
|
6
|
+
[](https://raw.githubusercontent.com/fornellas/evanescent/master/LICENSE)
|
7
|
+
[](https://rubygems.org/gems/evanescent)
|
8
|
+
|
9
|
+
* Home: [https://github.com/fornellas/evanescent/](https://github.com/fornellas/evanescent/)
|
10
|
+
* Bugs: [https://github.com/fornellas/evanescent/issues](https://github.com/fornellas/evanescent/issues)
|
11
|
+
|
12
|
+
## Description
|
13
|
+
|
14
|
+
This gem provides an IO like object, that can be used with any logging class (such as Ruby's native Logger). This object will save its input to a file, and allows:
|
15
|
+
|
16
|
+
* Hourly or daily rotation.
|
17
|
+
* Compression of rotated files.
|
18
|
+
* Removal of old compressed files.
|
19
|
+
|
20
|
+
This functionality supplement logging classes, allowing everything related to logging management, to be done within Ruby, without relying on external tools (such as logrotate).
|
21
|
+
|
22
|
+
## Install
|
23
|
+
|
24
|
+
gem install evanescent
|
25
|
+
|
26
|
+
This gem uses [Semantic Versioning](http://semver.org/), so you should add to your .gemspec something like:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
s.add_runtime_dependency 'evanescent', '~> 1.0'
|
30
|
+
```
|
31
|
+
|
32
|
+
Please, always check latest [available](https://rubygems.org/gems/evanescent) version!
|
33
|
+
|
34
|
+
## Example
|
35
|
+
|
36
|
+
### Logger
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'evanescent'
|
40
|
+
require 'timecop'
|
41
|
+
|
42
|
+
logger = Evanescent.logger(
|
43
|
+
path: 'test.log',
|
44
|
+
rotation: :hourly,
|
45
|
+
keep: '2 hours',
|
46
|
+
)
|
47
|
+
|
48
|
+
logger.class # => Logger
|
49
|
+
|
50
|
+
# Within first hour, only test.log will exist.
|
51
|
+
Timecop.freeze(Time.now)
|
52
|
+
logger.info 'first message'
|
53
|
+
Dir.entries('.') # => [".", "..", "test.log"]
|
54
|
+
|
55
|
+
# One hour later, rotation and compression will happen.
|
56
|
+
Timecop.freeze(Time.now + 3600)
|
57
|
+
logger.info 'second message'
|
58
|
+
Dir.entries('.') # => [".", "..", "test.log", "test.log.2015122315.gz"]
|
59
|
+
|
60
|
+
# Another hour later, we'll have 2 compressed files.
|
61
|
+
Timecop.freeze(Time.now + 3600)
|
62
|
+
logger.info 'third message'
|
63
|
+
Dir.entries('.') # => [".", "..", "test.log", "test.log.2015122315.gz", "test.log.2015122316.gz"]
|
64
|
+
|
65
|
+
# At last, after keep period, old compressed files are purged.
|
66
|
+
Timecop.freeze(Time.now + 3600)
|
67
|
+
logger.info 'fourth message'
|
68
|
+
Dir.entries('.') # => [".", "..", "test.log", "test.log.2015122316.gz", "test.log.2015122317.gz"]
|
69
|
+
```
|
70
|
+
|
71
|
+
### Generic usage
|
72
|
+
|
73
|
+
Evanescent is an IO like object: it responds to <tt>:write</tt> and <tt>:close</tt>:
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
io = Evanescent.new(
|
77
|
+
path: 'test.log',
|
78
|
+
rotation: :hourly,
|
79
|
+
keep: '2 hours',
|
80
|
+
)
|
81
|
+
io.write('message') # writes message to test.log
|
82
|
+
io.close
|
83
|
+
```
|
84
|
+
|
85
|
+
## Limitations
|
86
|
+
|
87
|
+
Although Evanescent supports mult-thread operation, inter-process locking is not currently implemented, and behavior is unpredicted in this situation.
|
data/lib/evanescent.rb
CHANGED
@@ -3,10 +3,12 @@ require 'fileutils'
|
|
3
3
|
require 'zlib'
|
4
4
|
|
5
5
|
# IO like object, that can be used with any logging class (such as Ruby's native Logger). This object will save its input to a file, and allows:
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
#
|
6
|
+
#
|
7
|
+
# * Hourly or daily rotation.
|
8
|
+
# * Compression of rotated files.
|
9
|
+
# * Removal of old compressed files.
|
10
|
+
#
|
11
|
+
# This functionality supplement logging classes, allowing everything related to logging management, to be done within Ruby, without relying on external tools (such as logrotate).
|
10
12
|
class Evanescent
|
11
13
|
# Current path being written to.
|
12
14
|
attr_reader :path
|
@@ -17,16 +19,27 @@ class Evanescent
|
|
17
19
|
# How long rotated files are kept (in seconds).
|
18
20
|
attr_reader :keep
|
19
21
|
|
22
|
+
# Shortcut for: <tt>Logger.new(Evanescent.new(opts))</tt>.
|
23
|
+
# Requires logger if needed.
|
24
|
+
def self.logger opts
|
25
|
+
unless Object.const_defined? :Logger
|
26
|
+
require 'logger'
|
27
|
+
end
|
28
|
+
Logger.new(
|
29
|
+
self.new(opts)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
20
33
|
# Must receive a Hash with:
|
21
34
|
# +:path+:: Path where to write to.
|
22
35
|
# +:rotation+:: Either +:hourly+ or +:daily+.
|
23
|
-
# +:keep+:: For how long to keep rotated files. It is parsed with
|
36
|
+
# +:keep+:: For how long to keep rotated files. It is parsed with chronic_duration Gem natural language features. Examples: '1 day', '1 month'.
|
24
37
|
def initialize opts
|
25
38
|
@path = opts[:path]
|
26
39
|
@rotation = opts[:rotation]
|
27
40
|
@keep = ChronicDuration.parse(opts[:keep])
|
28
41
|
@mutex = Mutex.new
|
29
|
-
@last_prefix =
|
42
|
+
@last_prefix = make_suffix(Time.now)
|
30
43
|
@io = nil
|
31
44
|
@compress_thread = nil
|
32
45
|
end
|
@@ -34,11 +47,22 @@ class Evanescent
|
|
34
47
|
# Writes to #path and rotate, compress and purge if necessary.
|
35
48
|
def write string
|
36
49
|
@mutex.synchronize do
|
37
|
-
|
38
|
-
|
39
|
-
|
50
|
+
if new_path = rotation_path
|
51
|
+
# All methods here must have exceptions threated. See:
|
52
|
+
# https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/logger.rb#L647
|
53
|
+
purge
|
54
|
+
mv_path(new_path)
|
55
|
+
compress
|
56
|
+
end
|
40
57
|
open_io
|
41
|
-
@io
|
58
|
+
if @io
|
59
|
+
# No exceptions threated here, they should be handled by caller. See:
|
60
|
+
# https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/logger.rb#L653
|
61
|
+
@io.write(string)
|
62
|
+
else
|
63
|
+
warn("Unable to log: '#{path}' not open!")
|
64
|
+
0
|
65
|
+
end
|
42
66
|
end
|
43
67
|
end
|
44
68
|
|
@@ -49,13 +73,13 @@ class Evanescent
|
|
49
73
|
end
|
50
74
|
end
|
51
75
|
|
52
|
-
# Compression is done in a separate thread.
|
76
|
+
# Compression is done in a separate thread. This method suspends current thread execution until existing compression thread returns. If no compression thread is running, returns immediately.
|
53
77
|
def wait_compression
|
54
78
|
if @compress_thread
|
55
79
|
begin
|
56
80
|
@compress_thread.join
|
57
81
|
rescue
|
58
|
-
warn("Compression thread failed: #{$!}")
|
82
|
+
warn("Compression thread failed: #{$!} (#{$!.class})")
|
59
83
|
ensure
|
60
84
|
@compress_thread = nil
|
61
85
|
end
|
@@ -64,13 +88,6 @@ class Evanescent
|
|
64
88
|
|
65
89
|
private
|
66
90
|
|
67
|
-
def open_io
|
68
|
-
unless @io
|
69
|
-
@io = File.open(path, File::APPEND | File::CREAT | File::WRONLY)
|
70
|
-
@io.sync = true
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
91
|
PARAMS = {
|
75
92
|
hourly: {
|
76
93
|
strftime: '%Y%m%d%H',
|
@@ -84,42 +101,63 @@ class Evanescent
|
|
84
101
|
}
|
85
102
|
}
|
86
103
|
|
87
|
-
def
|
104
|
+
def make_suffix time
|
88
105
|
time.strftime(PARAMS[rotation][:strftime])
|
89
106
|
end
|
90
107
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
rotate_with_closed_io
|
108
|
+
def open_io
|
109
|
+
unless @io
|
110
|
+
@io = File.open(path, File::APPEND | File::CREAT | File::WRONLY)
|
111
|
+
@io.sync = true
|
96
112
|
end
|
113
|
+
rescue
|
114
|
+
warn("Unable to open '#{path}': #{$!} (#{$!.class})")
|
97
115
|
end
|
98
116
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
117
|
+
# Returns new path for rotation. If no rotation is needed, returns nil.
|
118
|
+
def rotation_path
|
119
|
+
if @io
|
120
|
+
curr_suffix = make_suffix(Time.now)
|
121
|
+
return nil if curr_suffix == @last_prefix
|
122
|
+
# Same as https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/logger.rb#L760
|
123
|
+
begin
|
124
|
+
@io.close
|
125
|
+
rescue
|
126
|
+
warn("Error closing '#{path}': #{$!} (#{$!.class})")
|
127
|
+
end
|
128
|
+
@io = nil
|
129
|
+
@last_prefix = curr_suffix
|
130
|
+
"#{path}.#{curr_suffix}"
|
131
|
+
else
|
132
|
+
return nil unless File.exist?(path)
|
133
|
+
curr_suffix = make_suffix(Time.now+PARAMS[rotation][:interval])
|
134
|
+
rotation_suffix = make_suffix(File.mtime(path) + PARAMS[rotation][:interval])
|
135
|
+
return nil if curr_suffix == rotation_suffix
|
136
|
+
@last_prefix = curr_suffix
|
137
|
+
"#{path}.#{rotation_suffix}"
|
138
|
+
end
|
106
139
|
end
|
107
140
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
141
|
+
def purge
|
142
|
+
Dir.glob("#{path}.#{PARAMS[rotation][:glob]}.gz").each do |compressed|
|
143
|
+
time_extractor = Regexp.new(
|
144
|
+
'^' + Regexp.escape("#{path}.") + "(?<time>.+)" + Regexp.escape(".gz") + '$'
|
145
|
+
)
|
146
|
+
time_string = compressed.match(time_extractor)[:time]
|
147
|
+
compressed_time = Time.strptime(time_string, PARAMS[rotation][:strftime])
|
148
|
+
age = Time.now - compressed_time
|
149
|
+
if age >= keep
|
150
|
+
File.delete(compressed)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
rescue
|
154
|
+
warn("Error purging old files: #{$!} (#{$!.class})")
|
115
155
|
end
|
116
156
|
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
warn("Error renaming '#{path}' to '#{new_path}': #{$!}")
|
122
|
-
end
|
157
|
+
def mv_path new_path
|
158
|
+
FileUtils.mv(path, new_path)
|
159
|
+
rescue
|
160
|
+
warn("Error renaming '#{path}' to '#{new_path}': #{$!} (#{$!.class})")
|
123
161
|
end
|
124
162
|
|
125
163
|
def compress
|
@@ -141,23 +179,7 @@ class Evanescent
|
|
141
179
|
end
|
142
180
|
end
|
143
181
|
rescue
|
144
|
-
warn("Error compressing files: #{$!}")
|
145
|
-
end
|
146
|
-
|
147
|
-
def purge
|
148
|
-
Dir.glob("#{path}.#{PARAMS[rotation][:glob]}.gz").each do |compressed|
|
149
|
-
time_extractor = Regexp.new(
|
150
|
-
'^' + Regexp.escape("#{path}.") + "(?<time>.+)" + Regexp.escape(".gz") + '$'
|
151
|
-
)
|
152
|
-
time_string = compressed.match(time_extractor)[:time]
|
153
|
-
compressed_time = Time.strptime(time_string, PARAMS[rotation][:strftime])
|
154
|
-
age = Time.now - compressed_time
|
155
|
-
if age > keep
|
156
|
-
File.delete(compressed)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
rescue
|
160
|
-
warn("Error purging old files: #{$!}")
|
182
|
+
warn("Error compressing files: #{$!} (#{$!.class})")
|
161
183
|
end
|
162
184
|
|
163
185
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: evanescent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fabio Pugliese Ornellas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chronic_duration
|
@@ -123,13 +123,19 @@ executables: []
|
|
123
123
|
extensions: []
|
124
124
|
extra_rdoc_files: []
|
125
125
|
files:
|
126
|
+
- README.md
|
126
127
|
- lib/evanescent.rb
|
127
128
|
homepage: https://github.com/fornellas/evanescent
|
128
129
|
licenses:
|
129
130
|
- GPLv3
|
130
131
|
metadata: {}
|
131
132
|
post_install_message:
|
132
|
-
rdoc_options:
|
133
|
+
rdoc_options:
|
134
|
+
- "--markup=rdoc"
|
135
|
+
- "--main=README.md"
|
136
|
+
- "--visibility=public"
|
137
|
+
- lib/
|
138
|
+
- README.md
|
133
139
|
require_paths:
|
134
140
|
- lib
|
135
141
|
required_ruby_version: !ruby/object:Gem::Requirement
|