logdna 1.4.2 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 Colors](https://placehold.it/15/515151/000000?text=+) "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
|
- - ">="
|