Skip to content

Commit 390cbcb

Browse files
committed
blocking test
1 parent 3577a25 commit 390cbcb

File tree

2 files changed

+199
-55
lines changed

2 files changed

+199
-55
lines changed

lib/logstash/outputs/file.rb

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,27 @@ class LogStash::Outputs::File < LogStash::Outputs::Base
4242
# Gzip the output stream before writing to disk.
4343
config :gzip, :validate => :boolean, :default => false
4444

45+
# If the event try to create a file outside of the extracted file root
46+
# we will tag the message and save it to @filepath_error.
47+
config :tag_on_failure, :validate => :array, :default => ["_filepath_failure"]
48+
49+
# If the generated path is invalid, the events will be saved
50+
# into this file and inside the defined path.
51+
config :filename_failure, :validate => :string, :default => '_filepath_failures'
52+
4553
public
4654
def register
4755
require "fileutils" # For mkdir_p
4856

4957
workers_not_supported
5058

5159
@files = {}
60+
61+
if interpolated_path?
62+
@file_root = extract_file_root
63+
@failure_path = File.join(@file_root, @filename_failure)
64+
end
65+
5266
now = Time.now
5367
@last_flush_cycle = now
5468
@last_stale_cleanup_cycle = now
@@ -60,23 +74,61 @@ def register
6074
def receive(event)
6175
return unless output?(event)
6276

63-
path = event.sprintf(@path)
64-
fd = open(path)
77+
log_path = generate_filepath(event)
6578

66-
# TODO(sissel): Check if we should rotate the file.
79+
if interpolated_path? && !inside_file_root?(log_path)
80+
tag_as_filepath_failure(event)
81+
log_path = @failure_path
82+
end
6783

68-
if @message_format
69-
output = event.sprintf(@message_format)
70-
else
71-
output = event.to_json
84+
output = format_message(event)
85+
log_event(log_path, output)
86+
end # def receive
87+
88+
def tag_as_filepath_failure(event)
89+
event["tags"] ||= []
90+
@tag_on_failure.each do |tag|
91+
event["tags"] << tags unless event["tags"].include?(tag)
7292
end
93+
end
94+
95+
def inside_file_root?(log_path)
96+
target_file = File.expand_path(log_path)
97+
return target_file.start_with?("#{@file_root.to_s}/")
98+
end
99+
100+
def log_event(log_path, event)
101+
@logger.debug("File, writing event to file.", :filename => log_path)
102+
fd = open(log_path)
73103

74-
fd.write(output)
104+
# TODO(sissel): Check if we should rotate the file.
105+
106+
fd.write(event)
75107
fd.write("\n")
76108

77109
flush(fd)
78110
close_stale_files
79-
end # def receive
111+
end
112+
113+
def generate_filepath(event)
114+
event.sprintf(@path)
115+
end
116+
117+
def interpolated_path?
118+
path =~ /%\{[^}]+\}/
119+
end
120+
121+
def format_message(event)
122+
if @message_format
123+
event.sprintf(@message_format)
124+
else
125+
event.to_json
126+
end
127+
end
128+
def extract_file_root
129+
extracted_path = File.expand_path(path.gsub(/%{.+/, ''))
130+
Pathname.new(extracted_path).expand_path
131+
end
80132

81133
def teardown
82134
@logger.debug("Teardown: closing files")
@@ -85,7 +137,7 @@ def teardown
85137
fd.close
86138
@logger.debug("Closed file #{path}", :fd => fd)
87139
rescue Exception => e
88-
@logger.error("Excpetion while flushing and closing files.", :exception => e)
140+
@logger.error("Exception while flushing and closing files.", :exception => e)
89141
end
90142
end
91143
finished

spec/outputs/file_spec.rb

Lines changed: 137 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1+
# encoding: UTF-8
12
require "spec_helper"
23
require "logstash/outputs/file"
4+
require "logstash/event"
35
require "logstash/json"
4-
require "tempfile"
6+
require "stud/temporary"
57

68
describe LogStash::Outputs::File do
7-
8-
99
describe "ship lots of events to a file" do
10-
event_count = 10000 + rand(500)
11-
tmp_file = Tempfile.new('logstash-spec-output-file')
10+
Stud::Temporary.file('logstash-spec-output-file') do |tmp_file|
11+
event_count = 10000 + rand(500)
1212

13-
config <<-CONFIG
13+
config <<-CONFIG
1414
input {
1515
generator {
1616
message => "hello world"
@@ -23,51 +23,143 @@
2323
path => "#{tmp_file.path}"
2424
}
2525
}
26-
CONFIG
27-
28-
agent do
29-
line_num = 0
30-
# Now check all events for order and correctness.
31-
File.foreach(tmp_file) do |line|
32-
event = LogStash::Event.new(LogStash::Json.load(line))
33-
insist {event["message"]} == "hello world"
34-
insist {event["sequence"]} == line_num
35-
line_num += 1
36-
end
37-
insist {line_num} == event_count
38-
end # agent
26+
CONFIG
27+
28+
agent do
29+
line_num = 0
30+
# Now check all events for order and correctness.
31+
tmp_file.each_line do |line|
32+
# File.foreach(tmp_file) do |line|
33+
event = LogStash::Event.new(LogStash::Json.load(line))
34+
insist {event["message"]} == "hello world"
35+
insist {event["sequence"]} == line_num
36+
line_num += 1
37+
end
38+
insist {line_num} == event_count
39+
end # agent
40+
end
3941
end
4042

4143
describe "ship lots of events to a file gzipped" do
42-
event_count = 10000 + rand(500)
43-
tmp_file = Tempfile.new('logstash-spec-output-file')
44+
Stud::Temporary.file('logstash-spec-output-file') do |tmp_file|
45+
event_count = 10000 + rand(500)
4446

45-
config <<-CONFIG
46-
input {
47-
generator {
48-
message => "hello world"
49-
count => #{event_count}
50-
type => "generator"
47+
config <<-CONFIG
48+
input {
49+
generator {
50+
message => "hello world"
51+
count => #{event_count}
52+
type => "generator"
53+
}
5154
}
52-
}
53-
output {
54-
file {
55-
path => "#{tmp_file.path}"
56-
gzip => true
55+
output {
56+
file {
57+
path => "#{tmp_file.path}"
58+
gzip => true
59+
}
5760
}
58-
}
59-
CONFIG
60-
61-
agent do
62-
line_num = 0
63-
# Now check all events for order and correctness.
64-
Zlib::GzipReader.open(tmp_file.path).each_line do |line|
65-
event = LogStash::Event.new(LogStash::Json.load(line))
66-
insist {event["message"]} == "hello world"
67-
insist {event["sequence"]} == line_num
68-
line_num += 1
61+
CONFIG
62+
63+
agent do
64+
line_num = 0
65+
# Now check all events for order and correctness.
66+
Zlib::GzipReader.open(tmp_file.path).each_line do |line|
67+
event = LogStash::Event.new(LogStash::Json.load(line))
68+
insist {event["message"]} == "hello world"
69+
insist {event["sequence"]} == line_num
70+
line_num += 1
71+
end
72+
insist {line_num} == event_count
73+
end # agent
74+
end
75+
end
76+
77+
describe "receiving events" do
78+
context "when using an interpolated path" do
79+
it 'permits to write inside the file root of the defined path' do
80+
event = LogStash::Event.new('@metadata' => { "name" => 'application', 'error' => )
6981
end
70-
insist {line_num} == event_count
71-
end # agent
82+
end
7283
end
84+
85+
# describe '#generate_filepath' do
86+
# let(:event) do
87+
# event = LogStash::Event.new
88+
89+
# event["name"] = "name"
90+
# event["type"] = "awesome"
91+
92+
# event
93+
# end
94+
95+
# it 'uses the event data to generated the path' do
96+
# path = '/tmp/%{type}/%{name}'
97+
98+
# output = LogStash::Outputs::File.new({ "path" => path })
99+
100+
# expect(output.generate_filepath(event)).to eq('/tmp/awesome/name')
101+
# end
102+
103+
# it 'ignores relative path' do
104+
# path = '/tmp/%{type}/%{name}/%{relative}/'
105+
# event[:relative] = '../aaa/'
106+
107+
# output = LogStash::Outputs::File.new({ "path" => path })
108+
109+
# expect(output.generate_filepath(event)).to eq('/tmp/awesome/name/relative')
110+
# end
111+
# end
112+
113+
# describe '#extract_file_root' do
114+
# context 'with interpolated strings in the path' do
115+
# it 'extracts the file root from the default path' do
116+
# path = '/tmp/%{type}/%{name}.txt'
117+
118+
# output = LogStash::Outputs::File.new({ "path" => path })
119+
# expect(output.extract_file_root().to_s).to eq('/tmp')
120+
# end
121+
122+
# it 'extracts to the file root down to the last concrete directory' do
123+
# path = '/tmp/down/%{type}/%{name}.txt'
124+
125+
# output = LogStash::Outputs::File.new({ "path" => path })
126+
# expect(output.extract_file_root.to_s).to eq('/tmp/down')
127+
# end
128+
# end
129+
130+
# context "without interpolated strings" do
131+
# it 'extracts the full path as the file root' do
132+
# path = '/tmp/down/log.txt'
133+
134+
# output = LogStash::Outputs::File.new({ "path" => path })
135+
# expect(output.extract_file_root.to_s).to eq(path)
136+
# end
137+
# end
138+
# end
139+
140+
# describe '#inside_file_root?' do
141+
# context 'when we follow relative paths' do
142+
# let(:path) { '/tmp/%{type}/%{name}.txt' }
143+
144+
# it 'returns false if the target file is outside the file root' do
145+
# output = LogStash::Outputs::File.new({ 'path' => path })
146+
# output.register
147+
# expect(output.inside_file_root?('/tmp/../etc/eviluser/2004.txt')).to eq(false)
148+
# end
149+
150+
# it 'returns true if the target file is inside the file root' do
151+
# output = LogStash::Outputs::File.new({ 'path' => path })
152+
# output.register
153+
# expect(output.inside_file_root?('/tmp/not/../etc/eviluser/2004.txt')).to eq(true)
154+
# end
155+
156+
# it 'returns true if the target file is inside the file root' do
157+
# Stud::Temporary.file('logstash-spec-output-file') do |tmp_file|
158+
# output = LogStash::Outputs::File.new({ 'path' => tmp_file.path })
159+
# output.register
160+
# expect(output.inside_file_root?(tmp_file.path)).to eq(true)
161+
# end
162+
# end
163+
# end
164+
# end
73165
end

0 commit comments

Comments
 (0)