logdna 1.4.2 → 1.5.0
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/README.md +6 -2
- data/lib/logdna.rb +6 -8
- data/lib/logdna/client.rb +139 -66
- data/lib/logdna/resources.rb +6 -2
- data/lib/logdna/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32488a458ed8004dcb65531121c286ae1df1b5b3586cf146bb5e973e24e0f559
|
4
|
+
data.tar.gz: 03f487fbff81de61177296ec51849659c66391d7f16a00a2d170e7d03971eaf9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a525a02bc844af91ecc52e324ebc9c1323c26ff0fada9cfb32092b9d57f84b7c8dacd7239e8ffb4d36caca3b89533b0fac577a17e3264643c2678368b7abc10
|
7
|
+
data.tar.gz: fa9721cf9605ab44e08d6222a3d8a222c0c0b3eca7802db673ab7a2c3c8bc840ddb060197a39ddb7b5aa149b93fc26d197c09c7c10a54e84f4579d00f7b8ed61
|
data/README.md
CHANGED
@@ -123,8 +123,12 @@ Instantiates a new instance of the class it is called on. ingestion_key is requi
|
|
123
123
|
|{ :env => STAGING, PRODUCTION .. etc} | Nil |
|
124
124
|
|{ :meta => metadata} | Nil |
|
125
125
|
|{ :endpoint => LogDNA Ingestion URI | 'https://logs.logdna.com/logs/ingest' |
|
126
|
-
|{ :
|
127
|
-
|{ :
|
126
|
+
|{ :flush_interval => Limit to trigger a flush in seconds } | 0.25 seconds |
|
127
|
+
|{ :flush_size => Limit to trigger a flush in bytes } | 2097152 bytes = 2 MiB |
|
128
|
+
|{ :request_size => Upper limit of request in bytes } | 2097152 bytes = 2 MiB |
|
129
|
+
|{ :retry_timeout => Base timeout for retries in seconds } | 0.25 seconds |
|
130
|
+
|{ :retry_max_attempts => Maximum number of retries per request } | 3 attempts |
|
131
|
+
|{ :retry_max_jitter => Maximum amount of jitter to add to each retry request in seconds } | 0.25 seconds |
|
128
132
|
|
129
133
|
Different log level displays log messages in different colors as well.
|
130
134
|
-  "Trace" "Debug" "Info"
|
data/lib/logdna.rb
CHANGED
@@ -3,12 +3,13 @@
|
|
3
3
|
require "logger"
|
4
4
|
require "socket"
|
5
5
|
require "uri"
|
6
|
-
require_relative "logdna/client
|
7
|
-
require_relative "logdna/resources
|
8
|
-
require_relative "logdna/version
|
6
|
+
require_relative "logdna/client"
|
7
|
+
require_relative "logdna/resources"
|
8
|
+
require_relative "logdna/version"
|
9
9
|
|
10
10
|
module Logdna
|
11
11
|
class ValidURLRequired < ArgumentError; end
|
12
|
+
|
12
13
|
class MaxLengthExceeded < ArgumentError; end
|
13
14
|
|
14
15
|
class Ruby < ::Logger
|
@@ -18,11 +19,12 @@ module Logdna
|
|
18
19
|
attr_accessor :app, :env, :meta
|
19
20
|
|
20
21
|
def initialize(key, opts = {})
|
22
|
+
super(nil, nil, nil)
|
21
23
|
@app = opts[:app] || "default"
|
22
24
|
@log_level = opts[:level] || "INFO"
|
23
25
|
@env = opts[:env]
|
24
26
|
@meta = opts[:meta]
|
25
|
-
@internal_logger = Logger.new(
|
27
|
+
@internal_logger = Logger.new($stdout)
|
26
28
|
@internal_logger.level = Logger::DEBUG
|
27
29
|
endpoint = opts[:endpoint] || Resources::ENDPOINT
|
28
30
|
hostname = opts[:hostname] || Socket.gethostname
|
@@ -127,9 +129,5 @@ module Logdna
|
|
127
129
|
def close
|
128
130
|
@client&.exitout
|
129
131
|
end
|
130
|
-
|
131
|
-
at_exit do
|
132
|
-
@client&.exitout
|
133
|
-
end
|
134
132
|
end
|
135
133
|
end
|
data/lib/logdna/client.rb
CHANGED
@@ -1,34 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "etc"
|
3
4
|
require "net/http"
|
4
5
|
require "socket"
|
5
6
|
require "json"
|
6
7
|
require "concurrent"
|
7
8
|
require "date"
|
9
|
+
require "securerandom"
|
8
10
|
|
9
11
|
module Logdna
|
12
|
+
Message = Struct.new(:source, :running_size)
|
13
|
+
|
10
14
|
class Client
|
11
15
|
def initialize(request, uri, opts)
|
12
16
|
@uri = uri
|
13
17
|
|
14
18
|
# NOTE: buffer is in memory
|
15
19
|
@buffer = []
|
16
|
-
@buffer_byte_size = 0
|
17
|
-
|
18
|
-
@side_messages = []
|
19
20
|
|
20
21
|
@lock = Mutex.new
|
21
|
-
|
22
|
-
@flush_limit = opts[:flush_size] || Resources::FLUSH_BYTE_LIMIT
|
22
|
+
|
23
23
|
@flush_interval = opts[:flush_interval] || Resources::FLUSH_INTERVAL
|
24
|
-
@
|
25
|
-
@exception_flag = false
|
24
|
+
@flush_size = opts[:flush_size] || Resources::FLUSH_SIZE
|
26
25
|
|
27
26
|
@request = request
|
27
|
+
@request_size = opts[:request_size] || Resources::REQUEST_SIZE
|
28
|
+
|
28
29
|
@retry_timeout = opts[:retry_timeout] || Resources::RETRY_TIMEOUT
|
30
|
+
@retry_max_jitter = opts[:retry_max_jitter] || Resources::RETRY_MAX_JITTER
|
31
|
+
@retry_max_attempts = opts[:retry_max_attempts] || Resources::RETRY_MAX_ATTEMPTS
|
29
32
|
|
30
|
-
@internal_logger = Logger.new(
|
33
|
+
@internal_logger = Logger.new($stdout)
|
31
34
|
@internal_logger.level = Logger::DEBUG
|
35
|
+
|
36
|
+
@work_thread_pool = Concurrent::FixedThreadPool.new(Etc.nprocessors)
|
37
|
+
# TODO: Expose an option to configure the maximum concurrent requests
|
38
|
+
# Requires the instance-global request to be resolved first
|
39
|
+
@request_thread_pool = Concurrent::FixedThreadPool.new(Resources::MAX_CONCURRENT_REQUESTS)
|
40
|
+
|
41
|
+
@scheduled_flush = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def schedule_flush
|
45
|
+
if @scheduled_flush.nil? || @scheduled_flush.complete?
|
46
|
+
@scheduled_flush = Concurrent::ScheduledTask.execute(@flush_interval) { flush }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def unschedule_flush
|
51
|
+
if !@scheduled_flush.nil?
|
52
|
+
@scheduled_flush.cancel
|
53
|
+
@scheduled_flush = nil
|
54
|
+
end
|
32
55
|
end
|
33
56
|
|
34
57
|
def process_message(msg, opts = {})
|
@@ -44,95 +67,145 @@ module Logdna
|
|
44
67
|
processed_message
|
45
68
|
end
|
46
69
|
|
47
|
-
def
|
48
|
-
|
49
|
-
sleep(@exception_flag ? @retry_timeout : @flush_interval)
|
50
|
-
flush if @flush_scheduled
|
51
|
-
}
|
52
|
-
Thread.new { start_timer.call }
|
70
|
+
def write_to_buffer(msg, opts)
|
71
|
+
Concurrent::Future.execute({ executor: @work_thread_pool }) { write_to_buffer_sync(msg, opts) }
|
53
72
|
end
|
54
73
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
@
|
62
|
-
|
63
|
-
|
64
|
-
if @flush_limit <= @buffer_byte_size
|
65
|
-
flush
|
66
|
-
else
|
67
|
-
schedule_flush
|
74
|
+
def write_to_buffer_sync(msg, opts)
|
75
|
+
processed_message = process_message(msg, opts)
|
76
|
+
message_size = processed_message.to_s.bytesize
|
77
|
+
|
78
|
+
running_size = @lock.synchronize do
|
79
|
+
running_size = message_size
|
80
|
+
if @buffer.any?
|
81
|
+
running_size += @buffer[-1].running_size
|
68
82
|
end
|
83
|
+
@buffer.push(Message.new(processed_message, running_size))
|
84
|
+
|
85
|
+
running_size
|
86
|
+
end
|
87
|
+
|
88
|
+
if running_size >= @flush_size
|
89
|
+
unschedule_flush
|
90
|
+
flush_sync
|
69
91
|
else
|
70
|
-
|
71
|
-
@side_messages.push(process_message(msg, opts))
|
72
|
-
end
|
92
|
+
schedule_flush
|
73
93
|
end
|
74
94
|
end
|
75
95
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
96
|
+
##
|
97
|
+
# Flushes all logs to LogDNA asynchronously
|
98
|
+
def flush(options = {})
|
99
|
+
Concurrent::Future.execute({ executor: @work_thread_pool }) { flush_sync(options) }
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Flushes all logs to LogDNA synchronously
|
104
|
+
def flush_sync(options = {})
|
105
|
+
slices = @lock.synchronize do
|
106
|
+
# Slice the buffer into chunks that try to be no larger than @request_size. Slice points are found with
|
107
|
+
# a binary search thanks to the structure of @buffer. We are working backwards because it's cheaper to
|
108
|
+
# remove from the tail of an array instead of the head
|
109
|
+
slices = []
|
110
|
+
until @buffer.empty?
|
111
|
+
search_size = @buffer[-1].running_size - @request_size
|
112
|
+
if search_size.negative?
|
113
|
+
search_size = 0
|
114
|
+
end
|
115
|
+
|
116
|
+
slice_index = @buffer.bsearch_index { |message| message.running_size >= search_size }
|
117
|
+
slices.push(@buffer.pop(@buffer.length - slice_index).map(&:source))
|
118
|
+
end
|
119
|
+
slices
|
120
|
+
end
|
121
|
+
|
122
|
+
# Remember the chunks are in reverse order, this un-reverses them
|
123
|
+
slices.reverse_each do |slice|
|
124
|
+
if options[:block_on_requests]
|
125
|
+
try_request(slice)
|
126
|
+
else
|
127
|
+
Concurrent::Future.execute({ executor: @request_thread_pool }) { try_request(slice) }
|
128
|
+
end
|
81
129
|
end
|
130
|
+
end
|
82
131
|
|
83
|
-
|
132
|
+
def try_request(slice)
|
133
|
+
body = {
|
84
134
|
e: "ls",
|
85
|
-
ls:
|
135
|
+
ls: slice
|
86
136
|
}.to_json
|
87
137
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
138
|
+
flush_id = "#{SecureRandom.uuid} [#{slice.length} lines]"
|
139
|
+
error_header = "Flush {#{flush_id}} failed."
|
140
|
+
tries = 0
|
141
|
+
loop do
|
142
|
+
tries += 1
|
143
|
+
|
144
|
+
if tries > @retry_max_attempts
|
145
|
+
@internal_logger.debug("Flush {#{flush_id}} exceeded 3 tries. Discarding flush buffer")
|
146
|
+
break
|
93
147
|
end
|
148
|
+
|
149
|
+
if send_request(body, error_header)
|
150
|
+
break
|
151
|
+
end
|
152
|
+
|
153
|
+
sleep(@retry_timeout * (1 << (tries - 1)) + rand(@retry_max_jitter))
|
94
154
|
end
|
155
|
+
end
|
95
156
|
|
157
|
+
def send_request(body, error_header)
|
158
|
+
# TODO: Remove instance-global request object
|
159
|
+
@request.body = body
|
96
160
|
begin
|
97
|
-
|
161
|
+
response = Net::HTTP.start(
|
98
162
|
@uri.hostname,
|
99
163
|
@uri.port,
|
100
164
|
use_ssl: @uri.scheme == "https"
|
101
165
|
) do |http|
|
102
166
|
http.request(@request)
|
103
167
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
168
|
+
|
169
|
+
code = response.code.to_i
|
170
|
+
if [401, 403].include?(code)
|
171
|
+
@internal_logger.debug("#{error_header} Please provide a valid ingestion key. Discarding flush buffer")
|
172
|
+
return true
|
173
|
+
elsif [408, 500, 504].include?(code)
|
174
|
+
# These codes might indicate a temporary ingester issue
|
175
|
+
@internal_logger.debug("#{error_header} The request failed #{response}. Retrying")
|
176
|
+
elsif code == 200
|
177
|
+
return true
|
178
|
+
else
|
179
|
+
@internal_logger.debug("#{error_header} The request failed #{response}. Discarding flush buffer")
|
180
|
+
return true
|
108
181
|
end
|
109
|
-
@exception_flag = false
|
110
182
|
rescue SocketError
|
111
|
-
|
183
|
+
@internal_logger.debug("#{error_header} Network connectivity issue. Retrying")
|
112
184
|
rescue Errno::ECONNREFUSED => e
|
113
|
-
|
185
|
+
@internal_logger.debug("#{error_header} The server is down. #{e.message}. Retrying")
|
114
186
|
rescue Timeout::Error => e
|
115
|
-
|
116
|
-
ensure
|
117
|
-
@buffer.clear
|
187
|
+
@internal_logger.debug("#{error_header} Timeout error occurred. #{e.message}. Retrying")
|
118
188
|
end
|
119
|
-
end
|
120
189
|
|
121
|
-
|
122
|
-
if @lock.try_lock
|
123
|
-
@flush_scheduled = false
|
124
|
-
if @buffer.any? || @side_messages.any?
|
125
|
-
send_request
|
126
|
-
end
|
127
|
-
@lock.unlock
|
128
|
-
else
|
129
|
-
schedule_flush
|
130
|
-
end
|
190
|
+
false
|
131
191
|
end
|
132
192
|
|
133
193
|
def exitout
|
134
|
-
|
135
|
-
@
|
194
|
+
unschedule_flush
|
195
|
+
@work_thread_pool.shutdown
|
196
|
+
if !@work_thread_pool.wait_for_termination(1)
|
197
|
+
@internal_logger.warn("Work thread pool unable to shutdown gracefully. Logs potentially dropped")
|
198
|
+
end
|
199
|
+
@request_thread_pool.shutdown
|
200
|
+
if !@request_thread_pool.wait_for_termination(5)
|
201
|
+
@internal_logger.warn("Request thread pool unable to shutdown gracefully. Logs potentially dropped")
|
202
|
+
end
|
203
|
+
|
204
|
+
if @buffer.any?
|
205
|
+
@internal_logger.debug("Exiting LogDNA logger: Logging remaining messages")
|
206
|
+
flush_sync({ block_on_requests: true })
|
207
|
+
@internal_logger.debug("Finished flushing logs to LogDNA")
|
208
|
+
end
|
136
209
|
end
|
137
210
|
end
|
138
211
|
end
|
data/lib/logdna/resources.rb
CHANGED
@@ -8,10 +8,14 @@ module Resources
|
|
8
8
|
MAX_REQUEST_TIMEOUT = 300_000
|
9
9
|
MAX_LINE_LENGTH = 32_000
|
10
10
|
MAX_INPUT_LENGTH = 80
|
11
|
-
RETRY_TIMEOUT =
|
11
|
+
RETRY_TIMEOUT = 0.25
|
12
|
+
RETRY_MAX_ATTEMPTS = 3
|
13
|
+
RETRY_MAX_JITTER = 0.5
|
12
14
|
FLUSH_INTERVAL = 0.25
|
13
|
-
|
15
|
+
FLUSH_SIZE = 2 * 1_024 * 1_024
|
16
|
+
REQUEST_SIZE = 2 * 1_024 * 1_024
|
14
17
|
ENDPOINT = "https://logs.logdna.com/logs/ingest"
|
15
18
|
MAC_ADDR_CHECK = /^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/.freeze
|
16
19
|
IP_ADDR_CHECK = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.freeze
|
20
|
+
MAX_CONCURRENT_REQUESTS = 1
|
17
21
|
end
|
data/lib/logdna/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logdna
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gun Woo Choi, Derek Zhou, Vilya Levitskiy, Muaz Siddiqui
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -90,7 +90,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
90
|
requirements:
|
91
91
|
- - ">="
|
92
92
|
- !ruby/object:Gem::Version
|
93
|
-
version:
|
93
|
+
version: 2.5.0
|
94
94
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
95
|
requirements:
|
96
96
|
- - ">="
|