logstash-output-kusto 2.1.6-java → 2.2.0-java
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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +42 -2
- data/lib/logstash/outputs/kusto/ingestor.rb +314 -13
- data/lib/logstash/outputs/kusto/streaming_chunker.rb +42 -0
- data/lib/logstash/outputs/kusto.rb +373 -41
- data/spec/outputs/kusto/ingestor_spec.rb +480 -0
- data/spec/outputs/kusto/streaming_chunker_spec.rb +57 -0
- data/spec/outputs/kusto_spec.rb +391 -0
- data/version +1 -1
- metadata +6 -3
- /data/vendor/jar-dependencies/org/logstash/outputs/logstash-output-kusto/{2.1.6/logstash-output-kusto-2.1.6.jar → 2.2.0/logstash-output-kusto-2.2.0.jar} +0 -0
|
@@ -6,6 +6,7 @@ require 'logstash/errors'
|
|
|
6
6
|
|
|
7
7
|
require 'logstash/outputs/kusto/ingestor'
|
|
8
8
|
require 'logstash/outputs/kusto/interval'
|
|
9
|
+
require 'logstash/outputs/kusto/streaming_chunker'
|
|
9
10
|
|
|
10
11
|
##
|
|
11
12
|
# This plugin sends messages to Azure Kusto in batches.
|
|
@@ -28,7 +29,7 @@ class LogStash::Outputs::Kusto < LogStash::Outputs::Base
|
|
|
28
29
|
#
|
|
29
30
|
# If you use an absolute path you cannot start with a dynamic string.
|
|
30
31
|
# E.g: `/%{myfield}/`, `/test-%{myfield}/` are not valid paths
|
|
31
|
-
config :path, validate: :string, required:
|
|
32
|
+
config :path, validate: :string, required: false
|
|
32
33
|
|
|
33
34
|
# Flush interval (in seconds) for flushing writes to files.
|
|
34
35
|
# 0 will flush on every message. Increase this value to recude IO calls but keep
|
|
@@ -110,6 +111,26 @@ class LogStash::Outputs::Kusto < LogStash::Outputs::Base
|
|
|
110
111
|
# starts processing them in the main thread (not healthy)
|
|
111
112
|
config :upload_queue_size, validate: :number, default: 30
|
|
112
113
|
|
|
114
|
+
# Queued ingestion is optimized for throughput. Streaming ingestion is
|
|
115
|
+
# optimized for low latency and automatically falls back to queued ingestion.
|
|
116
|
+
config :ingestion_mode, validate: %w[queued streaming], default: 'queued'
|
|
117
|
+
|
|
118
|
+
# Maximum encoded bytes in one streaming request. Events are never split.
|
|
119
|
+
config :streaming_max_request_bytes, validate: :number, default: 1_048_576
|
|
120
|
+
|
|
121
|
+
# Additional retries for transient errors that escape the streaming client.
|
|
122
|
+
config :streaming_max_retry_attempts, validate: :number, default: 2
|
|
123
|
+
|
|
124
|
+
# Initial retry delay. Subsequent streaming retries use exponential backoff.
|
|
125
|
+
config :streaming_retry_backoff_seconds, validate: :number, default: 1
|
|
126
|
+
|
|
127
|
+
# Limit concurrent requests issued by shared Logstash pipeline workers.
|
|
128
|
+
config :streaming_concurrent_requests, validate: :number, default: 4
|
|
129
|
+
|
|
130
|
+
# Local durable spool for streaming requests. A stable default is
|
|
131
|
+
# derived from the Kusto destination so files can be recovered after restart.
|
|
132
|
+
config :streaming_temp_directory, validate: :string, required: false
|
|
133
|
+
|
|
113
134
|
# Host of the proxy , is an optional field. Can connect directly
|
|
114
135
|
config :proxy_host, validate: :string, required: false
|
|
115
136
|
|
|
@@ -132,43 +153,199 @@ class LogStash::Outputs::Kusto < LogStash::Outputs::Base
|
|
|
132
153
|
final_mapping = mapping
|
|
133
154
|
end
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
if ingestion_mode == 'queued'
|
|
157
|
+
raise LogStash::ConfigurationError, 'path is required for queued ingestion.' if path.nil? || path.empty?
|
|
158
|
+
|
|
159
|
+
# TODO: add id to the tmp path to support multiple outputs of the same type.
|
|
160
|
+
# TODO: Fix final_mapping when dynamic routing is supported
|
|
161
|
+
@path = if dynamic_event_routing
|
|
162
|
+
File.expand_path("#{path}.%{[@metadata][database]}.%{[@metadata][table]}.%{[@metadata][final_mapping]}")
|
|
163
|
+
else
|
|
164
|
+
File.expand_path("#{path}.#{database}.#{table}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
validate_path
|
|
168
|
+
|
|
169
|
+
@file_root = if path_with_field_ref?
|
|
170
|
+
extract_file_root
|
|
171
|
+
else
|
|
172
|
+
File.dirname(path)
|
|
173
|
+
end
|
|
174
|
+
@failure_path = File.join(@file_root, @filename_failure)
|
|
175
|
+
else
|
|
176
|
+
validate_streaming_config
|
|
177
|
+
@streaming_chunker = StreamingChunker.new(streaming_max_request_bytes.to_i)
|
|
178
|
+
require 'digest'
|
|
179
|
+
destination_id = Digest::SHA256.hexdigest(
|
|
180
|
+
[ingest_url, database, table, final_mapping].join("\0")
|
|
181
|
+
)[0, 20]
|
|
182
|
+
@streaming_temp_directory = File.expand_path(
|
|
183
|
+
streaming_temp_directory ||
|
|
184
|
+
File.join(
|
|
185
|
+
LogStash::SETTINGS.get('path.data'),
|
|
186
|
+
'plugins',
|
|
187
|
+
'logstash-output-kusto',
|
|
188
|
+
destination_id
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
@streaming_dir_mode = @dir_mode == -1 ? 0o700 : @dir_mode.to_i
|
|
192
|
+
@streaming_file_mode = @file_mode == -1 ? 0o600 : @file_mode.to_i
|
|
193
|
+
validate_streaming_modes
|
|
194
|
+
prepare_streaming_spool_directory
|
|
195
|
+
acquire_streaming_spool_lock
|
|
196
|
+
@streaming_metric = metric.namespace(:streaming)
|
|
197
|
+
%i[
|
|
198
|
+
requests
|
|
199
|
+
events
|
|
200
|
+
bytes
|
|
201
|
+
streamed
|
|
202
|
+
queued_fallback
|
|
203
|
+
pending
|
|
204
|
+
final_non_success
|
|
205
|
+
oversized_events
|
|
206
|
+
failures
|
|
207
|
+
retry_cycles
|
|
208
|
+
].each { |counter| @streaming_metric.increment(counter, 0) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
begin
|
|
212
|
+
executor = Concurrent::ThreadPoolExecutor.new(
|
|
213
|
+
min_threads: 1,
|
|
214
|
+
max_threads: ingestion_mode == 'queued' ? upload_concurrent_count : streaming_concurrent_requests,
|
|
215
|
+
max_queue: upload_queue_size,
|
|
216
|
+
fallback_policy: :caller_runs
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@ingestor = Ingestor.new(
|
|
220
|
+
ingest_url,
|
|
221
|
+
app_id,
|
|
222
|
+
app_key,
|
|
223
|
+
app_tenant,
|
|
224
|
+
managed_identity,
|
|
225
|
+
cli_auth,
|
|
226
|
+
database,
|
|
227
|
+
table,
|
|
228
|
+
final_mapping,
|
|
229
|
+
delete_temp_files,
|
|
230
|
+
proxy_host,
|
|
231
|
+
proxy_port,
|
|
232
|
+
proxy_protocol,
|
|
233
|
+
@logger,
|
|
234
|
+
executor,
|
|
235
|
+
ingestion_mode,
|
|
236
|
+
streaming_max_retry_attempts.to_i,
|
|
237
|
+
streaming_retry_backoff_seconds.to_f,
|
|
238
|
+
nil,
|
|
239
|
+
nil,
|
|
240
|
+
ingestion_mode == 'streaming' ? @streaming_metric : nil
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if recovery
|
|
244
|
+
ingestion_mode == 'queued' ? recover_past_files : recover_streaming_files
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
@last_stale_cleanup_cycle = Time.now
|
|
248
|
+
|
|
249
|
+
@flush_interval = @flush_interval.to_i
|
|
250
|
+
if ingestion_mode == 'queued' && @flush_interval > 0
|
|
251
|
+
@flusher = Interval.start(@flush_interval, -> { flush_pending_files })
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if ingestion_mode == 'queued' && (@stale_cleanup_type == 'interval') && (@stale_cleanup_interval > 0)
|
|
255
|
+
@cleaner = Interval.start(stale_cleanup_interval, -> { close_stale_files })
|
|
256
|
+
end
|
|
257
|
+
rescue
|
|
258
|
+
release_streaming_spool_lock
|
|
259
|
+
raise
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
def validate_streaming_config
|
|
265
|
+
if streaming_max_request_bytes.to_i <= 0
|
|
266
|
+
raise LogStash::ConfigurationError,
|
|
267
|
+
'streaming_max_request_bytes must be greater than zero.'
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
if streaming_max_retry_attempts.to_i < 0
|
|
271
|
+
raise LogStash::ConfigurationError,
|
|
272
|
+
'streaming_max_retry_attempts must be zero or greater.'
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if streaming_retry_backoff_seconds.to_f <= 0
|
|
276
|
+
raise LogStash::ConfigurationError,
|
|
277
|
+
'streaming_retry_backoff_seconds must be greater than zero.'
|
|
278
|
+
end
|
|
143
279
|
|
|
144
|
-
|
|
280
|
+
if streaming_concurrent_requests.to_i <= 0
|
|
281
|
+
raise LogStash::ConfigurationError,
|
|
282
|
+
'streaming_concurrent_requests must be greater than zero.'
|
|
283
|
+
end
|
|
284
|
+
end
|
|
145
285
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
286
|
+
private
|
|
287
|
+
def validate_streaming_modes
|
|
288
|
+
if (@streaming_dir_mode & 0o022).positive?
|
|
289
|
+
raise LogStash::ConfigurationError,
|
|
290
|
+
'dir_mode must not allow group or world writes for streaming.'
|
|
291
|
+
end
|
|
152
292
|
|
|
153
|
-
|
|
154
|
-
max_threads: upload_concurrent_count,
|
|
155
|
-
max_queue: upload_queue_size,
|
|
156
|
-
fallback_policy: :caller_runs)
|
|
293
|
+
return unless (@streaming_file_mode & 0o022).positive?
|
|
157
294
|
|
|
158
|
-
|
|
295
|
+
raise LogStash::ConfigurationError,
|
|
296
|
+
'file_mode must not allow group or world writes for streaming.'
|
|
297
|
+
end
|
|
159
298
|
|
|
160
|
-
|
|
161
|
-
|
|
299
|
+
private
|
|
300
|
+
def prepare_streaming_spool_directory
|
|
301
|
+
if File.symlink?(@streaming_temp_directory)
|
|
302
|
+
raise LogStash::ConfigurationError,
|
|
303
|
+
"Streaming spool directory must not be a symlink: #{@streaming_temp_directory}"
|
|
304
|
+
end
|
|
162
305
|
|
|
163
|
-
@
|
|
306
|
+
FileUtils.mkdir_p(@streaming_temp_directory, mode: @streaming_dir_mode)
|
|
307
|
+
File.chmod(@streaming_dir_mode, @streaming_temp_directory)
|
|
308
|
+
@streaming_temp_directory = File.realpath(@streaming_temp_directory)
|
|
309
|
+
validate_streaming_spool_directory
|
|
310
|
+
validate_streaming_spool_parents unless Gem.win_platform?
|
|
311
|
+
end
|
|
164
312
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
313
|
+
private
|
|
314
|
+
def validate_streaming_spool_directory
|
|
315
|
+
stat = File.lstat(@streaming_temp_directory)
|
|
316
|
+
unless stat.directory? && !stat.symlink?
|
|
317
|
+
raise LogStash::ConfigurationError,
|
|
318
|
+
"Streaming spool path is not a directory: #{@streaming_temp_directory}"
|
|
168
319
|
end
|
|
169
320
|
|
|
170
|
-
if
|
|
171
|
-
|
|
321
|
+
return if Gem.win_platform? || stat.uid == Process.uid
|
|
322
|
+
|
|
323
|
+
raise LogStash::ConfigurationError,
|
|
324
|
+
"Streaming spool directory is not owned by the Logstash user: #{@streaming_temp_directory}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
private
|
|
328
|
+
def validate_streaming_spool_parents
|
|
329
|
+
current = File.dirname(@streaming_temp_directory)
|
|
330
|
+
loop do
|
|
331
|
+
stat = File.lstat(current)
|
|
332
|
+
mode = stat.mode & 0o7777
|
|
333
|
+
owned_by_trusted_user = stat.uid == Process.uid || stat.uid.zero?
|
|
334
|
+
sticky_shared_directory = stat.sticky? && (mode & 0o002).positive?
|
|
335
|
+
|
|
336
|
+
unless owned_by_trusted_user || ((mode & 0o200).zero? && (mode & 0o022).zero?)
|
|
337
|
+
raise LogStash::ConfigurationError,
|
|
338
|
+
"Streaming spool parent is owned by an untrusted user: #{current}"
|
|
339
|
+
end
|
|
340
|
+
if (mode & 0o022).positive? && !sticky_shared_directory
|
|
341
|
+
raise LogStash::ConfigurationError,
|
|
342
|
+
"Streaming spool parent is writable by untrusted users: #{current}"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
parent = File.dirname(current)
|
|
346
|
+
break if parent == current
|
|
347
|
+
|
|
348
|
+
current = parent
|
|
172
349
|
end
|
|
173
350
|
end
|
|
174
351
|
|
|
@@ -198,6 +375,16 @@ class LogStash::Outputs::Kusto < LogStash::Outputs::Base
|
|
|
198
375
|
|
|
199
376
|
public
|
|
200
377
|
def multi_receive_encoded(events_and_encoded)
|
|
378
|
+
if ingestion_mode == 'streaming'
|
|
379
|
+
streaming_files = write_streaming_chunks(
|
|
380
|
+
@streaming_chunker.chunks(events_and_encoded.map(&:last))
|
|
381
|
+
)
|
|
382
|
+
streaming_files.each do |file|
|
|
383
|
+
@ingestor.upload_async(file[:path], delete_temp_files)
|
|
384
|
+
end
|
|
385
|
+
return
|
|
386
|
+
end
|
|
387
|
+
|
|
201
388
|
encoded_by_path = Hash.new { |h, k| h[k] = [] }
|
|
202
389
|
|
|
203
390
|
events_and_encoded.each do |event, encoded|
|
|
@@ -220,22 +407,28 @@ class LogStash::Outputs::Kusto < LogStash::Outputs::Base
|
|
|
220
407
|
def close
|
|
221
408
|
@flusher.stop unless @flusher.nil?
|
|
222
409
|
@cleaner.stop unless @cleaner.nil?
|
|
223
|
-
|
|
224
|
-
@
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
410
|
+
if ingestion_mode == 'queued'
|
|
411
|
+
@io_mutex.synchronize do
|
|
412
|
+
@logger.debug('Close: closing files')
|
|
413
|
+
|
|
414
|
+
@files.each do |path, fd|
|
|
415
|
+
begin
|
|
416
|
+
fd.close
|
|
417
|
+
@logger.debug("Closed file #{path}", fd: fd)
|
|
418
|
+
|
|
419
|
+
kusto_send_file(path)
|
|
420
|
+
rescue Exception => e
|
|
421
|
+
@logger.error('Exception while flushing and closing files.', exception: e)
|
|
422
|
+
end
|
|
234
423
|
end
|
|
235
424
|
end
|
|
236
425
|
end
|
|
237
426
|
|
|
238
|
-
|
|
427
|
+
begin
|
|
428
|
+
@ingestor.stop unless @ingestor.nil?
|
|
429
|
+
ensure
|
|
430
|
+
release_streaming_spool_lock
|
|
431
|
+
end
|
|
239
432
|
end
|
|
240
433
|
|
|
241
434
|
private
|
|
@@ -398,6 +591,145 @@ class LogStash::Outputs::Kusto < LogStash::Outputs::Base
|
|
|
398
591
|
@logger.warn('No such file or directory', exception: e.class, message: e.message, path: new_path, backtrace: e.backtrace)
|
|
399
592
|
end
|
|
400
593
|
end
|
|
594
|
+
|
|
595
|
+
private
|
|
596
|
+
def write_streaming_chunks(chunks)
|
|
597
|
+
require 'securerandom'
|
|
598
|
+
|
|
599
|
+
return [] if chunks.empty?
|
|
600
|
+
|
|
601
|
+
batch_id = SecureRandom.uuid
|
|
602
|
+
temporary_directory = File.join(@streaming_temp_directory, ".batch-#{batch_id}.tmp")
|
|
603
|
+
ready_directory = File.join(@streaming_temp_directory, "batch-#{batch_id}.ready")
|
|
604
|
+
files = []
|
|
605
|
+
committed = false
|
|
606
|
+
Dir.mkdir(temporary_directory, @streaming_dir_mode)
|
|
607
|
+
|
|
608
|
+
chunks.each_with_index do |chunk, index|
|
|
609
|
+
source_id = SecureRandom.uuid
|
|
610
|
+
path = File.join(
|
|
611
|
+
temporary_directory,
|
|
612
|
+
format('stream-%06d-%s.json', index, source_id)
|
|
613
|
+
)
|
|
614
|
+
bytes = chunk.sum(&:bytesize)
|
|
615
|
+
write_streaming_file(path, chunk)
|
|
616
|
+
files << { path: path, bytes: bytes, events: chunk.length }
|
|
617
|
+
end
|
|
618
|
+
fsync_directory(temporary_directory)
|
|
619
|
+
File.rename(temporary_directory, ready_directory)
|
|
620
|
+
committed = true
|
|
621
|
+
fsync_directory(@streaming_temp_directory)
|
|
622
|
+
files.each do |file|
|
|
623
|
+
file[:path] = File.join(ready_directory, File.basename(file[:path]))
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
files.each do |file|
|
|
627
|
+
@streaming_metric.increment(:requests)
|
|
628
|
+
@streaming_metric.increment(:events, file[:events])
|
|
629
|
+
@streaming_metric.increment(:bytes, file[:bytes])
|
|
630
|
+
@streaming_metric.increment(:oversized_events) if
|
|
631
|
+
file[:events] == 1 && file[:bytes] > streaming_max_request_bytes.to_i
|
|
632
|
+
end
|
|
633
|
+
files
|
|
634
|
+
rescue
|
|
635
|
+
FileUtils.rm_rf(temporary_directory) if temporary_directory
|
|
636
|
+
FileUtils.rm_rf(ready_directory) if ready_directory && !committed
|
|
637
|
+
@streaming_metric.increment(:failures)
|
|
638
|
+
raise
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
private
|
|
642
|
+
def write_streaming_file(path, encoded_events)
|
|
643
|
+
flags = File::WRONLY | File::CREAT | File::EXCL
|
|
644
|
+
flags |= File::NOFOLLOW if defined?(File::NOFOLLOW)
|
|
645
|
+
file = File.new(path, flags, @streaming_file_mode)
|
|
646
|
+
begin
|
|
647
|
+
encoded_events.each { |encoded| file.write(encoded) }
|
|
648
|
+
file.flush
|
|
649
|
+
file.fsync
|
|
650
|
+
ensure
|
|
651
|
+
file.close
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
private
|
|
656
|
+
def recover_streaming_files
|
|
657
|
+
Dir.glob(File.join(@streaming_temp_directory, '.batch-*.tmp')).each do |directory|
|
|
658
|
+
@logger.warn('Discarding incomplete streaming spool batch.', path: directory)
|
|
659
|
+
FileUtils.rm_rf(directory)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
Dir.glob(File.join(@streaming_temp_directory, 'batch-*.ready', 'stream-*.json')).sort.each do |file|
|
|
663
|
+
validate_recovered_streaming_file(file)
|
|
664
|
+
@logger.info('Recovering streaming spool file.', path: file)
|
|
665
|
+
@ingestor.upload_async(file, delete_temp_files)
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
private
|
|
670
|
+
def validate_recovered_streaming_file(file)
|
|
671
|
+
batch_directory = File.dirname(file)
|
|
672
|
+
batch_stat = File.lstat(batch_directory)
|
|
673
|
+
file_stat = File.lstat(file)
|
|
674
|
+
safe_batch = batch_stat.directory? && !batch_stat.symlink? &&
|
|
675
|
+
safe_streaming_spool_stat?(batch_stat)
|
|
676
|
+
safe_file = file_stat.file? && !file_stat.symlink? &&
|
|
677
|
+
safe_streaming_spool_stat?(file_stat)
|
|
678
|
+
return if safe_batch && safe_file
|
|
679
|
+
|
|
680
|
+
raise LogStash::ConfigurationError,
|
|
681
|
+
"Streaming recovery rejected an unsafe spool file: #{file}"
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
private
|
|
685
|
+
def safe_streaming_spool_stat?(stat)
|
|
686
|
+
owner_is_safe = Gem.win_platform? || stat.uid == Process.uid
|
|
687
|
+
owner_is_safe && (stat.mode & 0o022).zero?
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
private
|
|
691
|
+
def fsync_directory(directory)
|
|
692
|
+
return if Gem.win_platform?
|
|
693
|
+
|
|
694
|
+
File.open(directory, File::RDONLY) { |file| file.fsync }
|
|
695
|
+
rescue SystemCallError => e
|
|
696
|
+
unsupported_errors = [Errno::EINVAL::Errno]
|
|
697
|
+
unsupported_errors << Errno::EISDIR::Errno if defined?(Errno::EISDIR)
|
|
698
|
+
unsupported_errors << Errno::ENOTSUP::Errno if defined?(Errno::ENOTSUP)
|
|
699
|
+
raise unless unsupported_errors.include?(e.errno)
|
|
700
|
+
|
|
701
|
+
@logger.debug('Directory fsync is not supported on this platform.', path: directory)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
private
|
|
705
|
+
def acquire_streaming_spool_lock
|
|
706
|
+
lock_path = File.join(@streaming_temp_directory, '.lock')
|
|
707
|
+
if File.symlink?(lock_path)
|
|
708
|
+
raise LogStash::ConfigurationError,
|
|
709
|
+
"Streaming spool lock must not be a symlink: #{lock_path}"
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
flags = File::RDWR | File::CREAT
|
|
713
|
+
flags |= File::NOFOLLOW if defined?(File::NOFOLLOW)
|
|
714
|
+
lock_file = File.open(lock_path, flags, @streaming_file_mode)
|
|
715
|
+
File.chmod(@streaming_file_mode, lock_path)
|
|
716
|
+
unless lock_file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
717
|
+
lock_file.close
|
|
718
|
+
raise LogStash::ConfigurationError,
|
|
719
|
+
"Streaming spool directory is already in use: #{@streaming_temp_directory}"
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
@streaming_lock_file = lock_file
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
private
|
|
726
|
+
def release_streaming_spool_lock
|
|
727
|
+
return if @streaming_lock_file.nil?
|
|
728
|
+
|
|
729
|
+
@streaming_lock_file.flock(File::LOCK_UN)
|
|
730
|
+
@streaming_lock_file.close
|
|
731
|
+
@streaming_lock_file = nil
|
|
732
|
+
end
|
|
401
733
|
end
|
|
402
734
|
|
|
403
735
|
# wrapper class
|