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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -0
  3. data/lib/evanescent.rb +84 -62
  4. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f2393af3e93eeb53ec45a8392fa56b72a35735a5
4
- data.tar.gz: 8c5e60dc35ff8ea1c57a236bc3930965f67ded39
3
+ metadata.gz: 6f14da7d6ca2902263af5f1beb2ed7e5dcf1292b
4
+ data.tar.gz: fd56792706e49bf8b2a6b4bae1462cdfc07748cc
5
5
  SHA512:
6
- metadata.gz: bc1391185e9633931792141d36dbfd28b0f86bb861e0a4fdb9b8471ad2288c8c1dbd7f8c2385cf942248c3d6853ccf8cfb1a34f3ce0bfdcb7d5b5fd9c88b2367
7
- data.tar.gz: f8fac72778804d08224ae46abe08d3fbc046ed5604c09d6936d07164e805e7d1e7c0cec239956fa4794defe5a30873782dd499cef3ba001cf3f42791010dc803
6
+ metadata.gz: 14749bb864d45f29e2149d99f34ff6570172ac1d7063f57995047a9665a88429bd0c1f8df352cbde8a534c39e95511464f7a05b4c8da427997723024febe1362
7
+ data.tar.gz: 4d587a7ebed997339cb32c668d4e1074db764fa78387a1c4e27c2ed26a9d1141f1d89740b423baa418f028df72f0a169b9961c002c83c8c6bb574e628813b38d
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # evanescent
2
+
3
+ [![Build Status](https://travis-ci.org/fornellas/evanescent.svg?branch=master)](https://travis-ci.org/fornellas/evanescent)
4
+ [![Gem Version](https://badge.fury.io/rb/evanescent.svg)](http://badge.fury.io/rb/evanescent)
5
+ [![GitHub issues](https://img.shields.io/github/issues/fornellas/evanescent.svg)](https://github.com/fornellas/evanescent/issues)
6
+ [![GitHub license](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://raw.githubusercontent.com/fornellas/evanescent/master/LICENSE)
7
+ [![Downloads](http://ruby-gem-downloads-badge.herokuapp.com/evanescent?type=total)](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
- #* Rotation by time / date.
7
- #* Compression of old files.
8
- #* Removal of old compressed files.
9
- # Its purpuse is to supplement logging classes, allowing everything related to logging management, to be done within Ruby, without relying on external tools (such as logrotate).
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 ChronicDuration's natural language features. Examples: '1 day', '1 month'.
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 = make_prefix(Time.now)
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
- rotate
38
- compress
39
- purge
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.write(string)
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. Thus method suspends current thread execution until existing compression thread returns. If no compression thread is running, returns immediately.
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 make_prefix time
104
+ def make_suffix time
88
105
  time.strftime(PARAMS[rotation][:strftime])
89
106
  end
90
107
 
91
- def rotate
92
- if @io
93
- rotate_with_open_io
94
- else
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
- def rotate_with_open_io
100
- curr_suffix = make_prefix(Time.now)
101
- return if curr_suffix == @last_prefix
102
- @io.close
103
- @io = nil
104
- do_rotation("#{path}.#{curr_suffix}")
105
- @last_prefix = curr_suffix
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 rotate_with_closed_io
109
- return unless File.exist?(path)
110
- curr_suffix = make_prefix(Time.now+PARAMS[rotation][:interval])
111
- rotation_suffix = make_prefix(File.mtime(path) + PARAMS[rotation][:interval])
112
- return if curr_suffix == rotation_suffix
113
- do_rotation("#{path}.#{rotation_suffix}")
114
- @last_prefix = curr_suffix
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 do_rotation new_path
118
- begin
119
- FileUtils.mv(path, new_path)
120
- rescue
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.3
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-17 00:00:00.000000000 Z
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