tailf2norikra 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4379541d238cda27ebe548abe19ec81cd91ac986
4
+ data.tar.gz: 1ac994db1dcb5c200ee198692cfa2c2aa9624213
5
+ SHA512:
6
+ metadata.gz: 18eb2bdc2bcec3d8d33daf0d9ceb37b4d7dc375346e8aba993aabdf9986eae895f8fd00f12a4326797e875813e34264e1f83b77d84d7d5a09e03fa73dbd8aafc
7
+ data.tar.gz: 09880fad9565d47a913df0d19d1472ac4f8b43d592c575f0c090a647dc49f77e741690981d70b93b214017fe7a4559228bcbe9c10d32f5be3a1e17a820b2de49
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015 Supersonic LTD
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Tailf2Norikra
2
+
3
+ Watch and tail files in dirs with specified filename time based patterns and send them to norikra.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'tailf2norikra'
11
+
12
+ And then execute:
13
+
14
+ $ bundle install
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install tailf2norikra
19
+
20
+ ## Usage
21
+
22
+ $ tailf2norikra -h
23
+ Usage: tailf2norikra [options]
24
+ --config PATH Path to settings config
25
+ -h, --help Display this screen
26
+ $
27
+
28
+ ## Config
29
+
30
+ tailf:
31
+ files:
32
+ - target: delivery
33
+ prefix: /var/log/app/events
34
+ suffix: ''
35
+ time_pattern: ".%Y-%m-%d"
36
+ max_batch_lines: 48
37
+ timestamp_field: time
38
+ prune_events_older_than: 10
39
+ position_file: "/var/lib/app/tail2norikra.offsets"
40
+ flush_interval: 1
41
+ max_batch_lines: 1024
42
+ from_begining: false
43
+ delete_old_tailed_files: true
44
+ norikra:
45
+ host: localhost
46
+ port: 6666
47
+ send: true
48
+
49
+ * norikra.host - Norkra server
50
+ * norikra.port - Norikra port
51
+ * tailf.position_file - file where to save tailed files offsets which were sent to norikra
52
+ * tailf.flush_interval - how often in seconds to save the offsets to a file
53
+ * tailf.max_batch_lines - max number of lines to batch in each send request
54
+ * tailf.from_beggining - in case of a new file added to tailing , if to start tailing from beggining or end of the file
55
+ * tailf.delete_old_tailed_files - if to delete files once their time_pattern does not match the current time window and if they have been fully sent to norikra
56
+ * tailf.files - array of file configs for tail, each tailed file configs consists of:
57
+ * target - which target to send the events to
58
+ * prefix - the files prefix to watch for
59
+ * time_pattern - ruby time pattern of files to tail
60
+ * suffix - optional suffix of files to watch for
61
+ so the tool will watch for files that match - prefix + time_pattern + suffix
62
+ * max_batch_lines - max number of lines to batch in each send request just for this specific file pattern
63
+ * timestamp field: field which comatains timestamp/date
64
+ * prune_events_older_than: events with timestamp older than specified number of seconds will be ignored
65
+
66
+
67
+ ## Features/Facts
68
+
69
+ * The config is validated by [schash](https://github.com/ryotarai/schash) gem
70
+ * Tailed files are watched for changes by [rb-notify](https://github.com/nex3/rb-inotify) gem
71
+ * Dirnames of all files prefixes are watched for new files creation or files moved to the dir and are automaticaly
72
+ added to tailing.
73
+ * As well dirnames are watched for deletion or files being moved out of directory, and they are removed from the list of files watched for changing.
74
+ * Based time_pattern, files are periodicaly autodeleted , thus avoiding need for log rotation tools.
75
+ * Files are matched by converting time_pattern to a regexp
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
84
+ 6. Go to 1
data/bin/tailf2norikra ADDED
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'norikra-client'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'hash_symbolizer'
7
+ require 'schash'
8
+ require 'rb-inotify'
9
+ require 'timers'
10
+ require 'socket'
11
+ require 'fileutils'
12
+ require 'logger'
13
+ require 'mixlib/shellout'
14
+ require 'optparse'
15
+
16
+ $stdout.sync = true
17
+
18
+ Thread.abort_on_exception = true
19
+
20
+ @config = nil
21
+
22
+ loglevels = {
23
+ :debug => Logger::DEBUG,
24
+ :info => Logger::INFO,
25
+ :warn => Logger::WARN,
26
+ :error => Logger::Error,
27
+ :fatal => Logger::FATAL,
28
+ :unknown => Logger::UNKNOWN
29
+ }
30
+
31
+ @loglevel = Logger::INFO
32
+
33
+ opts = OptionParser.new
34
+ opts.banner = "Usage: #{$0} [options]"
35
+ opts.on( '--config PATH', String, 'Path to settings config' ) { |c| @config = c }
36
+ opts.on( '--log-level [LEVEL]', [:debug, :info, :warn, :error, :fatal, :unknown] ) { |l| @loglevel = loglevels[l] }
37
+ opts.on( '-h', '--help', 'Display this screen' ) { puts opts; exit 0 }
38
+ opts.parse!
39
+
40
+ unless @config
41
+ puts opts
42
+ exit 1
43
+ end
44
+
45
+ @logger = Logger.new(STDOUT)
46
+
47
+ @settings = YAML.load_file(@config).symbolize_keys(true)
48
+
49
+ validator = Schash::Validator.new do
50
+ {
51
+ tailf: {
52
+ files: array_of({
53
+ target: string,
54
+ prefix: string,
55
+ suffix: optional(string),
56
+ time_pattern: string,
57
+ timestamp_field: optional(string),
58
+ prune_events_older_than: optional(integer),
59
+ max_batch_lines: optional(integer)
60
+ }),
61
+ position_file: string,
62
+ flush_interval: integer,
63
+ max_batch_lines: optional(integer),
64
+ from_begining: boolean,
65
+ delete_old_tailed_files: optional(boolean),
66
+ post_delete_command: optional(string),
67
+ },
68
+ norikra: {
69
+ host: string,
70
+ port: integer
71
+ }
72
+ }
73
+ end
74
+
75
+ unless validator.validate(@settings).empty?
76
+ @logger.error("ERROR: bad settings")
77
+ @logger.error(validator.validate(@settings))
78
+ exit 1
79
+ end
80
+
81
+ @settings[:tailf][:files] = @settings[:tailf][:files].map{|h| h.symbolize_keys(true)}
82
+
83
+ @mutex = Mutex.new
84
+
85
+ @create_notifier = INotify::Notifier.new
86
+ @delete_notifier = INotify::Notifier.new
87
+ @tailf_notifier = INotify::Notifier.new
88
+
89
+ @dirs = {}
90
+ @files = {}
91
+ @threads = {}
92
+ @position_file = @settings[:tailf][:position_file]
93
+ @flush_interval = @settings[:tailf][:flush_interval]
94
+ @max_batch_lines = @settings[:tailf].has_key?(:max_batch_lines) ? @settings[:tailf][:max_batch_lines] : 1024
95
+ @from_begining = @settings[:tailf][:from_begining]
96
+ @delete_old_tailed_files = @settings[:tailf].has_key?(:delete_old_tailed_files) ? @settings[:tailf][:delete_old_tailed_files] : false
97
+ @norikra_host = @settings[:norikra][:host]
98
+ @norikra_port = @settings[:norikra][:port]
99
+ @send = @settings[:norikra].has_key?(:send) ? @settings[:norikra][:send] : true
100
+
101
+ def write_position_file
102
+ @mutex.synchronize do
103
+ File.open(@position_file, 'w') do |file|
104
+ @files.each do |path, attrs|
105
+ file.puts "#{path} #{attrs[:pattern]} #{attrs[:target]} #{attrs[:inode]} #{attrs[:offset]}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def load_position_file
112
+ if File.exist?(@position_file)
113
+ IO.readlines(@position_file).each do |line|
114
+ path, pattern, target, inode, offset = line.split(' ')
115
+ #Load state only for that exist with same inode and were not truncated/rewinded.
116
+ if File.exists?(path) and File.stat(path).ino == inode.to_i and File.stat(path).size >= offset.to_i
117
+ @files[path] = { :pattern => pattern, :target => target, :inode => inode.to_i, :offset => offset.to_i }
118
+ end
119
+ end
120
+ end
121
+ write_position_file
122
+ end
123
+
124
+ load_position_file
125
+
126
+ @norikra = Norikra::Client.new(@norikra_host, @norikra_port)
127
+
128
+ @events_queue = SizedQueue.new(10)
129
+
130
+ @sender_thread = Thread.new do
131
+ loop do
132
+ batch = @events_queue.pop
133
+ begin
134
+ @norikra.send(batch[:target], batch[:events]) if @send
135
+ rescue Errno::ECONNREFUSED
136
+ @logger.warn("Connection refused to norikra server, retrying in 1 second ...")
137
+ sleep 1
138
+ retry
139
+ rescue Norikra::RPC::ServiceUnavailableError
140
+ @logger.warn("Got Norikra::RPC::ServiceUnavailableError while trying to senv events to norikra, retrying in 1 second ...")
141
+ sleep 1
142
+ retry
143
+ end
144
+ @files[batch[:path]][:offset] = batch[:offset]
145
+ end
146
+ end
147
+
148
+ def norikra_send(path, buffer, offset)
149
+ prune_old_events = @files[path].has_key?(:timestamp_field) and @files[path].has_key?(:prune_events_older_than)
150
+ events = []
151
+ while event = buffer.shift
152
+ begin
153
+ event = JSON.parse(event)
154
+ if prune_old_events
155
+ unless event.has_key?(@files[path][:timestamp_field])
156
+ @logger.warn("Ignoring event without timestamp field #{@files[path][:timestamp_field]} #{event}")
157
+ else
158
+ if Time.now.to_i - event[@files[path][:timestamp_field]] > @files[path][:prune_events_older_than]
159
+ @logger.debug("Ignoring old event #{event}")
160
+ else
161
+ events << event
162
+ end
163
+ end
164
+ end
165
+ rescue => e
166
+ @logger.warn("Warning: Got bad json event #{event} #{e.message}")
167
+ end
168
+ end
169
+ @events_queue.push({ :path => path, :target => @files[path][:target], :events => events, :offset => offset})
170
+ end
171
+
172
+ def tailf(path)
173
+ file = File.open(path, 'r')
174
+ @files[path][:fd] = file
175
+ file.seek(@files[path][:offset], IO::SEEK_SET)
176
+
177
+ loop do #Fast read file in batches until we reach EOF upon which we start the tailf modify watcher
178
+ batch = file.each_line.take(@files[path][:max_batch_lines])
179
+ break if batch.empty?
180
+ norikra_send(path, batch, file.pos)
181
+ end
182
+
183
+ mutex = Mutex.new
184
+ @tailf_notifier.watch(path, :modify) do |event|
185
+ mutex.synchronize do
186
+ unless file.closed?
187
+ loop do
188
+ batch = file.each_line.take(@files[path][:max_batch_lines])
189
+ break if batch.empty?
190
+ norikra_send(path, batch, file.pos)
191
+ end
192
+ else
193
+ @logger.warn("watcher got modify event on closed file #{event.name}")
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ @time_regexp_hash = {
200
+ 'Y' => '[0-9]{4}',
201
+ 'm' => '[0-9]{2}',
202
+ 'd' => '[0-9]{2}',
203
+ 'H' => '[0-9]{2}',
204
+ 'M' => '[0-9]{2}'
205
+ }
206
+
207
+ def time_pattern_to_regexp(pattern)
208
+ pattern.gsub(/%([^%])/) do
209
+ match = $1
210
+ @time_regexp_hash.has_key?(match) ? @time_regexp_hash[match] : match
211
+ end
212
+ end
213
+
214
+ #Scan existing files that match watched prefixes and start failing them
215
+ @settings[:tailf][:files].each do |tailf_file|
216
+ tailf_file[:prefix] = File.expand_path(tailf_file[:prefix])
217
+ dir = File.dirname(tailf_file[:prefix])
218
+ if File.exists?(dir) and File.directory?(dir)
219
+ dir_pattern_config = { :prefix => File.basename(tailf_file[:prefix]), :pattern => tailf_file[:time_pattern], :suffix => "#{tailf_file[:suffix]}", :targer => tailf_file[:target] }
220
+ dir_pattern_config[:timestamp_field] = tailf_file[:timestamp_field] if tailf_file.has_key?(:timestamp_field)
221
+ dir_pattern_config[:prune_events_older_than] = tailf_file[:prune_events_older_than] if tailf_file.has_key?(:prune_events_older_than)
222
+ dir_pattern_config[:max_batch_lines] = tailf_file.has_key?(:max_batch_lines) ? tailf_file[:max_batch_lines] : @max_batch_lines
223
+ @dirs[dir] ||= []
224
+ @dirs[dir] << dir_pattern_config
225
+ Dir.glob("#{tailf_file[:prefix]}*#{tailf_file[:suffix]}").each do |path|
226
+ if path.match(Regexp.new(time_pattern_to_regexp(tailf_file[:time_pattern])))
227
+ unless File.directory?(path)
228
+ #Populate state only if it was not loaded from position file
229
+ unless @files.has_key?(path)
230
+ @files[path] = { :pattern => tailf_file[:time_pattern], :target => tailf_file[:target], :inode => File.stat(path).ino, :offset => 0 }
231
+ @files[path][:offset] = File.stat(path).size unless @from_begining
232
+ end
233
+ @files[path][:max_batch_lines] = dir_pattern_config[:max_batch_lines]
234
+ if tailf_file.has_key?(:timestamp_field) and tailf_file.has_key?(:prune_events_older_than)
235
+ @files[path][:timestamp_field] = tailf_file[:timestamp_field]
236
+ @files[path][:prune_events_older_than] = tailf_file[:prune_events_older_than]
237
+ end
238
+ @threads[path] = Thread.new { tailf(path) } unless @threads.has_key?(path)
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ def delete_old_tailed_files
246
+ @mutex.synchronize do
247
+ @files.each_key do |path|
248
+ unless path.match(Regexp.new(Time.now.strftime(@files[path][:pattern])))
249
+ if File.exists?(path) and File.stat(path).ino == @files[path][:inode] and File.stat(path).size == @files[path][:offset] and (Time.now - File.stat(path).mtime) > 30
250
+ @logger.info("Deleteing old time pattern fully kafka produced file #{path}")
251
+ FileUtils.rm_r(path)
252
+ if @settings[:tailf].has_key?(:post_delete_command)
253
+ @logger.info("Running post delete command => #{@settings[:tailf][:post_delete_command]}")
254
+ command = Mixlib::ShellOut.new(@settings[:tailf][:post_delete_command])
255
+ begin
256
+ command.run_command
257
+ if command.error?
258
+ @logger.error("Failed post delete command => #{@settings[:tailf][:post_delete_command]}")
259
+ @logger.info("STDOUT: #{command.stdout}")
260
+ @logger.info("STDERR: #{command.stderr}")
261
+ end
262
+ rescue => e
263
+ @logger.error("Failed post delete command => #{@settings[:tailf][:post_delete_command]}")
264
+ @logger.info(e.message)
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ @timers = Timers::Group.new
274
+ @uploads_timer = @timers.every(@flush_interval) { write_position_file }
275
+ @delete_old_tailed_files_timer = @timers.every(60) { delete_old_tailed_files } if @delete_old_tailed_files
276
+ Thread.new { loop { @timers.wait } }
277
+
278
+ @dirs.each_key do |dir|
279
+
280
+ @create_notifier.watch(dir, :create, :moved_to) do |event|
281
+ @mutex.synchronize do
282
+ path = "#{dir}/#{event.name}"
283
+ match = @dirs[dir].detect{|h| event.name.match(Regexp.new(h[:prefix] + time_pattern_to_regexp(h[:pattern]) + h[:suffix]))}
284
+ if match
285
+ unless File.directory?(path)
286
+ unless @threads.has_key?(path)
287
+ @logger.info("File #{event.name} was created in / moved into watched dir #{dir}")
288
+ @files[path] = { :pattern => match[:pattern], :target => match[:target], :inode => File.stat(path).ino, :offset => 0, :max_batch_lines => match[:max_batch_lines] }
289
+ if match.has_key?(:timestamp_field) and match.has_key?(:prune_events_older_than)
290
+ @files[path][:timestamp_field] = match[:timestamp_field]
291
+ @files[path][:prune_events_older_than] = match[:prune_events_older_than]
292
+ end
293
+ @threads[path] = Thread.new { tailf(path) }
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ @delete_notifier.watch(dir, :delete, :moved_from) do |event|
301
+ @mutex.synchronize do
302
+ path = "#{dir}/#{event.name}"
303
+ if @threads.has_key?(path)
304
+ @logger.info("File #{event.name} was deleted / moved from watched dir #{dir}")
305
+ if @threads[path].alive?
306
+ @threads[path].terminate
307
+ @threads[path].join
308
+ end
309
+ @threads.delete(path)
310
+ @files[path][:fd].close unless @files[path][:fd].closed?
311
+ @files.delete(path)
312
+ end
313
+ end
314
+ end
315
+
316
+ end
317
+
318
+ Thread.new { @create_notifier.run }
319
+ Thread.new { @delete_notifier.run }
320
+
321
+ @tailf_notifier.run
@@ -0,0 +1,3 @@
1
+ module Tailf2Norikra
2
+ require 'tailf2norikra/version.rb'
3
+ end
@@ -0,0 +1,3 @@
1
+ module Tailf2Norikra
2
+ VERSION ||= '0.0.1'
3
+ end
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require "tailf2norikra/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "tailf2norikra"
8
+ s.version = Tailf2Norikra::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Alexander Piavlo"]
11
+ s.email = ["devops@supersonic.com"]
12
+ s.homepage = "http://github.com/SupersonicAds/tailf2norikra"
13
+ s.summary = "Watch and tail files with specified time based patterns and push them to norikra"
14
+ s.description = "Watch and tail files with specified time based patterns and push them to norikra"
15
+ s.license = 'MIT'
16
+ s.has_rdoc = false
17
+
18
+ s.add_dependency('norikra-client')
19
+ s.add_dependency('hash_symbolizer')
20
+ s.add_dependency('schash')
21
+ s.add_dependency('rb-inotify')
22
+ s.add_dependency('timers')
23
+ s.add_dependency('mixlib-shellout')
24
+
25
+ s.add_development_dependency('rake')
26
+
27
+ s.files = Dir.glob("{bin,lib}/**/*") + %w(tailf2norikra.gemspec LICENSE README.md)
28
+ s.executables = Dir.glob('bin/**/*').map { |file| File.basename(file) }
29
+ s.test_files = nil
30
+ s.require_paths = ['lib']
31
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tailf2norikra
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Piavlo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: norikra-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hash_symbolizer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: schash
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rb-inotify
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: timers
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mixlib-shellout
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Watch and tail files with specified time based patterns and push them
112
+ to norikra
113
+ email:
114
+ - devops@supersonic.com
115
+ executables:
116
+ - tailf2norikra
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - LICENSE
121
+ - README.md
122
+ - bin/tailf2norikra
123
+ - lib/tailf2norikra.rb
124
+ - lib/tailf2norikra/version.rb
125
+ - tailf2norikra.gemspec
126
+ homepage: http://github.com/SupersonicAds/tailf2norikra
127
+ licenses:
128
+ - MIT
129
+ metadata: {}
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubyforge_project:
146
+ rubygems_version: 2.2.2
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: Watch and tail files with specified time based patterns and push them to
150
+ norikra
151
+ test_files: []