tailf2norikra 0.0.1

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