logstash-output-sumologic 1.1.4 → 1.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,167 @@
1
+ # encoding: utf-8
2
+ require "net/https"
3
+ require "socket"
4
+ require "thread"
5
+ require "uri"
6
+ require "logstash/outputs/sumologic/common"
7
+ require "logstash/outputs/sumologic/compressor"
8
+ require "logstash/outputs/sumologic/header_builder"
9
+ require "logstash/outputs/sumologic/statistics"
10
+ require "logstash/outputs/sumologic/message_queue"
11
+
12
+ module LogStash; module Outputs; class SumoLogic;
13
+ class Sender
14
+
15
+ include LogStash::Outputs::SumoLogic::Common
16
+ STOP_TAG = "PLUGIN STOPPED"
17
+
18
+ def initialize(client, queue, stats, config)
19
+ @client = client
20
+ @queue = queue
21
+ @stats = stats
22
+ @stopping = Concurrent::AtomicBoolean.new(false)
23
+ @url = config["url"]
24
+ @sender_max = (config["sender_max"] ||= 1) < 1 ? 1 : config["sender_max"]
25
+ @sleep_before_requeue = config["sleep_before_requeue"] ||= 30
26
+ @stats_enabled = config["stats_enabled"] ||= false
27
+
28
+ @tokens = SizedQueue.new(@sender_max)
29
+ @sender_max.times { |t| @tokens << t }
30
+
31
+ @header_builder = LogStash::Outputs::SumoLogic::HeaderBuilder.new(config)
32
+ @headers = @header_builder.build()
33
+ @stats_headers = @header_builder.build_stats()
34
+ @compressor = LogStash::Outputs::SumoLogic::Compressor.new(config)
35
+
36
+ end # def initialize
37
+
38
+ def start()
39
+ @stopping.make_false()
40
+ @sender_t = Thread.new {
41
+ while @stopping.false?
42
+ content = @queue.deq()
43
+ send_request(content)
44
+ end # while
45
+ @queue.drain().map { |content|
46
+ send_request(content)
47
+ }
48
+ log_info "waiting messages sent out..."
49
+ while @tokens.size < @sender_max
50
+ sleep 1
51
+ end # while
52
+ }
53
+ end # def start_sender
54
+
55
+ def stop()
56
+ log_info "shutting down sender..."
57
+ @stopping.make_true()
58
+ @queue.enq(STOP_TAG)
59
+ @sender_t.join
60
+ log_info "sender is fully shutted down"
61
+ end # def stop_sender
62
+
63
+ def connect()
64
+ uri = URI.parse(@url)
65
+ http = Net::HTTP.new(uri.host, uri.port)
66
+ http.use_ssl = @url.downcase().start_with?("https")
67
+ request = Net::HTTP::Get.new(uri.request_uri)
68
+ begin
69
+ res = http.request(request)
70
+ if res.code.to_i != 200
71
+ log_err(
72
+ "Server rejected the request",
73
+ :url => @url,
74
+ :code => res.code
75
+ )
76
+ false
77
+ else
78
+ log_dbg(
79
+ "Server accepted the request",
80
+ :url => @url
81
+ )
82
+ true
83
+ end
84
+ rescue Exception => ex
85
+ log_err(
86
+ "Cannot connect to given url",
87
+ :url => @url,
88
+ :exception => ex
89
+ )
90
+ false
91
+ end
92
+ end # def connect
93
+
94
+ private
95
+
96
+ def send_request(content)
97
+ if content == STOP_TAG
98
+ log_dbg "STOP_TAG is received."
99
+ return
100
+ end
101
+
102
+ token = @tokens.pop()
103
+
104
+ if @stats_enabled && content.start_with?(STATS_TAG)
105
+ body = @compressor.compress(content[STATS_TAG.length..-1])
106
+ headers = @stats_headers
107
+ else
108
+ body = @compressor.compress(content)
109
+ headers = @headers
110
+ end
111
+
112
+ request = @client.send(:background).send(:post, @url, :body => body, :headers => headers)
113
+
114
+ request.on_complete do
115
+ @tokens << token
116
+ end
117
+
118
+ request.on_success do |response|
119
+ @stats.record_response_success(response.code)
120
+ if response.code < 200 || response.code > 299
121
+ log_err(
122
+ "HTTP request rejected(#{response.code})",
123
+ :token => token,
124
+ :code => response.code,
125
+ :headers => headers,
126
+ :contet => content[0..20]
127
+ )
128
+ if response.code == 429 || response.code == 503 || response.code == 504
129
+ requeue_message(content)
130
+ end
131
+ else
132
+ log_dbg(
133
+ "HTTP request accepted",
134
+ :token => token,
135
+ :code => response.code)
136
+ end
137
+ end
138
+
139
+ request.on_failure do |exception|
140
+ @stats.record_response_failure()
141
+ log_err(
142
+ "Error in network transmission",
143
+ :token => token,
144
+ :message => exception.message,
145
+ :class => exception.class.name,
146
+ :backtrace => exception.backtrace
147
+ )
148
+ requeue_message(content)
149
+ end
150
+
151
+ @stats.record_request(content.bytesize, body.bytesize)
152
+ request.call
153
+ end # def send_request
154
+
155
+ def requeue_message(content)
156
+ if @stopping.false? && @sleep_before_requeue >= 0
157
+ log_warn(
158
+ "requeue message",
159
+ :after => @sleep_before_requeue,
160
+ :content => content[0..20])
161
+ Stud.stoppable_sleep(@sleep_before_requeue) { @stopping.true? }
162
+ @queue.enq(content)
163
+ end
164
+ end # def reque_message
165
+
166
+ end
167
+ end; end; end
@@ -0,0 +1,124 @@
1
+ # encoding: utf-8
2
+ require "logstash/outputs/sumologic/common"
3
+
4
+ module LogStash; module Outputs; class SumoLogic;
5
+ class Statistics
6
+
7
+ include LogStash::Outputs::SumoLogic::Common
8
+
9
+ attr_reader :initialize_time
10
+ attr_reader :total_input_events
11
+ attr_reader :total_input_bytes
12
+ attr_reader :total_metrics_datapoints
13
+ attr_reader :total_log_lines
14
+ attr_reader :current_pile_items
15
+ attr_reader :current_pile_bytes
16
+ attr_reader :total_enque_times
17
+ attr_reader :total_enque_bytes
18
+ attr_reader :total_deque_times
19
+ attr_reader :total_deque_bytes
20
+ attr_reader :current_queue_items
21
+ attr_reader :current_queue_bytes
22
+ attr_reader :total_output_requests
23
+ attr_reader :total_output_bytes
24
+ attr_reader :total_output_bytes_compressed
25
+ attr_reader :total_response
26
+ attr_reader :total_response_times
27
+ attr_reader :total_response_success
28
+
29
+ def initialize()
30
+ @initialize_time = Time.now()
31
+ @total_input_events = Concurrent::AtomicFixnum.new
32
+ @total_input_bytes = Concurrent::AtomicFixnum.new
33
+ @total_metrics_datapoints = Concurrent::AtomicFixnum.new
34
+ @total_log_lines = Concurrent::AtomicFixnum.new
35
+ @current_pile_items = Concurrent::AtomicFixnum.new
36
+ @current_pile_bytes = Concurrent::AtomicFixnum.new
37
+ @total_enque_times = Concurrent::AtomicFixnum.new
38
+ @total_enque_bytes = Concurrent::AtomicFixnum.new
39
+ @total_deque_times = Concurrent::AtomicFixnum.new
40
+ @total_deque_bytes = Concurrent::AtomicFixnum.new
41
+ @current_queue_items = Concurrent::AtomicFixnum.new
42
+ @current_queue_bytes = Concurrent::AtomicFixnum.new
43
+ @total_output_requests = Concurrent::AtomicFixnum.new
44
+ @total_output_bytes = Concurrent::AtomicFixnum.new
45
+ @total_output_bytes_compressed = Concurrent::AtomicFixnum.new
46
+ @total_response = Concurrent::Map.new
47
+ @total_response_times = Concurrent::AtomicFixnum.new
48
+ @total_response_success = Concurrent::AtomicFixnum.new
49
+
50
+ end # def initialize
51
+
52
+ def total_response(key)
53
+ @total_response.get(key) ? @total_response.get(key).value : 0
54
+ end
55
+
56
+ def record_multi_input(events, bytesize)
57
+ @total_input_events.update { |v| v + events }
58
+ @total_input_bytes.update { |v| v + bytesize }
59
+ end # def record_multi_input
60
+
61
+ def record_input(entry)
62
+ @total_input_events.increment()
63
+ @total_input_bytes.update { |v| v + entry.bytesize }
64
+ @current_pile_items.increment()
65
+ @current_pile_bytes.update { |v| v + entry.bytesize }
66
+ end # def record_input
67
+
68
+ def record_log_process()
69
+ @total_log_lines.increment()
70
+ end # def record_log_process
71
+
72
+ def record_metrics_process(dps)
73
+ @total_metrics_datapoints.update { |v| v + dps }
74
+ end # def record_metrics_process
75
+
76
+ def record_clear_pile()
77
+ @current_pile_items.value= 0
78
+ @current_pile_bytes.value= 0
79
+ end # def record_pile_clear
80
+
81
+ def record_enque(payload)
82
+ @total_enque_times.increment()
83
+ @total_enque_bytes.update { |v| v + payload.bytesize }
84
+ @current_queue_items.increment()
85
+ @current_queue_bytes.update { |v| v + payload.bytesize }
86
+ end # def record_enque
87
+
88
+ def record_deque(payload)
89
+ @total_deque_times.increment()
90
+ @total_deque_bytes.update { |v| v + payload.bytesize }
91
+ @current_queue_items.decrement()
92
+ @current_queue_bytes.update { |v| v - payload.bytesize }
93
+ end # def record_deque
94
+
95
+ def record_request(size, size_compressed)
96
+ @total_output_requests.increment()
97
+ @total_output_bytes.update { |v| v + size }
98
+ @total_output_bytes_compressed.update { |v| v + size_compressed }
99
+ end # def record_request
100
+
101
+ def record_response_success(code)
102
+ atomic_map_increase(@total_response, code.to_s)
103
+ @total_response_success.increment() if code == 200
104
+ @total_response_times.increment()
105
+ end # def record_response_success
106
+
107
+ def record_response_failure()
108
+ atomic_map_increase(@total_response, "failure")
109
+ end # def record_response_failure
110
+
111
+ def atomic_map_increase(map, key)
112
+ number = map.get(key)
113
+ if number.nil?
114
+ newNumber = Concurrent::AtomicFixnum.new
115
+ number = map.put_if_absent(key, newNumber)
116
+ if number.nil?
117
+ number = newNumber
118
+ end
119
+ end
120
+ number.increment()
121
+ end # def atomic_map_increase
122
+
123
+ end
124
+ end; end; end
@@ -1,25 +1,27 @@
1
1
  Gem::Specification.new do |s|
2
- s.name = "logstash-output-sumologic"
3
- s.version = "1.1.4"
4
- s.licenses = ["Apache-2.0"]
5
- s.summary = "Deliever the log to Sumo Logic cloud service."
6
- s.description = "This gem is a Logstash output plugin to deliver the log or metrics to Sumo Logic cloud service. Go to https://github.com/SumoLogic/logstash-output-sumologic for getting help, reporting issues, etc."
7
- s.authors = ["Sumo Logic"]
8
- s.email = "collection@sumologic.com "
9
- s.homepage = "https://github.com/SumoLogic/logstash-output-sumologic"
10
- s.require_paths = ["lib"]
2
+ s.name = 'logstash-output-sumologic'
3
+ s.version = '1.1.9'
4
+ s.licenses = ['Apache-2.0']
5
+ s.summary = 'Deliever the log to Sumo Logic cloud service.'
6
+ s.description = 'This gem is a Logstash output plugin to deliver the log or metrics to Sumo Logic cloud service. Go to https://github.com/SumoLogic/logstash-output-sumologic for getting help, reporting issues, etc.'
7
+ s.authors = ['Sumo Logic']
8
+ s.email = 'collection@sumologic.com '
9
+ s.homepage = 'https://github.com/SumoLogic/logstash-output-sumologic'
10
+ s.require_paths = ['lib']
11
11
 
12
12
  # Files
13
- s.files = Dir["lib/**/*","spec/**/*","vendor/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT"]
13
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
14
14
  # Tests
15
15
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
16
16
 
17
17
  # Special flag to let us know this is actually a logstash plugin
18
- s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
18
+ s.metadata = { 'logstash_plugin' => 'true', 'logstash_group' => 'output' }
19
19
 
20
20
  # Gem dependencies
21
- s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
22
- s.add_runtime_dependency "logstash-codec-plain", ">= 0"
23
- s.add_runtime_dependency 'logstash-mixin-http_client', ">= 6.0.0", "< 7.0.0"
24
- s.add_development_dependency "logstash-devutils", "~> 1.3", ">= 1.3.1"
21
+ s.add_runtime_dependency 'manticore', '>= 0.5.4', '< 1.0.0'
22
+ s.add_runtime_dependency 'logstash-core-plugin-api', '>= 1.60', '<= 2.99'
23
+ s.add_runtime_dependency 'logstash-codec-plain'
24
+ s.add_runtime_dependency 'logstash-mixin-http_client'
25
+
26
+ s.add_development_dependency 'logstash-devutils'
25
27
  end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/sumologic"
4
+
5
+ describe LogStash::Outputs::SumoLogic::Compressor do
6
+
7
+ context "compress (deflate)" do
8
+ let(:compressor) {
9
+ LogStash::Outputs::SumoLogic::Compressor.new("compress" => true, "compress_encoding" => "deflate")
10
+ }
11
+ specify {
12
+ expect(compressor.compress("abcde").bytesize).to eq(13)
13
+ expect(compressor.compress("aaaaa").bytesize).to eq(11)
14
+ }
15
+ end # context
16
+
17
+ context "compress (gzip)" do
18
+ let(:compressor) {
19
+ LogStash::Outputs::SumoLogic::Compressor.new("compress" => true, "compress_encoding" => "gzip")
20
+ }
21
+ specify {
22
+ expect(compressor.compress("abcde").bytesize).to eq(25)
23
+ expect(compressor.compress("aaaaa").bytesize).to eq(23)
24
+ }
25
+ end # context
26
+
27
+ end # describe
@@ -0,0 +1,197 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/sumologic"
4
+
5
+ describe LogStash::Outputs::SumoLogic::HeaderBuilder do
6
+
7
+ result = {}
8
+
9
+ before :each do
10
+ result = builder.build()
11
+ end
12
+
13
+ context "should build headers by default" do
14
+ let(:builder) { LogStash::Outputs::SumoLogic::HeaderBuilder.new("url" => "http://localhost/1234") }
15
+
16
+ specify {
17
+ expected = {
18
+ "X-Sumo-Client" => "logstash-output-sumologic",
19
+ "X-Sumo-Name" => "logstash-output-sumologic",
20
+ "X-Sumo-Host" => Socket.gethostname,
21
+ "X-Sumo-Category" => "Logstash",
22
+ "Content-Type" => "text/plain"
23
+ }
24
+ expect(result).to eq(expected)
25
+ }
26
+
27
+ end # context
28
+
29
+ context "should override source_category" do
30
+
31
+ let(:builder) {
32
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
33
+ "url" => "http://localhost/1234",
34
+ "source_category" => "my source category")
35
+ }
36
+
37
+ specify {
38
+ expect(result.count).to eq(5)
39
+ expect(result["X-Sumo-Category"]).to eq("my source category")
40
+ }
41
+
42
+ end # context
43
+
44
+ context "should override source_name" do
45
+
46
+ let(:builder) {
47
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
48
+ "url" => "http://localhost/1234",
49
+ "source_name" => "my source name")
50
+ }
51
+
52
+ specify {
53
+ expect(result.count).to eq(5)
54
+ expect(result["X-Sumo-Name"]).to eq("my source name")
55
+ }
56
+
57
+ end # context
58
+
59
+ context "should override source_host" do
60
+
61
+ let(:builder) {
62
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
63
+ "url" => "http://localhost/1234",
64
+ "source_host" => "my source host")
65
+ }
66
+
67
+ specify {
68
+ expect(result.count).to eq(5)
69
+ expect(result["X-Sumo-Host"]).to eq("my source host")
70
+ }
71
+
72
+ end # context
73
+
74
+ context "should hornor extra_headers" do
75
+
76
+ let(:builder) {
77
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
78
+ "url" => "http://localhost/1234",
79
+ "extra_headers" => {
80
+ "foo" => "bar"
81
+ })
82
+ }
83
+
84
+ specify {
85
+ expect(result.count).to eq(6)
86
+ expect(result["foo"]).to eq("bar")
87
+ }
88
+
89
+ end # context
90
+
91
+ context "should hornor extra_headers but never overwrite pre-defined headers" do
92
+
93
+ let(:builder) {
94
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
95
+ "url" => "http://localhost/1234",
96
+ "extra_headers" => {
97
+ "foo" => "bar",
98
+ "X-Sumo-Client" => "a",
99
+ "X-Sumo-Name" => "b",
100
+ "X-Sumo-Host" => "c",
101
+ "X-Sumo-Category" => "d",
102
+ "Content-Type" => "e"
103
+ })
104
+ }
105
+
106
+ specify {
107
+ expected = {
108
+ "foo" => "bar",
109
+ "X-Sumo-Client" => "logstash-output-sumologic",
110
+ "X-Sumo-Name" => "logstash-output-sumologic",
111
+ "X-Sumo-Host" => Socket.gethostname,
112
+ "X-Sumo-Category" => "Logstash",
113
+ "Content-Type" => "text/plain"
114
+ }
115
+ expect(result).to eq(expected)
116
+ }
117
+
118
+ end # context
119
+
120
+ context "should set content type correctly for log payload" do
121
+
122
+ let(:builder) {
123
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new("url" => "http://localhost/1234")
124
+ }
125
+
126
+ specify {
127
+ expect(result["Content-Type"]).to eq("text/plain")
128
+ }
129
+
130
+ end # context
131
+
132
+ context "should set content type correctly for metrics payload (CarbonV2, default)" do
133
+
134
+ let(:builder) {
135
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
136
+ "url" => "http://localhost/1234",
137
+ "fields_as_metrics" => true)
138
+ }
139
+
140
+ specify {
141
+ expect(result["Content-Type"]).to eq("application/vnd.sumologic.carbon2")
142
+ }
143
+
144
+ end # context
145
+
146
+ context "should set content type correctly for metrics payload (Graphite)" do
147
+
148
+ let(:builder) {
149
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new(
150
+ "url" => "http://localhost/1234",
151
+ "metrics_format" => "graphite",
152
+ "fields_as_metrics" => true)
153
+ }
154
+
155
+ specify {
156
+ expect(result["Content-Type"]).to eq("application/vnd.sumologic.graphite")
157
+ }
158
+
159
+ end # context
160
+
161
+ context "should set content encoding correctly for uncompressed payload" do
162
+
163
+ let(:builder) {
164
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new("url" => "http://localhost/1234")
165
+ }
166
+
167
+ specify {
168
+ expect(result["Content-Encoding"]).to be_nil
169
+ }
170
+
171
+ end # context
172
+
173
+ context "should set content encoding correctly for compressed payload (deflate, default)" do
174
+
175
+ let(:builder) {
176
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new("url" => "http://localhost/1234", "compress" => true)
177
+ }
178
+
179
+ specify {
180
+ expect(result["Content-Encoding"]).to eq("deflate")
181
+ }
182
+
183
+ end # context
184
+
185
+ context "should set content encoding correctly for compressed payload (gzip)" do
186
+
187
+ let(:builder) {
188
+ LogStash::Outputs::SumoLogic::HeaderBuilder.new("url" => "http://localhost/1234", "compress" => true, "compress_encoding" => "gzip")
189
+ }
190
+
191
+ specify {
192
+ expect(result["Content-Encoding"]).to eq("gzip")
193
+ }
194
+
195
+ end # context
196
+
197
+ end # describe