logtail 0.1.19 → 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.
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+
3
+ # Rails flushes Rails.logger after every request (Rails::Rack::Logger calls
4
+ # ActiveSupport::LogSubscriber.flush_all!), and Logger#flush returns right away for that call
5
+ # instead of waiting for delivery. These run ActiveSupport's real flush_all! in another process, so
6
+ # an ActiveSupport that calls flush differently fails here. The ingesting host holds its answers
7
+ # until the direct flush, so a flush_all! that waited for delivery would take 5 seconds.
8
+ describe Logtail::Logger, "#flush called by ActiveSupport::LogSubscriber.flush_all!" do
9
+ {
10
+ "a Logtail::Logger" => "logger",
11
+ "a Logtail::Logger in ActiveSupport::TaggedLogging" => "ActiveSupport::TaggedLogging.new(logger)",
12
+ "a Logtail::Logger in an ActiveSupport::BroadcastLogger, as Rails 7.1 and later wrap it" =>
13
+ "ActiveSupport::BroadcastLogger.new(logger)",
14
+ "a Logtail::Logger in ActiveSupport::TaggedLogging in an ActiveSupport::BroadcastLogger" =>
15
+ "ActiveSupport::BroadcastLogger.new(ActiveSupport::TaggedLogging.new(logger))"
16
+ }.each do |description, rails_logger|
17
+ it "returns right away for #{description}, and flushing it directly delivers" do
18
+ if rails_logger.include?("BroadcastLogger") && Gem.loaded_specs["activesupport"].version < Gem::Version.new("7.1")
19
+ skip "ActiveSupport::BroadcastLogger was added in ActiveSupport 7.1"
20
+ end
21
+
22
+ result = run_ruby(<<-RUBY)
23
+ require "logger" # ActiveSupport 6.1 needs it loaded first since concurrent-ruby 1.3.5
24
+ require "active_support"
25
+ require "active_support/log_subscriber"
26
+ require "active_support/tagged_logging"
27
+ require "json"
28
+ require "logtail"
29
+
30
+ # The ingesting host keeps the lines of every request, but answers only once `answering` is set
31
+ delivered = []
32
+ answering = false
33
+ server = TCPServer.new("127.0.0.1", 0)
34
+ Thread.new do
35
+ loop do
36
+ Thread.new(server.accept) do |socket|
37
+ while socket.gets
38
+ length = 0
39
+ while (header = socket.gets) != "\\r\\n"
40
+ length = header.split(":")[1].to_i if header.downcase.start_with?("content-length:")
41
+ end
42
+ delivered.concat(MessagePack.unpack(Zlib::Inflate.inflate(socket.read(length))).map { |line| line["message"] })
43
+ sleep 0.01 until answering
44
+ socket.write("HTTP/1.1 202 Accepted\\r\\nContent-Length: 0\\r\\n\\r\\n")
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # Logs a line for every request, like Rails' ActionController::LogSubscriber
51
+ class RequestLogSubscriber < ActiveSupport::LogSubscriber
52
+ def request(event)
53
+ info(event.payload[:message])
54
+ end
55
+ end
56
+ RequestLogSubscriber.attach_to(:app)
57
+
58
+ logger = Logtail::Logger.new(Logtail::LogDevices::HTTP.new("token", flush_interval: 60,
59
+ ingesting_host: "127.0.0.1", ingesting_port: server.addr[1], ingesting_scheme: "http"))
60
+ ActiveSupport::LogSubscriber.logger = #{rails_logger}
61
+ ActiveSupport::Notifications.instrument("request.app", message: "Completed 200 OK")
62
+
63
+ flushing = Process.clock_gettime(Process::CLOCK_MONOTONIC)
64
+ ActiveSupport::LogSubscriber.flush_all!
65
+ flush_all_seconds = Process.clock_gettime(Process::CLOCK_MONOTONIC) - flushing
66
+ delivered_by_flush_all = delivered.dup
67
+
68
+ answering = true
69
+ ActiveSupport::LogSubscriber.logger.flush
70
+ puts JSON.generate("flush_all_seconds" => flush_all_seconds, "delivered_by_flush_all" => delivered_by_flush_all,
71
+ "delivered_by_flush" => delivered.drop(delivered_by_flush_all.size))
72
+ RUBY
73
+
74
+ expect(result.status).to be_success, result.stderr
75
+ expect(JSON.parse(result.stdout)).to match(
76
+ "flush_all_seconds" => a_value < 1,
77
+ "delivered_by_flush_all" => [],
78
+ "delivered_by_flush" => ["Completed 200 OK"]
79
+ )
80
+ end
81
+ end
82
+ end
@@ -78,6 +78,19 @@ describe Logtail::Logger do
78
78
  expect(io.string).to eq("")
79
79
  end
80
80
 
81
+ # Ruby's Logger formats these severities as "ANY"
82
+ it "should log a nil severity at the unknown level" do
83
+ logger.add(nil, "log message")
84
+ expect(io.string).to include("log message")
85
+ expect(io.string).to include('"level":"unknown"')
86
+ end
87
+
88
+ it "should log a severity above UNKNOWN at the unknown level" do
89
+ logger.add(99, "log message")
90
+ expect(io.string).to include("log message")
91
+ expect(io.string).to include('"level":"unknown"')
92
+ end
93
+
81
94
  it "should not lose message when logging hashes with multiple io devices" do
82
95
  io1 = StringIO.new
83
96
  io2 = StringIO.new
@@ -147,6 +160,20 @@ describe Logtail::Logger do
147
160
  expect(tags).to eq(["tag1", "tag2"])
148
161
  end
149
162
 
163
+ it "should allow a single String in :tags" do
164
+ logger.info("event complete", tags: "tag1")
165
+ expect(io.string).to include("\"tags\":[\"tag1\"]")
166
+ end
167
+
168
+ it "should skip nil tags" do
169
+ logger.info("event complete", tag: nil, tags: nil)
170
+ logger.info("event complete", tags: ["tag1", nil])
171
+ lines = io.string.lines
172
+ expect(lines[0]).to start_with("event complete @metadata")
173
+ expect(lines[0]).not_to include("\"tags\"")
174
+ expect(lines[1]).to include("\"tags\":[\"tag1\"]")
175
+ end
176
+
150
177
  it "should allow functions" do
151
178
  logger.info do
152
179
  {message: "payment rejected", payment_rejected: {customer_id: "abcde1234", amount: 100}}
@@ -179,6 +206,21 @@ describe Logtail::Logger do
179
206
  end
180
207
  end
181
208
 
209
+ describe "#<<" do
210
+ it "logs the line at the info level" do
211
+ http_device = Logtail::LogDevices::HTTP.new("MYKEY", flush_continuously: false)
212
+ logger = Logtail::Logger.new(http_device)
213
+
214
+ logger << "appended line\n"
215
+
216
+ http_device.send(:flush_async)
217
+ request = http_device.instance_variable_get(:@request_queue).deq.request
218
+ entries = MessagePack.unpack(Zlib::Inflate.inflate(request.body))
219
+ expect(entries.size).to eq(1)
220
+ expect(entries[0]).to include("level" => "info", "message" => "appended line")
221
+ end
222
+ end
223
+
182
224
  describe "#error" do
183
225
  let(:io) { StringIO.new }
184
226
  let(:logger) { Logtail::Logger.new(io) }
@@ -197,6 +239,41 @@ describe Logtail::Logger do
197
239
  end
198
240
  end
199
241
 
242
+ describe "#unknown" do
243
+ let(:io) { StringIO.new }
244
+ let(:logger) { Logtail::Logger.new(io) }
245
+
246
+ it "should allow default usage" do
247
+ logger.unknown("log message")
248
+ expect(io.string).to include("log message")
249
+ expect(io.string).to include('"level":"unknown"')
250
+ end
251
+ end
252
+
253
+ describe "#flush" do
254
+ let(:device) { Logtail::LogDevices::HTTP.new("my_source_token") }
255
+ let(:extra_device) { Logtail::LogDevices::HTTP.new("my_source_token") }
256
+ let(:logger) { Logtail::Logger.new(device, extra_device) }
257
+
258
+ it "flushes the log devices, so that what was logged is delivered" do
259
+ expect(device).to receive(:flush)
260
+ expect(extra_device).to receive(:flush)
261
+ logger.flush
262
+ end
263
+
264
+ it "returns right away when Rails flushes it after every request" do
265
+ # Stands in for ActiveSupport::LogSubscriber.flush_all!, which calls Rails.logger.flush
266
+ log_subscriber = Module.new do
267
+ def self.flush_all!(logger)
268
+ logger.flush
269
+ end
270
+ end
271
+ expect(device).not_to receive(:flush)
272
+ expect(extra_device).not_to receive(:flush)
273
+ log_subscriber.flush_all!(logger)
274
+ end
275
+ end
276
+
200
277
  describe "#formatter=" do
201
278
  it "should not allow changing the formatter when the device is HTTP" do
202
279
  http_device = Logtail::LogDevices::HTTP.new("source_token")
data/spec/spec_helper.rb CHANGED
@@ -12,6 +12,7 @@ require File.join(File.dirname(__FILE__), 'support', 'socket_hostname')
12
12
  require File.join(File.dirname(__FILE__), 'support', 'timecop')
13
13
  require File.join(File.dirname(__FILE__), 'support', 'webmock')
14
14
  require File.join(File.dirname(__FILE__), 'support', 'logtail')
15
+ require File.join(File.dirname(__FILE__), 'support', 'processes')
15
16
 
16
17
  RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 5_000
17
18
 
@@ -0,0 +1,91 @@
1
+ require "msgpack"
2
+ require "rbconfig"
3
+ require "socket"
4
+ require "tmpdir"
5
+ require "zlib"
6
+
7
+ # A local stand-in for the Better Stack ingesting host, for tests that run logtail in a separate
8
+ # process: it answers every request with 202 and records the message of every line it receives,
9
+ # and how many lines each request held.
10
+ class LocalIngestServer
11
+ def initialize
12
+ @server = TCPServer.new("127.0.0.1", 0)
13
+ @messages = []
14
+ @batch_sizes = []
15
+ @lock = Mutex.new
16
+ @connections = []
17
+ @thread = Thread.new do
18
+ loop { @connections << Thread.new(@server.accept) { |socket| serve(socket) } }
19
+ end
20
+ end
21
+
22
+ # The options for Logtail::LogDevices::HTTP.new that deliver to this server, as Ruby code.
23
+ def device_options
24
+ %(ingesting_host: "127.0.0.1", ingesting_port: #{@server.addr[1]}, ingesting_scheme: "http")
25
+ end
26
+
27
+ def messages
28
+ @lock.synchronize { @messages.dup }
29
+ end
30
+
31
+ def batch_sizes
32
+ @lock.synchronize { @batch_sizes.dup }
33
+ end
34
+
35
+ def stop
36
+ @thread.kill.join
37
+ @connections.each { |connection| connection.kill.join }
38
+ @server.close
39
+ end
40
+
41
+ private
42
+
43
+ def serve(socket)
44
+ while socket.gets
45
+ headers = {}
46
+ while (header = socket.gets) && header != "\r\n"
47
+ name, value = header.split(":", 2)
48
+ headers[name.downcase] = value.strip
49
+ end
50
+ lines = MessagePack.unpack(Zlib::Inflate.inflate(socket.read(headers["content-length"].to_i)))
51
+ @lock.synchronize do
52
+ @messages.concat(lines.map { |line| line["message"] })
53
+ @batch_sizes << lines.size
54
+ end
55
+ socket.write("HTTP/1.1 202 Accepted\r\nContent-Length: 0\r\n\r\n")
56
+ end
57
+ rescue IOError, SystemCallError
58
+ ensure
59
+ socket.close
60
+ end
61
+ end
62
+
63
+ module ProcessHelpers
64
+ ProcessResult = Struct.new(:stdout, :stderr, :status)
65
+
66
+ # Runs the Ruby script in a new process with logtail from lib/ and waits for it to exit, at
67
+ # most `timeout` seconds (generous: TruffleRuby starts slowly on CI).
68
+ def run_ruby(script, timeout: 120)
69
+ Dir.mktmpdir do |dir|
70
+ File.write(File.join(dir, "script.rb"), script)
71
+ lib = File.expand_path("../../lib", __dir__)
72
+ pid = Process.spawn(RbConfig.ruby, "-I", lib, File.join(dir, "script.rb"),
73
+ out: File.join(dir, "stdout"), err: File.join(dir, "stderr"))
74
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
75
+ status = nil
76
+ until (status = Process.waitpid2(pid, Process::WNOHANG))
77
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
78
+ Process.kill("KILL", pid)
79
+ Process.wait(pid)
80
+ raise "The Ruby process didn't exit within #{timeout} seconds:\n#{File.read(File.join(dir, "stderr"))}"
81
+ end
82
+ sleep 0.05
83
+ end
84
+ ProcessResult.new(File.read(File.join(dir, "stdout")), File.read(File.join(dir, "stderr")), status[1])
85
+ end
86
+ end
87
+ end
88
+
89
+ RSpec.configure do |config|
90
+ config.include ProcessHelpers
91
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logtail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.19
4
+ version: 0.1.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Better Stack
@@ -188,14 +188,20 @@ files:
188
188
  - lib/logtail/version.rb
189
189
  - logtail.gemspec
190
190
  - spec/README.md
191
+ - spec/logtail/config_spec.rb
191
192
  - spec/logtail/current_context_spec.rb
192
193
  - spec/logtail/events/controller_call_spec.rb
193
194
  - spec/logtail/events/error_spec.rb
195
+ - spec/logtail/json_encoding_spec.rb
196
+ - spec/logtail/log_devices/http_exit_spec.rb
197
+ - spec/logtail/log_devices/http_fork_spec.rb
194
198
  - spec/logtail/log_devices/http_spec.rb
195
199
  - spec/logtail/log_entry_spec.rb
200
+ - spec/logtail/logger_flush_all_spec.rb
196
201
  - spec/logtail/logger_spec.rb
197
202
  - spec/spec_helper.rb
198
203
  - spec/support/logtail.rb
204
+ - spec/support/processes.rb
199
205
  - spec/support/socket_hostname.rb
200
206
  - spec/support/timecop.rb
201
207
  - spec/support/webmock.rb