logtail 0.1.20 → 0.1.21
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/Gemfile +3 -0
- data/lib/logtail/event.rb +1 -1
- data/lib/logtail/events/controller_call.rb +1 -1
- data/lib/logtail/events/error.rb +1 -1
- data/lib/logtail/log_devices/http/request_attempt.rb +3 -2
- data/lib/logtail/log_devices/http.rb +362 -28
- data/lib/logtail/log_entry.rb +19 -7
- data/lib/logtail/logger.rb +32 -7
- data/lib/logtail/util/non_nil_hash_builder.rb +1 -1
- data/lib/logtail/util.rb +8 -0
- data/lib/logtail/version.rb +1 -1
- data/spec/logtail/json_encoding_spec.rb +121 -0
- data/spec/logtail/log_devices/http_exit_spec.rb +142 -0
- data/spec/logtail/log_devices/http_fork_spec.rb +85 -0
- data/spec/logtail/log_devices/http_spec.rb +470 -1
- data/spec/logtail/log_entry_spec.rb +73 -0
- data/spec/logtail/logger_flush_all_spec.rb +82 -0
- data/spec/logtail/logger_spec.rb +77 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/processes.rb +91 -0
- metadata +6 -1
data/lib/logtail/logger.rb
CHANGED
|
@@ -15,25 +15,30 @@ module Logtail
|
|
|
15
15
|
#
|
|
16
16
|
# @example Logging an event
|
|
17
17
|
# logger.info "Payment rejected", payment_rejected: {customer_id: customer_id, amount: 100}
|
|
18
|
+
#
|
|
19
|
+
# @example Adding context to one log line (merged into the gem's context, whose own keys win)
|
|
20
|
+
# logger.info "Payment rejected", context: {tenant_id: tenant_id}
|
|
18
21
|
class Logger < ::Logger
|
|
19
22
|
|
|
20
23
|
# @private
|
|
21
24
|
class Formatter
|
|
22
|
-
# Formatters get the formatted level from the logger.
|
|
25
|
+
# Formatters get the formatted level from the logger. Ruby's Logger formats UNKNOWN, and any
|
|
26
|
+
# severity it doesn't know, as "ANY".
|
|
23
27
|
SEVERITY_MAP = {
|
|
24
28
|
"DEBUG" => :debug,
|
|
25
29
|
"INFO" => :info,
|
|
26
30
|
"WARN" => :warn,
|
|
27
31
|
"ERROR" => :error,
|
|
28
32
|
"FATAL" => :fatal,
|
|
29
|
-
"UNKNOWN" => :unknown
|
|
33
|
+
"UNKNOWN" => :unknown,
|
|
34
|
+
"ANY" => :unknown
|
|
30
35
|
}
|
|
31
36
|
EMPTY_ARRAY = []
|
|
32
37
|
|
|
33
38
|
private
|
|
34
39
|
def build_log_entry(severity, time, progname, logged_obj)
|
|
35
40
|
context_snapshot = CurrentContext.instance.snapshot
|
|
36
|
-
level = SEVERITY_MAP.fetch(severity)
|
|
41
|
+
level = SEVERITY_MAP.fetch(severity, :unknown)
|
|
37
42
|
tags = extract_active_support_tagged_logging_tags.clone
|
|
38
43
|
|
|
39
44
|
if logged_obj.is_a?(Event)
|
|
@@ -41,8 +46,9 @@ module Logtail
|
|
|
41
46
|
tags: tags)
|
|
42
47
|
elsif logged_obj.is_a?(Hash)
|
|
43
48
|
# Extract the tags
|
|
44
|
-
tags.
|
|
45
|
-
tags.concat(logged_obj[:tags]) if logged_obj.key?(:tags)
|
|
49
|
+
tags.concat(Array(logged_obj[:tag])) if logged_obj.key?(:tag)
|
|
50
|
+
tags.concat(Array(logged_obj[:tags])) if logged_obj.key?(:tags)
|
|
51
|
+
tags.compact!
|
|
46
52
|
tags.uniq!
|
|
47
53
|
|
|
48
54
|
message = logged_obj[:message]
|
|
@@ -192,8 +198,6 @@ module Logtail
|
|
|
192
198
|
Logtail::Config.instance.debug { "Logtail::Logger instantiated, level: #{level}, formatter: #{formatter.class}" }
|
|
193
199
|
|
|
194
200
|
@initialized = true
|
|
195
|
-
|
|
196
|
-
at_exit { self.close }
|
|
197
201
|
end
|
|
198
202
|
|
|
199
203
|
# Sets a new formatted on the logger.
|
|
@@ -216,6 +220,21 @@ module Logtail
|
|
|
216
220
|
super
|
|
217
221
|
end
|
|
218
222
|
|
|
223
|
+
# Delivers what was logged so far before it returns, waiting about 5 seconds at most with
|
|
224
|
+
# the HTTP log device. Call it before a process ends with `exit!`, which skips the at_exit
|
|
225
|
+
# hook that delivers the rest, as Resque's forked job processes do.
|
|
226
|
+
#
|
|
227
|
+
# Rails calls `flush` on Rails.logger after every request (ActiveSupport::LogSubscriber.flush_all!).
|
|
228
|
+
# Waiting there would hold up every request, so that call returns right away and the lines
|
|
229
|
+
# are delivered in the background as usual.
|
|
230
|
+
def flush
|
|
231
|
+
return true if caller_locations(1, 10).any? { |location| location.base_label == "flush_all!" }
|
|
232
|
+
|
|
233
|
+
@logdev.dev.flush if @logdev && @logdev.dev.respond_to?(:flush)
|
|
234
|
+
@extra_loggers.each { |logger| logger.flush if logger.respond_to?(:flush) }
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
|
|
219
238
|
# @private
|
|
220
239
|
def with_context(context, &block)
|
|
221
240
|
Logtail::CurrentContext.with(context, &block)
|
|
@@ -233,6 +252,12 @@ module Logtail
|
|
|
233
252
|
super
|
|
234
253
|
end
|
|
235
254
|
|
|
255
|
+
# Logs the message as an info line. ::Logger#<< writes it to the log device as it is, which
|
|
256
|
+
# the HTTP device can't deliver. Rack::CommonLogger, for example, logs requests this way.
|
|
257
|
+
def <<(msg)
|
|
258
|
+
info(msg.to_s.chomp)
|
|
259
|
+
end
|
|
260
|
+
|
|
236
261
|
# Backwards compatibility with older ActiveSupport::Logger versions
|
|
237
262
|
Logger::Severity.constants.each do |severity|
|
|
238
263
|
class_eval(<<-EOT, __FILE__, __LINE__ + 1)
|
data/lib/logtail/util.rb
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
1
3
|
require "logtail/util/non_nil_hash_builder"
|
|
2
4
|
|
|
3
5
|
module Logtail
|
|
4
6
|
# @private
|
|
5
7
|
module Util
|
|
8
|
+
# Encodes data as JSON. With ActiveSupport loaded, the data is converted with `as_json` first,
|
|
9
|
+
# so values come out as ActiveSupport's `to_json` encoded them (times in ISO 8601, NaN as
|
|
10
|
+
# null, ...), but without its encoder: with json 3, ActiveSupport 8.0 and older raise from it.
|
|
11
|
+
def self.generate_json(data)
|
|
12
|
+
::JSON.generate(data.respond_to?(:as_json) ? data.as_json : data)
|
|
13
|
+
end
|
|
6
14
|
end
|
|
7
15
|
end
|
data/lib/logtail/version.rb
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
# With json 3, ActiveSupport 8.0 and older break `to_json` called directly: ActiveSupport's
|
|
5
|
+
# encoder passes `quirks_mode:` to JSON.generate, and json 3 raises on the unknown keyword.
|
|
6
|
+
# Calls made by JSON.generate itself pass a JSON::State and keep working. The stub does the same
|
|
7
|
+
# for the Hashes and Arrays the gem encodes (TruffleRuby can't stub it on frozen String keys).
|
|
8
|
+
describe "JSON encoding when to_json raises like json 3 under ActiveSupport 8.0" do
|
|
9
|
+
let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
|
|
10
|
+
let(:io) { StringIO.new }
|
|
11
|
+
let(:logger) { Logtail::Logger.new(io) }
|
|
12
|
+
|
|
13
|
+
before(:each) do
|
|
14
|
+
[Hash, Array].each do |klass|
|
|
15
|
+
allow_any_instance_of(klass).to receive(:to_json).and_wrap_original do |original, *args|
|
|
16
|
+
raise ArgumentError, "unknown keyword: :quirks_mode" unless args.first.is_a?(::JSON::State)
|
|
17
|
+
original.call(*args)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
around(:each) do |example|
|
|
23
|
+
Timecop.freeze(time) { example.run }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "should log with the JSONFormatter" do
|
|
27
|
+
logger.formatter = Logtail::Logger::JSONFormatter.new
|
|
28
|
+
logger.info("log message", payment: {amount: 100, tags: ["a"]})
|
|
29
|
+
|
|
30
|
+
line = JSON.parse(io.string)
|
|
31
|
+
expect(line).to include("level" => "info", "dt" => "2016-09-01T12:00:00.000000Z", "message" => "log message")
|
|
32
|
+
expect(line["payment"]).to eq("amount" => 100, "tags" => ["a"])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "should log with the AugmentedFormatter" do
|
|
36
|
+
logger.formatter = Logtail::Logger::AugmentedFormatter.new
|
|
37
|
+
logger.info("log message", payment: {amount: 100})
|
|
38
|
+
|
|
39
|
+
expect(io.string).to start_with("log message @metadata {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\"")
|
|
40
|
+
expect(io.string).to include("\"payment\":{\"amount\":100}")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "should encode a log entry" do
|
|
44
|
+
log_entry = Logtail::LogEntry.new("INFO", time, nil, "log message", {custom: {a: "b"}}, nil)
|
|
45
|
+
|
|
46
|
+
expect(JSON.parse(log_entry.to_json)).to include("level" => "INFO", "message" => "log message", "context" => {"custom" => {"a" => "b"}, "runtime" => {}})
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "should encode an event" do
|
|
50
|
+
event = Logtail::Event.new("log message", {payment: {amount: 100}})
|
|
51
|
+
|
|
52
|
+
expect(event.to_json).to eq("{\"payment\":{\"amount\":100}}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "should encode the backtrace of an error event" do
|
|
56
|
+
event = Logtail::Events::Error.new(name: "RuntimeError", error_message: "Boom", backtrace: ["/path/to/file1.rb:26:in `function1'"])
|
|
57
|
+
|
|
58
|
+
expect(event.backtrace_json).to eq("[\"/path/to/file1.rb:26:in `function1'\"]")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "should encode the params of a controller call event" do
|
|
62
|
+
event = Logtail::Events::ControllerCall.new(controller: "OrdersController", action: "show", params: {"id" => "1"})
|
|
63
|
+
|
|
64
|
+
expect(event.params_json).to eq("{\"id\":\"1\"}")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "should encode values added with json_encode" do
|
|
68
|
+
hash = Logtail::Util::NonNilHashBuilder.build { |h| h.add(:headers_json, {"Accept" => "*/*"}, json_encode: true) }
|
|
69
|
+
|
|
70
|
+
expect(hash).to eq(headers_json: "{\"Accept\":\"*/*\"}")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ActiveSupport changes how the whole process encodes JSON, so these examples load it in another
|
|
75
|
+
# process. It prints a log entry encoded by ActiveSupport's encoder, as LogEntry#to_json did
|
|
76
|
+
# before, then by LogEntry#to_json and the JSONFormatter while that encoder raises, as it does
|
|
77
|
+
# with json 3 on ActiveSupport 8.0 and older.
|
|
78
|
+
describe "JSON encoding with ActiveSupport loaded" do
|
|
79
|
+
def encode_with_active_support(fields)
|
|
80
|
+
script = <<~RUBY
|
|
81
|
+
require "logger" # ActiveSupport 6.1 needs it loaded first since concurrent-ruby 1.3.5
|
|
82
|
+
require "active_support"
|
|
83
|
+
require "active_support/core_ext/object/json"
|
|
84
|
+
require "logtail"
|
|
85
|
+
|
|
86
|
+
fields = #{fields}
|
|
87
|
+
log_entry = Logtail::LogEntry.new("INFO", Time.utc(2016, 9, 1, 12), nil, "log message", nil, fields)
|
|
88
|
+
puts ActiveSupport::JSON.encode(log_entry.to_hash)
|
|
89
|
+
|
|
90
|
+
ActiveSupport::JSON.singleton_class.prepend(Module.new do
|
|
91
|
+
def encode(*)
|
|
92
|
+
raise ArgumentError, "unknown keyword: quirks_mode"
|
|
93
|
+
end
|
|
94
|
+
end)
|
|
95
|
+
puts log_entry.to_json
|
|
96
|
+
logger = Logtail::Logger.new($stdout)
|
|
97
|
+
logger.formatter = Logtail::Logger::JSONFormatter.new
|
|
98
|
+
logger.info("log message", fields)
|
|
99
|
+
RUBY
|
|
100
|
+
lib = File.expand_path("../../lib", __dir__)
|
|
101
|
+
output, error, status = Open3.capture3(RbConfig.ruby, "-rbundler/setup", "-I", lib, "-e", script)
|
|
102
|
+
expect(status.success?).to be(true), error
|
|
103
|
+
output.lines.map { |line| JSON.parse(line) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "should encode times and symbols as ActiveSupport did" do
|
|
107
|
+
encoded_by_active_support, encoded, logged = encode_with_active_support("{time: Time.utc(2026, 1, 2, 3, 4, 5), status: :active}")
|
|
108
|
+
|
|
109
|
+
expect(encoded).to eq(encoded_by_active_support)
|
|
110
|
+
expect(encoded).to include("time" => "2026-01-02T03:04:05.000Z", "status" => "active")
|
|
111
|
+
expect(logged).to include("level" => "info", "time" => "2026-01-02T03:04:05.000Z", "status" => "active")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "should encode NaN and Infinity as null, as ActiveSupport did" do
|
|
115
|
+
encoded_by_active_support, encoded, logged = encode_with_active_support("{nan: Float::NAN, infinity: Float::INFINITY}")
|
|
116
|
+
|
|
117
|
+
expect(encoded).to eq(encoded_by_active_support)
|
|
118
|
+
expect(encoded).to include("nan" => nil, "infinity" => nil)
|
|
119
|
+
expect(logged).to include("level" => "info", "nan" => nil, "infinity" => nil)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "delegate"
|
|
3
|
+
|
|
4
|
+
# A local ingesting server that takes 3 seconds to answer each request
|
|
5
|
+
class SlowAnsweringIngestServer < LocalIngestServer
|
|
6
|
+
class SlowSocket < SimpleDelegator
|
|
7
|
+
def write(*args)
|
|
8
|
+
sleep 3
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def serve(socket)
|
|
16
|
+
super(SlowSocket.new(socket))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What the HTTP device does while the process that logs with it exits. These run logtail in a
|
|
21
|
+
# real Ruby process, and count what reaches a local ingesting server.
|
|
22
|
+
describe Logtail::LogDevices::HTTP, "when the process exits" do
|
|
23
|
+
let(:ingest) { LocalIngestServer.new }
|
|
24
|
+
|
|
25
|
+
after { ingest.stop }
|
|
26
|
+
|
|
27
|
+
# A local port that nothing listens on, so connecting to it is refused.
|
|
28
|
+
def unused_port
|
|
29
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
30
|
+
server.addr[1]
|
|
31
|
+
ensure
|
|
32
|
+
server.close
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "delivers a line that a thread logs while Ruby kills it at exit, without printing an error" do
|
|
36
|
+
result = run_ruby(<<-RUBY)
|
|
37
|
+
require "logtail"
|
|
38
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", #{ingest.device_options}))
|
|
39
|
+
logger.info("logged before exit")
|
|
40
|
+
started = Queue.new
|
|
41
|
+
Thread.new do
|
|
42
|
+
begin
|
|
43
|
+
started << true
|
|
44
|
+
sleep
|
|
45
|
+
ensure
|
|
46
|
+
logger.info("logged by a thread that Ruby kills at exit")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
started.pop
|
|
50
|
+
RUBY
|
|
51
|
+
|
|
52
|
+
expect(result.stderr).not_to include("log writing failed")
|
|
53
|
+
expect(ingest.messages).to contain_exactly("logged before exit", "logged by a thread that Ruby kills at exit")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "doesn't warn about the instance variables it uses to close, with warnings on" do
|
|
57
|
+
# Ruby 2.7 and older warn about an instance variable that is read before it's set
|
|
58
|
+
result = run_ruby(<<-RUBY)
|
|
59
|
+
$VERBOSE = true
|
|
60
|
+
require "logtail"
|
|
61
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", #{ingest.device_options}))
|
|
62
|
+
logger.info("logged before close")
|
|
63
|
+
logger.close
|
|
64
|
+
logger.info("logged after close")
|
|
65
|
+
RUBY
|
|
66
|
+
|
|
67
|
+
expect(result.status).to be_success, result.stderr
|
|
68
|
+
expect(result.stderr).not_to match(/@(closed|late_delivery_failed|last_resp) not initialized/)
|
|
69
|
+
expect(ingest.messages).to contain_exactly("logged before close", "logged after close")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "delivers a line that an at_exit hook logs after the device closed" do
|
|
73
|
+
result = run_ruby(<<-RUBY)
|
|
74
|
+
require "logtail"
|
|
75
|
+
logger = nil
|
|
76
|
+
at_exit { logger.info("logged by an at_exit hook registered first, which runs last") }
|
|
77
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", #{ingest.device_options}))
|
|
78
|
+
logger.info("logged before exit")
|
|
79
|
+
RUBY
|
|
80
|
+
|
|
81
|
+
expect(result.status).to be_success, result.stderr
|
|
82
|
+
expect(ingest.messages).to contain_exactly("logged before exit", "logged by an at_exit hook registered first, which runs last")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "waits up to 5 seconds for a slow host when it delivers lines logged after closing" do
|
|
86
|
+
slow_ingest = SlowAnsweringIngestServer.new
|
|
87
|
+
result = run_ruby(<<-RUBY)
|
|
88
|
+
require "logtail"
|
|
89
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", #{slow_ingest.device_options}))
|
|
90
|
+
logger.close
|
|
91
|
+
logger.info("first line after close")
|
|
92
|
+
logger.info("second line after close")
|
|
93
|
+
RUBY
|
|
94
|
+
|
|
95
|
+
# A delivery that times out would drop the second line
|
|
96
|
+
expect(result.status).to be_success, result.stderr
|
|
97
|
+
expect(slow_ingest.messages).to contain_exactly("first line after close", "second line after close")
|
|
98
|
+
ensure
|
|
99
|
+
slow_ingest.stop if slow_ingest
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "delivers a batch that the outlet takes off the queue while close checks whether it's done" do
|
|
103
|
+
result = run_ruby(<<-RUBY)
|
|
104
|
+
require "logtail"
|
|
105
|
+
# Widens the moment between the outlet taking a batch off the queue and counting it as in flight
|
|
106
|
+
Logtail::LogDevices::HTTP::RequestAttempt.prepend(Module.new { define_method(:attempted!) { sleep 1.5; super() } })
|
|
107
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", #{ingest.device_options}))
|
|
108
|
+
logger.info("logged right before exit")
|
|
109
|
+
RUBY
|
|
110
|
+
|
|
111
|
+
expect(result.status).to be_success, result.stderr
|
|
112
|
+
expect(ingest.messages).to eq(["logged right before exit"])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "closes a device once at exit, however many loggers write to it" do
|
|
116
|
+
result = run_ruby(<<-RUBY)
|
|
117
|
+
require "logtail"
|
|
118
|
+
closes = 0
|
|
119
|
+
at_exit { puts "closes: " + closes.to_s }
|
|
120
|
+
Logtail::LogDevices::HTTP.prepend(Module.new { define_method(:close) { closes += 1; super() } })
|
|
121
|
+
device = Logtail::LogDevices::HTTP.new("token", #{ingest.device_options})
|
|
122
|
+
3.times { |n| Logtail::Logger.new(device).info("line " + n.to_s) }
|
|
123
|
+
RUBY
|
|
124
|
+
|
|
125
|
+
expect(result.stdout).to eq("closes: 1\n"), result.stderr
|
|
126
|
+
expect(ingest.messages).to contain_exactly("line 0", "line 1", "line 2")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "exits within 5 seconds when the ingesting host can't be reached, however many loggers there are" do
|
|
130
|
+
result = run_ruby(<<-RUBY)
|
|
131
|
+
exiting = nil
|
|
132
|
+
at_exit { puts Process.clock_gettime(Process::CLOCK_MONOTONIC) - exiting }
|
|
133
|
+
require "logtail"
|
|
134
|
+
device = Logtail::LogDevices::HTTP.new("token", ingesting_host: "127.0.0.1", ingesting_port: #{unused_port}, ingesting_scheme: "http")
|
|
135
|
+
2.times { |n| Logtail::Logger.new(device).info("line " + n.to_s) }
|
|
136
|
+
exiting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
137
|
+
RUBY
|
|
138
|
+
|
|
139
|
+
expect(result.status).to be_success, result.stderr
|
|
140
|
+
expect(result.stdout.to_f).to be < 5
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
# Forking servers and job runners (Puma, Unicorn, Resque) fork after the app logged. These run
|
|
4
|
+
# logtail in a real process that forks, and count what reaches a local ingesting server.
|
|
5
|
+
describe Logtail::LogDevices::HTTP, "after a fork" do
|
|
6
|
+
let(:ingest) { LocalIngestServer.new }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
skip "needs fork" if !Process.respond_to?(:fork) || RUBY_ENGINE == "truffleruby"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
after { ingest.stop }
|
|
13
|
+
|
|
14
|
+
it "delivers the lines the parent logged before forking once, not once per process" do
|
|
15
|
+
result = run_ruby(<<-RUBY)
|
|
16
|
+
require "logtail"
|
|
17
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", flush_interval: 60, #{ingest.device_options}))
|
|
18
|
+
logger.info("parent line before the fork")
|
|
19
|
+
2.times.map { |n| fork { logger.info("child line " + n.to_s) } }.each { |pid| Process.wait(pid) }
|
|
20
|
+
RUBY
|
|
21
|
+
|
|
22
|
+
expect(result.status).to be_success, result.stderr
|
|
23
|
+
expect(ingest.messages).to contain_exactly("parent line before the fork", "child line 0", "child line 1")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "delivers the lines a forked child logs" do
|
|
27
|
+
result = run_ruby(<<-RUBY)
|
|
28
|
+
require "logtail"
|
|
29
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", flush_interval: 60, #{ingest.device_options}))
|
|
30
|
+
Process.wait(fork { 3.times { |n| logger.info("child line " + n.to_s) } })
|
|
31
|
+
logger.info("parent line after the fork")
|
|
32
|
+
RUBY
|
|
33
|
+
|
|
34
|
+
expect(result.status).to be_success, result.stderr
|
|
35
|
+
expect(ingest.messages).to contain_exactly("child line 0", "child line 1", "child line 2", "parent line after the fork")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "lets a child that hasn't logged exit right away, without waiting on the parent's lines" do
|
|
39
|
+
result = run_ruby(<<-RUBY)
|
|
40
|
+
require "logtail"
|
|
41
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", flush_interval: 60, #{ingest.device_options}))
|
|
42
|
+
logger.info("parent line before the fork")
|
|
43
|
+
forking = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
44
|
+
Process.wait(fork {})
|
|
45
|
+
puts Process.clock_gettime(Process::CLOCK_MONOTONIC) - forking
|
|
46
|
+
RUBY
|
|
47
|
+
|
|
48
|
+
expect(result.stdout.to_f).to be < 5
|
|
49
|
+
expect(ingest.messages).to eq(["parent line before the fork"])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "batches a child's lines as usual when the parent closed the device before forking" do
|
|
53
|
+
result = run_ruby(<<-RUBY)
|
|
54
|
+
require "logtail"
|
|
55
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", flush_interval: 60, #{ingest.device_options}))
|
|
56
|
+
logger.info("parent line")
|
|
57
|
+
logger.close
|
|
58
|
+
Process.wait(fork { 3.times { |n| logger.info("child line " + n.to_s) } })
|
|
59
|
+
RUBY
|
|
60
|
+
|
|
61
|
+
expect(result.status).to be_success, result.stderr
|
|
62
|
+
expect(ingest.messages).to contain_exactly("parent line", "child line 0", "child line 1", "child line 2")
|
|
63
|
+
# Lines written after close are each delivered on their own, see Logtail::LogDevices::HTTP#write
|
|
64
|
+
expect(ingest.batch_sizes).to eq([1, 3])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "delivers a child's lines when it calls flush before leaving with exit!, which skips at_exit hooks" do
|
|
68
|
+
result = run_ruby(<<-RUBY)
|
|
69
|
+
require "logtail"
|
|
70
|
+
logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", flush_interval: 60, #{ingest.device_options}))
|
|
71
|
+
logger.info("parent line")
|
|
72
|
+
Process.wait(fork do
|
|
73
|
+
begin
|
|
74
|
+
logger.info("child line")
|
|
75
|
+
logger.flush
|
|
76
|
+
ensure
|
|
77
|
+
exit!(0) # the way Resque ends a job's process
|
|
78
|
+
end
|
|
79
|
+
end)
|
|
80
|
+
RUBY
|
|
81
|
+
|
|
82
|
+
expect(result.status).to be_success, result.stderr
|
|
83
|
+
expect(ingest.messages).to contain_exactly("parent line", "child line")
|
|
84
|
+
end
|
|
85
|
+
end
|