evanescent 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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