logtail-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +33 -0
- data/.gitignore +24 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +10 -0
- data/LICENSE.md +15 -0
- data/README.md +4 -0
- data/Rakefile +72 -0
- data/lib/logtail.rb +36 -0
- data/lib/logtail/config.rb +154 -0
- data/lib/logtail/config/integrations.rb +17 -0
- data/lib/logtail/context.rb +9 -0
- data/lib/logtail/contexts.rb +12 -0
- data/lib/logtail/contexts/http.rb +31 -0
- data/lib/logtail/contexts/release.rb +52 -0
- data/lib/logtail/contexts/runtime.rb +23 -0
- data/lib/logtail/contexts/session.rb +24 -0
- data/lib/logtail/contexts/system.rb +29 -0
- data/lib/logtail/contexts/user.rb +28 -0
- data/lib/logtail/current_context.rb +168 -0
- data/lib/logtail/event.rb +36 -0
- data/lib/logtail/events.rb +10 -0
- data/lib/logtail/events/controller_call.rb +44 -0
- data/lib/logtail/events/error.rb +40 -0
- data/lib/logtail/events/exception.rb +10 -0
- data/lib/logtail/events/sql_query.rb +26 -0
- data/lib/logtail/events/template_render.rb +25 -0
- data/lib/logtail/integration.rb +40 -0
- data/lib/logtail/integrator.rb +50 -0
- data/lib/logtail/log_devices.rb +8 -0
- data/lib/logtail/log_devices/http.rb +368 -0
- data/lib/logtail/log_devices/http/flushable_dropping_sized_queue.rb +52 -0
- data/lib/logtail/log_devices/http/request_attempt.rb +20 -0
- data/lib/logtail/log_entry.rb +110 -0
- data/lib/logtail/logger.rb +270 -0
- data/lib/logtail/logtail.rb +36 -0
- data/lib/logtail/timer.rb +21 -0
- data/lib/logtail/util.rb +7 -0
- data/lib/logtail/util/non_nil_hash_builder.rb +40 -0
- data/lib/logtail/version.rb +3 -0
- data/logtail-ruby.gemspec +43 -0
- data/spec/README.md +13 -0
- data/spec/logtail/current_context_spec.rb +113 -0
- data/spec/logtail/events/controller_call_spec.rb +12 -0
- data/spec/logtail/events/error_spec.rb +15 -0
- data/spec/logtail/log_devices/http_spec.rb +185 -0
- data/spec/logtail/log_entry_spec.rb +22 -0
- data/spec/logtail/logger_spec.rb +227 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/logtail.rb +5 -0
- data/spec/support/socket_hostname.rb +12 -0
- data/spec/support/timecop.rb +3 -0
- data/spec/support/webmock.rb +3 -0
- metadata +238 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'logtail/util'
|
2
|
+
require 'logtail/event'
|
3
|
+
|
4
|
+
module Logtail
|
5
|
+
module Events
|
6
|
+
# @private
|
7
|
+
class SQLQuery < Logtail::Event
|
8
|
+
attr_reader :sql, :duration_ms, :message
|
9
|
+
|
10
|
+
def initialize(attributes)
|
11
|
+
@sql = attributes[:sql]
|
12
|
+
@duration_ms = attributes[:duration_ms]
|
13
|
+
@message = attributes[:message]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_hash
|
17
|
+
{
|
18
|
+
sql_query_executed: Util::NonNilHashBuilder.build do |h|
|
19
|
+
h.add(:sql, sql)
|
20
|
+
h.add(:duration_ms, duration_ms)
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "logtail/util"
|
2
|
+
require "logtail/event"
|
3
|
+
module Logtail
|
4
|
+
module Events
|
5
|
+
# @private
|
6
|
+
class TemplateRender < Logtail::Event
|
7
|
+
attr_reader :message, :name, :duration_ms
|
8
|
+
|
9
|
+
def initialize(attributes)
|
10
|
+
@name = attributes[:name]
|
11
|
+
@duration_ms = attributes[:duration_ms]
|
12
|
+
@message = attributes[:message]
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
{
|
17
|
+
template_rendered: Util::NonNilHashBuilder.build do |h|
|
18
|
+
h.add(:name, name)
|
19
|
+
h.add(:duration_ms, duration_ms)
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Logtail
|
2
|
+
# An integration represent an integration for an entire library. For example, `Rack`.
|
3
|
+
# While the Logtail `Rack` integration is comprised of multiple middlewares, the
|
4
|
+
# `Logtail::Integrations::Rack` module is an entire integration that extends this module.
|
5
|
+
module Integration
|
6
|
+
# Easily sisable entire library integrations. This is like removing the code from
|
7
|
+
# Logtail. It will not touch this library and the library will function as it would
|
8
|
+
# without Logtail.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# Logtail::Integrations::ActiveRecord.enabled = false
|
12
|
+
def enabled=(value)
|
13
|
+
@enabled = value
|
14
|
+
end
|
15
|
+
|
16
|
+
# Accessor method for {#enabled=}
|
17
|
+
def enabled?
|
18
|
+
@enabled != false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Silences a library's logs. This ensures that logs are not generated at all
|
22
|
+
# from this library.
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# Logtail::Integrations::ActiveRecord.silence = true
|
26
|
+
def silence=(value)
|
27
|
+
@silence = value
|
28
|
+
end
|
29
|
+
|
30
|
+
# Accessor method for {#silence=}
|
31
|
+
def silence?
|
32
|
+
@silence == true
|
33
|
+
end
|
34
|
+
|
35
|
+
# Abstract method that each integration must implement.
|
36
|
+
def integrate!
|
37
|
+
raise NotImplementedError.new
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Logtail
|
2
|
+
# Base class for `Logtail::Integrations::*`. Provides a common interface for all integrators.
|
3
|
+
# An integrator is a single specific integration into a part of a library. See
|
4
|
+
# {Integration} for higher library level integration settings.
|
5
|
+
class Integrator
|
6
|
+
# Raised when an integrators requirements are not met. For example, this will be raised
|
7
|
+
# in the ActiveRecord integration if ActiveRecord is not available as a dependency in
|
8
|
+
# the current application.
|
9
|
+
class RequirementNotMetError < StandardError; end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_writer :enabled
|
13
|
+
|
14
|
+
# Allows you to enable / disable specific integrations.
|
15
|
+
#
|
16
|
+
# @note Disabling specific low level integrations should only be needed for edge cases.
|
17
|
+
# If you want to disable integration with an entire library, we recommend doing so
|
18
|
+
# at a higher level. Ex: `Logtail::Integrations::ActiveRecord.enabled = false`.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Logtail::Integrations::ActiveRecord::LogSubscriber.enabled = false
|
22
|
+
def enabled?
|
23
|
+
@enabled != false
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convenience class level method that runs the integrator by instantiating a new
|
27
|
+
# object and calling {#integrate!}. It also takes care to look at the if the integrator
|
28
|
+
# is enabled, skipping it if not.
|
29
|
+
def integrate!(*args)
|
30
|
+
if !enabled?
|
31
|
+
Config.instance.debug_logger.debug("#{name} integration disabled, skipping") if Config.instance.debug_logger
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
new(*args).integrate!
|
36
|
+
Config.instance.debug_logger.debug("Integrated #{name}") if Config.instance.debug_logger
|
37
|
+
true
|
38
|
+
# RequirementUnsatisfiedError is the only silent failure we support
|
39
|
+
rescue RequirementNotMetError => e
|
40
|
+
Config.instance.debug_logger.debug("Failed integrating #{name}: #{e.message}") if Config.instance.debug_logger
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Abstract method that each integration must implement.
|
46
|
+
def integrate!
|
47
|
+
raise NotImplementedError.new
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "msgpack"
|
3
|
+
require "net/https"
|
4
|
+
|
5
|
+
require "logtail/config"
|
6
|
+
require "logtail/log_devices/http/flushable_dropping_sized_queue"
|
7
|
+
require "logtail/log_devices/http/request_attempt"
|
8
|
+
require "logtail/version"
|
9
|
+
|
10
|
+
module Logtail
|
11
|
+
module LogDevices
|
12
|
+
# A highly efficient log device that buffers and delivers log messages over HTTPS to
|
13
|
+
# the Logtail API. It uses batches, keep-alive connections, and msgpack to deliver logs with
|
14
|
+
# high-throughput and little overhead. All log preparation and delivery is done asynchronously
|
15
|
+
# in a thread as not to block application execution and efficiently deliver logs for
|
16
|
+
# multi-threaded environments.
|
17
|
+
#
|
18
|
+
# See {#initialize} for options and more details.
|
19
|
+
class HTTP
|
20
|
+
LOGTAIL_STAGING_HOST = "logs-staging.logtail.com".freeze
|
21
|
+
LOGTAIL_PRODUCTION_HOST = "logs.logtail.com".freeze
|
22
|
+
LOGTAIL_HOST = ENV['LOGTAIL_STAGING'] ? LOGTAIL_STAGING_HOST : LOGTAIL_PRODUCTION_HOST
|
23
|
+
LOGTAIL_PORT = 443
|
24
|
+
LOGTAIL_SCHEME = "https".freeze
|
25
|
+
CONTENT_TYPE = "application/msgpack".freeze
|
26
|
+
USER_AGENT = "Logtail Ruby/#{Logtail::VERSION} (HTTP)".freeze
|
27
|
+
|
28
|
+
# Instantiates a new HTTP log device that can be passed to {Logtail::Logger#initialize}.
|
29
|
+
#
|
30
|
+
# The class maintains a buffer which is flushed in batches to the Logtail API. 2
|
31
|
+
# options control when the flush happens, `:batch_byte_size` and `:flush_interval`.
|
32
|
+
# If either of these are surpassed, the buffer will be flushed.
|
33
|
+
#
|
34
|
+
# By default, the buffer will apply back pressure when the rate of log messages exceeds
|
35
|
+
# the maximum delivery rate. If you don't want to sacrifice app performance in this case
|
36
|
+
# you can drop the log messages instead by passing a {DroppingSizedQueue} via the
|
37
|
+
# `:request_queue` option.
|
38
|
+
#
|
39
|
+
# @param api_key [String] The API key provided to you after you add your application to
|
40
|
+
# [Logtail](https://logtail.com).
|
41
|
+
# @param [Hash] options the options to create a HTTP log device with.
|
42
|
+
# @option attributes [Symbol] :batch_size (1000) Determines the maximum of log lines in
|
43
|
+
# each HTTP payload. If the queue exceeds this limit an HTTP request will be issued. Bigger
|
44
|
+
# payloads mean higher throughput, but also use more memory. Logtail will not accept
|
45
|
+
# payloads larger than 1mb.
|
46
|
+
# @option attributes [Symbol] :flush_continuously (true) This should only be disabled under
|
47
|
+
# special circumstsances (like test suites). Setting this to `false` disables the
|
48
|
+
# continuous flushing of log message. As a result, flushing must be handled externally
|
49
|
+
# via the #flush method.
|
50
|
+
# @option attributes [Symbol] :flush_interval (1) How often the client should
|
51
|
+
# attempt to deliver logs to the Logtail API in fractional seconds. The HTTP client buffers
|
52
|
+
# logs and this options represents how often that will happen, assuming `:batch_byte_size`
|
53
|
+
# is not met.
|
54
|
+
# @option attributes [Symbol] :requests_per_conn (2500) The number of requests to send over a
|
55
|
+
# single persistent connection. After this number is met, the connection will be closed
|
56
|
+
# and a new one will be opened.
|
57
|
+
# @option attributes [Symbol] :request_queue (FlushableDroppingSizedQueue.new(25)) The request
|
58
|
+
# queue object that queues Net::HTTP requests for delivery. By deafult this is a
|
59
|
+
# `FlushableDroppingSizedQueue` of size `25`. Meaning once the queue fills up to 25
|
60
|
+
# requests new requests will be dropped. If you'd prefer to apply back pressure,
|
61
|
+
# ensuring you do not lose log data, pass a standard {SizedQueue}. See examples for
|
62
|
+
# an example.
|
63
|
+
# @option attributes [Symbol] :logtail_host The Logtail host to delivery the log lines to.
|
64
|
+
# The default is set via {LOGTAIL_HOST}.
|
65
|
+
#
|
66
|
+
# @example Basic usage
|
67
|
+
# Logtail::Logger.new(Logtail::LogDevices::HTTP.new("my_logtail_api_key"))
|
68
|
+
#
|
69
|
+
# @example Apply back pressure instead of dropping messages
|
70
|
+
# http_log_device = Logtail::LogDevices::HTTP.new("my_logtail_api_key", request_queue: SizedQueue.new(25))
|
71
|
+
# Logtail::Logger.new(http_log_device)
|
72
|
+
def initialize(api_key, options = {})
|
73
|
+
@api_key = api_key || raise(ArgumentError.new("The api_key parameter cannot be blank"))
|
74
|
+
@logtail_host = options[:logtail_host] || ENV['LOGTAIL_HOST'] || LOGTAIL_HOST
|
75
|
+
@logtail_port = options[:logtail_port] || ENV['LOGTAIL_PORT'] || LOGTAIL_PORT
|
76
|
+
@logtail_scheme = options[:logtail_scheme] || ENV['LOGTAIL_SCHEME'] || LOGTAIL_SCHEME
|
77
|
+
@batch_size = options[:batch_size] || 1_000
|
78
|
+
@flush_continuously = options[:flush_continuously] != false
|
79
|
+
@flush_interval = options[:flush_interval] || 2 # 2 seconds
|
80
|
+
@requests_per_conn = options[:requests_per_conn] || 2_500
|
81
|
+
@msg_queue = FlushableDroppingSizedQueue.new(@batch_size)
|
82
|
+
@request_queue = options[:request_queue] || FlushableDroppingSizedQueue.new(25)
|
83
|
+
@successive_error_count = 0
|
84
|
+
@requests_in_flight = 0
|
85
|
+
end
|
86
|
+
|
87
|
+
# Write a new log line message to the buffer, and flush asynchronously if the
|
88
|
+
# message queue is full. We flush asynchronously because the maximum message batch
|
89
|
+
# size is constricted by the Logtail API. The actual application limit is a multiple
|
90
|
+
# of this. Hence the `@request_queue`.
|
91
|
+
def write(msg)
|
92
|
+
@msg_queue.enq(msg)
|
93
|
+
|
94
|
+
# Lazily start flush threads to ensure threads are alive after forking processes.
|
95
|
+
# If the threads are started during instantiation they will not be copied when
|
96
|
+
# the current process is forked. This is the case with various web servers,
|
97
|
+
# such as phusion passenger.
|
98
|
+
ensure_flush_threads_are_started
|
99
|
+
|
100
|
+
if @msg_queue.full?
|
101
|
+
Logtail::Config.instance.debug { "Flushing HTTP buffer via write" }
|
102
|
+
flush_async
|
103
|
+
end
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
# Flush all log messages in the buffer synchronously. This method will not return
|
108
|
+
# until delivery of the messages has been successful. If you want to flush
|
109
|
+
# asynchronously see {#flush_async}.
|
110
|
+
def flush
|
111
|
+
flush_async
|
112
|
+
wait_on_request_queue
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
# Closes the log device, cleans up, and attempts one last delivery.
|
117
|
+
def close
|
118
|
+
# Kill the flush thread immediately since we are about to flush again.
|
119
|
+
@flush_thread.kill if @flush_thread
|
120
|
+
|
121
|
+
# Flush all remaining messages
|
122
|
+
flush
|
123
|
+
|
124
|
+
# Kill the request queue thread. Flushing ensures that no requests are pending.
|
125
|
+
@request_outlet_thread.kill if @request_outlet_thread
|
126
|
+
end
|
127
|
+
|
128
|
+
def deliver_one(msg)
|
129
|
+
http = build_http
|
130
|
+
|
131
|
+
begin
|
132
|
+
resp = http.start do |conn|
|
133
|
+
req = build_request([msg])
|
134
|
+
@requests_in_flight += 1
|
135
|
+
conn.request(req)
|
136
|
+
end
|
137
|
+
return resp
|
138
|
+
rescue => e
|
139
|
+
Logtail::Config.instance.debug { "error: #{e.message}" }
|
140
|
+
return e
|
141
|
+
ensure
|
142
|
+
http.finish if http.started?
|
143
|
+
@requests_in_flight -= 1
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def verify_delivery!
|
148
|
+
5.times do |i|
|
149
|
+
sleep(2)
|
150
|
+
|
151
|
+
if @last_resp.nil?
|
152
|
+
print "."
|
153
|
+
elsif @last_resp.code == "202"
|
154
|
+
puts "Log delivery successful! View your logs at https://logtail.com"
|
155
|
+
else
|
156
|
+
raise <<-MESSAGE
|
157
|
+
|
158
|
+
Log delivery failed!
|
159
|
+
|
160
|
+
Status: #{@last_resp.code}
|
161
|
+
Body: #{@last_resp.body}
|
162
|
+
|
163
|
+
You can enable internal Logtail debug logging with the following:
|
164
|
+
|
165
|
+
Logtail::Config.instance.debug_logger = ::Logger.new(STDOUT)
|
166
|
+
MESSAGE
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
raise <<-MESSAGE
|
171
|
+
|
172
|
+
Log delivery failed! No request was made.
|
173
|
+
|
174
|
+
You can enable internal debug logging with the following:
|
175
|
+
|
176
|
+
Logtail::Config.instance.debug_logger = ::Logger.new(STDOUT)
|
177
|
+
MESSAGE
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
# This is a convenience method to ensure the flush thread are
|
182
|
+
# started. This is called lazily from {#write} so that we
|
183
|
+
# only start the threads as needed, but it also ensures
|
184
|
+
# threads are started after process forking.
|
185
|
+
def ensure_flush_threads_are_started
|
186
|
+
if @flush_continuously
|
187
|
+
if @request_outlet_thread.nil? || !@request_outlet_thread.alive?
|
188
|
+
@request_outlet_thread = Thread.new { request_outlet }
|
189
|
+
end
|
190
|
+
|
191
|
+
if @flush_thread.nil? || !@flush_thread.alive?
|
192
|
+
@flush_thread = Thread.new { intervaled_flush }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Builds an HTTP request based on the current messages queued.
|
198
|
+
def build_request(msgs)
|
199
|
+
path = '/'
|
200
|
+
req = Net::HTTP::Post.new(path)
|
201
|
+
req['Authorization'] = authorization_payload
|
202
|
+
req['Content-Type'] = CONTENT_TYPE
|
203
|
+
req['User-Agent'] = USER_AGENT
|
204
|
+
req.body = msgs.to_msgpack
|
205
|
+
req
|
206
|
+
end
|
207
|
+
|
208
|
+
# Flushes the message buffer asynchronously. The reason we provide this
|
209
|
+
# method is because the message buffer limit is constricted by the
|
210
|
+
# Logtail API. The application limit is multiples of the buffer limit,
|
211
|
+
# hence the `@request_queue`, allowing us to buffer beyond the Logtail API
|
212
|
+
# imposed limit.
|
213
|
+
def flush_async
|
214
|
+
@last_async_flush = Time.now
|
215
|
+
msgs = @msg_queue.flush
|
216
|
+
return if msgs.empty?
|
217
|
+
|
218
|
+
req = build_request(msgs)
|
219
|
+
if !req.nil?
|
220
|
+
Logtail::Config.instance.debug { "New request placed on queue" }
|
221
|
+
request_attempt = RequestAttempt.new(req)
|
222
|
+
@request_queue.enq(request_attempt)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Waits on the request queue. This is used in {#flush} to ensure
|
227
|
+
# the log data has been delivered before returning.
|
228
|
+
def wait_on_request_queue
|
229
|
+
# Wait 20 seconds
|
230
|
+
40.times do |i|
|
231
|
+
if @request_queue.size == 0 && @requests_in_flight == 0
|
232
|
+
Logtail::Config.instance.debug { "Request queue is empty and no requests are in flight, finish waiting" }
|
233
|
+
return true
|
234
|
+
end
|
235
|
+
Logtail::Config.instance.debug do
|
236
|
+
"Request size #{@request_queue.size}, reqs in-flight #{@requests_in_flight}, " \
|
237
|
+
"continue waiting (iteration #{i + 1})"
|
238
|
+
end
|
239
|
+
sleep 0.5
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Flushes the message queue on an interval. You will notice that {#write} also
|
244
|
+
# flushes the buffer if it is full. This method takes note of this via the
|
245
|
+
# `@last_async_flush` variable as to not flush immediately after a write flush.
|
246
|
+
def intervaled_flush
|
247
|
+
# Wait specified time period before starting
|
248
|
+
sleep @flush_interval
|
249
|
+
|
250
|
+
loop do
|
251
|
+
begin
|
252
|
+
if intervaled_flush_ready?
|
253
|
+
Logtail::Config.instance.debug { "Flushing HTTP buffer via the interval" }
|
254
|
+
flush_async
|
255
|
+
end
|
256
|
+
|
257
|
+
sleep(0.5)
|
258
|
+
rescue Exception => e
|
259
|
+
Logtail::Config.instance.debug { "Intervaled HTTP flush failed: #{e.inspect}\n\n#{e.backtrace}" }
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Determines if the loop in {#intervaled_flush} is ready to be flushed again. It
|
265
|
+
# uses the `@last_async_flush` variable to ensure that a flush does not happen
|
266
|
+
# too rapidly ({#write} also triggers a flush).
|
267
|
+
def intervaled_flush_ready?
|
268
|
+
@last_async_flush.nil? || (Time.now.to_f - @last_async_flush.to_f).abs >= @flush_interval
|
269
|
+
end
|
270
|
+
|
271
|
+
# Builds an `Net::HTTP` object to deliver requests over.
|
272
|
+
def build_http
|
273
|
+
http = Net::HTTP.new(@logtail_host, @logtail_port)
|
274
|
+
http.set_debug_output(Config.instance.debug_logger) if Config.instance.debug_logger
|
275
|
+
if @logtail_scheme == 'https'
|
276
|
+
http.use_ssl = true
|
277
|
+
# Verification on Windows fails despite having a valid certificate.
|
278
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
279
|
+
end
|
280
|
+
http.read_timeout = 30
|
281
|
+
http.ssl_timeout = 10
|
282
|
+
http.open_timeout = 10
|
283
|
+
http
|
284
|
+
end
|
285
|
+
|
286
|
+
# Creates a loop that processes the `@request_queue` on an interval.
|
287
|
+
def request_outlet
|
288
|
+
loop do
|
289
|
+
http = build_http
|
290
|
+
|
291
|
+
begin
|
292
|
+
Logtail::Config.instance.debug { "Starting HTTP connection" }
|
293
|
+
|
294
|
+
http.start do |conn|
|
295
|
+
deliver_requests(conn)
|
296
|
+
end
|
297
|
+
rescue => e
|
298
|
+
Logtail::Config.instance.debug { "#request_outlet error: #{e.message}" }
|
299
|
+
ensure
|
300
|
+
Logtail::Config.instance.debug { "Finishing HTTP connection" }
|
301
|
+
http.finish if http.started?
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# Creates a loop that delivers requests over an open (kept alive) HTTP connection.
|
307
|
+
# If the connection dies, the request is thrown back onto the queue and
|
308
|
+
# the method returns. It is the responsibility of the caller to implement retries
|
309
|
+
# and establish a new connection.
|
310
|
+
def deliver_requests(conn)
|
311
|
+
num_reqs = 0
|
312
|
+
|
313
|
+
while num_reqs < @requests_per_conn
|
314
|
+
if @request_queue.size > 0
|
315
|
+
Logtail::Config.instance.debug { "Waiting on next request, threads waiting: #{@request_queue.size}" }
|
316
|
+
end
|
317
|
+
|
318
|
+
request_attempt = @request_queue.deq
|
319
|
+
|
320
|
+
if request_attempt.nil?
|
321
|
+
sleep(1)
|
322
|
+
else
|
323
|
+
request_attempt.attempted!
|
324
|
+
@requests_in_flight += 1
|
325
|
+
|
326
|
+
begin
|
327
|
+
resp = conn.request(request_attempt.request)
|
328
|
+
rescue => e
|
329
|
+
Logtail::Config.instance.debug { "#deliver_requests error: #{e.message}" }
|
330
|
+
|
331
|
+
# Throw the request back on the queue for a retry if it has been attempted less
|
332
|
+
# than 3 times
|
333
|
+
if request_attempt.attempts < 3
|
334
|
+
Logtail::Config.instance.debug { "Request is being retried, #{request_attempt.attempts} previous attempts" }
|
335
|
+
@request_queue.enq(request_attempt)
|
336
|
+
else
|
337
|
+
Logtail::Config.instance.debug { "Request is being dropped, #{request_attempt.attempts} previous attempts" }
|
338
|
+
end
|
339
|
+
|
340
|
+
return false
|
341
|
+
ensure
|
342
|
+
@requests_in_flight -= 1
|
343
|
+
end
|
344
|
+
|
345
|
+
num_reqs += 1
|
346
|
+
|
347
|
+
@last_resp = resp
|
348
|
+
|
349
|
+
Logtail::Config.instance.debug do
|
350
|
+
if resp.code == "202"
|
351
|
+
"Logs successfully sent! View your logs at https://logtail.com"
|
352
|
+
else
|
353
|
+
"Log delivery failed! status: #{resp.code}, body: #{resp.body}"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
true
|
360
|
+
end
|
361
|
+
|
362
|
+
# Builds the `Authorization` header value for HTTP delivery to the Logtail API.
|
363
|
+
def authorization_payload
|
364
|
+
"Bearer #{@api_key}"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|