logstash-output-site24x7 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6bd8186dfce378fb29ed98539b61fde9687671c20b3c0d67e4595fa5c7f5bb04
4
+ data.tar.gz: c331664bfbec18153a6bad79233541df5e13e06fc38e112f1a3434aff2e1f225
5
+ SHA512:
6
+ metadata.gz: 8304ad22beade27cfd7b5490428e7a9d941df97e925596de880edd1ae6f606b3a281e66e7758b216925962f009c6a140cdb7825837ed45ef31d25ed1dadba668
7
+ data.tar.gz: f1d6c23a49abc27ab5d1545e6a19fa1cdc91840e70eb5384613280c52fbe69c423d578dea18f9e6bf77e32bb227b06bb5d8fd6e4a1dd678ef5d59c2b70ba4b89
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.1.0
2
+ - Created output plugin to send logs to Site24x7
data/CONTRIBUTORS ADDED
@@ -0,0 +1,10 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * -
6
+
7
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
8
+ Logstash, and you aren't on the list above and want to be, please let us know
9
+ and we'll make sure you're here. Contributions from folks like you are what make
10
+ open source awesome.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
data/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2021, ZOHO CORPORATION
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Logstash output plugin for Site24x7
2
+
3
+ With Site24x7 plugin for Logstash, you can parse and send logs directly from Logstash, without having to use a separate log shipper.
4
+
5
+ # Installation
6
+
7
+ To add the plugin to your Logstash, use the following command:
8
+
9
+ ```
10
+ logstash-plugin install logstash-output-site24x7
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ **Configure the output plugin**
16
+
17
+ To forward events to Site24x7 add the following code to your Logstash configuration file
18
+ ```
19
+ output {
20
+ site24x7 {
21
+ log_type_config => "<your_log_type_config>"
22
+ }
23
+ }
24
+ ```
25
+ ## Parameters
26
+
27
+ Property | Description | Default Value
28
+ ------------ | -------------|------------
29
+ log_type_config | log_type_config of your configured log type in site24x7 | nil
30
+ max_retry | Number of times to resend failed uploads | 3
31
+ retry_interval | Time interval to sleep initially between retries, exponential step-off | 2 seconds
32
+ ## Need Help?
33
+
34
+ If you need any support please contact us at support@site24x7.com.
@@ -0,0 +1,299 @@
1
+ require "logstash/outputs/base"
2
+ require "logstash/json"
3
+ require "zlib"
4
+ require "date"
5
+ require "json"
6
+ require "base64"
7
+
8
+ class LogStash::Outputs::Site24x7 < LogStash::Outputs::Base
9
+ config_name "site24x7"
10
+
11
+ S247_MAX_RECORD_COUNT = 500
12
+ S247_MAX_RECORD_SIZE = 1000000
13
+ S247_MAX_BATCH_SIZE = 5000000
14
+ S247_LOG_UPLOAD_CHECK_INTERVAL = 600 #10 minutes
15
+ S247_TRUNCATION_SUFFIX = "##TRUNCATED###"
16
+
17
+ default :codec, "json"
18
+
19
+ config :log_type_config,:validate => :string, :required => true
20
+ config :max_retry, :validate => :number, :required => false, :default => 3
21
+ config :retry_interval, :validate => :number, :required => false, :default => 2
22
+
23
+ public
24
+ def register
25
+ init_variables()
26
+ init_http_client(@logtype_config)
27
+ end # def register
28
+
29
+
30
+ public
31
+ def multi_receive(events)
32
+ return if events.empty?
33
+ process_http_events(events)
34
+ end
35
+
36
+ def close
37
+ @s247_http_client.close if @s247_http_client
38
+ end
39
+
40
+ def base64_url_decode(str)
41
+ str += '=' * (4 - str.length.modulo(4))
42
+ Base64.decode64(str.tr('-_','+/'))
43
+ end
44
+
45
+ def init_variables()
46
+ @logtype_config = JSON.parse(base64_url_decode(@log_type_config))
47
+ @s247_custom_regex = if @logtype_config.has_key? 'regex' then Regexp.compile(@logtype_config['regex'].gsub('?P<','?<')) else nil end
48
+ @s247_ignored_fields = if @logtype_config.has_key? 'ignored_fields' then @logtype_config['ignored_fields'] else [] end
49
+ @s247_tz = {'hrs': 0, 'mins': 0} #UTC
50
+ @log_source = Socket.gethostname
51
+ @valid_logtype = true
52
+ @log_upload_allowed = true
53
+ @log_upload_stopped_time = 0
54
+ @s247_datetime_format_string = @logtype_config['dateFormat']
55
+ @s247_datetime_format_string = @s247_datetime_format_string.sub('%f', '%N')
56
+ if !@s247_datetime_format_string.include? 'unix'
57
+ @is_year_present = if @s247_datetime_format_string.include?('%y') || @s247_datetime_format_string.include?('%Y') then true else false end
58
+ if !@is_year_present
59
+ @s247_datetime_format_string = @s247_datetime_format_string+ ' %Y'
60
+ end
61
+ @is_timezone_present = if @s247_datetime_format_string.include? '%z' then true else false end
62
+ if !@is_timezone_present && @logtype_config.has_key?('timezone')
63
+ tz_value = @logtype_config['timezone']
64
+ if tz_value.start_with?('+')
65
+ @s247_tz['hrs'] = Integer('-' + tz_value[1..4])
66
+ @s247_tz['mins'] = Integer('-' + tz_value[3..6])
67
+ elsif tz_value.start_with?('-')
68
+ @s247_tz['hrs'] = Integer('+' + tz_value[1..4])
69
+ @s247_tz['mins'] = Integer('+' + tz_value[3..6])
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ def init_http_client(logtype_config)
76
+ require 'manticore'
77
+ @upload_url = 'https://'+logtype_config['uploadDomain']+'/upload'
78
+ @logger.info("Starting HTTP connection to #{@upload_url}")
79
+ @headers = {"Content-Type" => "application/json", "Content-Encoding" => "gzip", "X-DeviceKey" => logtype_config['apiKey'], "X-LogType" => logtype_config['logType'], "X-StreamMode" => "1", "User-Agent" => "LogStash"}
80
+ @s247_http_client = Manticore::Client.new({})
81
+ end
82
+
83
+ def get_timestamp(datetime_string)
84
+ begin
85
+ # If the date value is in unix format the no need to process the date string
86
+ if @s247_datetime_format_string.include? 'unix'
87
+ return (if @s247_datetime_format_string == 'unix' then datetime_string+'000' else datetime_string end)
88
+ end
89
+ datetime_string += if !@is_year_present then ' '+String(Time.new.year) else '' end
90
+ if !@is_timezone_present && @logtype_config.has_key?('timezone')
91
+ @s247_datetime_format_string += '%z'
92
+ time_zone = String(@s247_tz['hrs'])+':'+String(@s247_tz['mins'])
93
+ datetime_string += if time_zone.start_with?('-') then time_zone else '+'+time_zone end
94
+ end
95
+ datetime_data = DateTime.strptime(datetime_string, @s247_datetime_format_string)
96
+ return Integer(datetime_data.strftime('%Q'))
97
+ rescue
98
+ return 0
99
+ end
100
+ end
101
+
102
+ def parse_lines(lines)
103
+ parsed_lines = []
104
+ log_size = 0
105
+ lines.each do |line|
106
+ if !line.empty?
107
+ begin
108
+ if match = line.match(@s247_custom_regex)
109
+ log_size += line.bytesize
110
+ log_fields = match&.named_captures
111
+ removed_log_size=0
112
+ @s247_ignored_fields.each do |field_name|
113
+ removed_log_size += if log_fields.has_key?field_name then log_fields.delete(field_name).bytesize else 0 end
114
+ end
115
+ formatted_line = {'_zl_timestamp' => get_timestamp(log_fields[@logtype_config['dateField']]), 's247agentuid' => @log_source}
116
+ formatted_line.merge!(log_fields)
117
+ parsed_lines.push(formatted_line)
118
+ log_size -= removed_log_size
119
+ else
120
+ @logger.debug("pattern not matched regex : #{@s247_custom_regex} and received line : #{line}")
121
+ end
122
+ rescue Exception => e
123
+ @logger.error("Exception in parse_line #{e.backtrace}")
124
+ end
125
+ end
126
+ end
127
+ return parsed_lines, log_size
128
+ end
129
+
130
+ def is_filters_matched(formatted_line)
131
+ if @logtype_config.has_key?'filterConfig'
132
+ @logtype_config['filterConfig'].each do |config|
133
+ if formatted_line.has_key?config && (filter_config[config]['match'] ^ (filter_config[config]['values'].include?formatted_line[config]))
134
+ return false
135
+ end
136
+ end
137
+ end
138
+ return true
139
+ end
140
+
141
+ def get_json_value(obj, key, datatype=nil)
142
+ if obj != nil && (obj.has_key?key)
143
+ if datatype and datatype == 'json-object'
144
+ arr_json = []
145
+ child_obj = obj[key]
146
+ if child_obj.class == String
147
+ child_obj = JSON.parse(child_obj.gsub('\\','\\\\'))
148
+ end
149
+ child_obj.each do |key, value|
150
+ arr_json.push({'key' => key, 'value' => String(value)})
151
+ end
152
+ return arr_json
153
+ else
154
+ return (if obj.has_key?key then obj[key] else obj[key.downcase] end)
155
+ end
156
+ elsif key.include?'.'
157
+ parent_key = key[0..key.index('.')-1]
158
+ child_key = key[key.index('.')+1..-1]
159
+ child_obj = obj[if obj.has_key?parent_key then parent_key else parent_key.capitalize() end]
160
+ if child_obj.class == String
161
+ child_obj = JSON.parse(child_obj.replace('\\','\\\\'))
162
+ end
163
+ return get_json_value(child_obj, child_key)
164
+ end
165
+ end
166
+
167
+ def json_log_parser(lines_read)
168
+ log_size = 0
169
+ parsed_lines = []
170
+ lines_read.each do |line|
171
+ if !line.empty?
172
+ current_log_size = 0
173
+ formatted_line = {}
174
+ event_obj = Yajl::Parser.parse(line)
175
+ @logtype_config['jsonPath'].each do |path_obj|
176
+ value = get_json_value(event_obj, path_obj[if path_obj.has_key?'key' then 'key' else 'name' end], path_obj['type'])
177
+ if value
178
+ formatted_line[path_obj['name']] = value
179
+ current_log_size+= String(value).size - (if value.class == Array then value.size*20 else 0 end)
180
+ end
181
+ end
182
+ if is_filters_matched(formatted_line)
183
+ formatted_line['_zl_timestamp'] = get_timestamp(formatted_line[@logtype_config['dateField']])
184
+ formatted_line['s247agentuid'] = @log_source
185
+ parsed_lines.push(formatted_line)
186
+ log_size+=current_log_size
187
+ end
188
+ end
189
+ end
190
+ return parsed_lines, log_size
191
+ end
192
+
193
+ def process_http_events(events)
194
+ batches = batch_http_events(events)
195
+ batches.each do |batched_event|
196
+ formatted_events, log_size = format_http_event_batch(batched_event)
197
+ formatted_events = gzip_compress(formatted_events)
198
+ send_logs_to_s247(formatted_events, log_size)
199
+ end
200
+ end
201
+
202
+ def batch_http_events(encoded_events)
203
+ batches = []
204
+ current_batch = []
205
+ current_batch_size = 0
206
+ encoded_events.each_with_index do |encoded_event, i|
207
+ event_message = encoded_event.to_hash['message']
208
+ current_event_size = event_message.bytesize
209
+ if current_event_size > S247_MAX_RECORD_SIZE
210
+ event_message = event_message[0..(S247_MAX_RECORD_SIZE-DD_TRUNCATION_SUFFIX.length)]+DD_TRUNCATION_SUFFIX
211
+ current_event_size = event_message.bytesize
212
+ end
213
+
214
+ if (i > 0 and i % S247_MAX_RECORD_COUNT == 0) or (current_batch_size + current_event_size > S247_MAX_BATCH_SIZE)
215
+ batches << current_batch
216
+ current_batch = []
217
+ current_batch_size = 0
218
+ end
219
+
220
+ current_batch_size += current_event_size
221
+ current_batch << event_message
222
+ end
223
+ batches << current_batch
224
+ batches
225
+ end
226
+
227
+ def format_http_event_batch(events)
228
+ parsed_lines = []
229
+ log_size = 0
230
+ if @logtype_config.has_key?'jsonPath'
231
+ parsed_lines, log_size = json_log_parser(events)
232
+ else
233
+ parsed_lines, log_size = parse_lines(events)
234
+ end
235
+ return LogStash::Json.dump(parsed_lines), log_size
236
+ end
237
+
238
+ def gzip_compress(payload)
239
+ gz = StringIO.new
240
+ gz.set_encoding("BINARY")
241
+ z = Zlib::GzipWriter.new(gz, 9)
242
+ begin
243
+ z.write(payload)
244
+ ensure
245
+ z.close
246
+ end
247
+ gz.string
248
+ end
249
+
250
+ def send_logs_to_s247(gzipped_parsed_lines, log_size)
251
+ @headers['Log-Size'] = String(log_size)
252
+ sleep_interval = @retry_interval
253
+ begin
254
+ @max_retry.times do |counter|
255
+ need_retry = false
256
+ begin
257
+ response = @s247_http_client.post(@upload_url, body: gzipped_parsed_lines, headers: @headers).call
258
+ resp_headers = response.headers.to_h
259
+ if response.code == 200
260
+ if resp_headers.has_key?'LOG_LICENSE_EXCEEDS' && resp_headers['LOG_LICENSE_EXCEEDS'] == 'True'
261
+ @logger.error("Log license limit exceeds so not able to send logs")
262
+ @log_upload_allowed = false
263
+ @log_upload_stopped_time =Time.now.to_i
264
+ elsif resp_headers.has_key?'BLOCKED_LOGTYPE' && resp_headers['BLOCKED_LOGTYPE'] == 'True'
265
+ @logger.error("Max upload limit reached for log type")
266
+ @log_upload_allowed = false
267
+ @log_upload_stopped_time =Time.now.to_i
268
+ elsif resp_headers.has_key?'INVALID_LOGTYPE' && resp_headers['INVALID_LOGTYPE'] == 'True'
269
+ @logger.error("Log type not present in this account so stopping log collection")
270
+ @valid_logtype = false
271
+ else
272
+ @log_upload_allowed = true
273
+ @logger.debug("Successfully sent logs with size #{gzipped_parsed_lines.size} / #{log_size} to site24x7. Upload Id : #{resp_headers['x-uploadid']}")
274
+ end
275
+ else
276
+ @logger.error("Response Code #{response.code} from Site24x7, so retrying (#{counter + 1}/#{@max_retry})")
277
+ need_retry = true
278
+ end
279
+ rescue StandardError => e
280
+ @logger.error("Error connecting to Site24x7. exception: #{e.backtrace}")
281
+ end
282
+
283
+ if need_retry
284
+ if counter == @max_retry - 1
285
+ @logger.error("Could not send your logs after #{max_retry} tries")
286
+ break
287
+ end
288
+ sleep(sleep_interval)
289
+ sleep_interval *= 2
290
+ else
291
+ return
292
+ end
293
+ end
294
+ rescue Exception => e
295
+ @logger.error("Exception occurred in sendig logs : #{e.backtrace}")
296
+ end
297
+ end
298
+
299
+ end
@@ -0,0 +1,26 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-output-site24x7'
3
+ s.version = '0.1.0'
4
+ s.licenses = ['']
5
+ s.summary = 'Site24x7 output plugin for Logstash event collector'
6
+ s.homepage = 'https://github.com/site24x7/logstash-output-site24x7'
7
+ s.authors = ['Magesh Rajan']
8
+ s.email = 'magesh.rajan@zohocorp.com'
9
+ s.require_paths = ['lib']
10
+
11
+ # Files
12
+ s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
13
+ # Tests
14
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
15
+
16
+ # Special flag to let us know this is actually a logstash plugin
17
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
18
+
19
+ # Gem dependencies
20
+ s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
21
+ s.add_runtime_dependency "logstash-codec-plain"
22
+ s.add_runtime_dependency 'manticore', '>= 0.5.2', '< 1.0.0'
23
+ s.add_runtime_dependency 'logstash-codec-json'
24
+
25
+ s.add_development_dependency "logstash-devutils"
26
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/outputs/site24x7"
4
+ require "logstash/codecs/plain"
5
+ require "logstash/event"
6
+
7
+ describe LogStash::Outputs::Site24x7 do
8
+ let(:sample_event) { LogStash::Event.new }
9
+ let(:output) { LogStash::Outputs::Site24x7.new }
10
+
11
+ before do
12
+ output.register
13
+ end
14
+
15
+ describe "receive message" do
16
+ subject { output.receive(sample_event) }
17
+
18
+ it "returns a string" do
19
+ expect(subject).to eq("Event received")
20
+ end
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-output-site24x7
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Magesh Rajan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash-core-plugin-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logstash-codec-plain
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: manticore
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.5.2
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 1.0.0
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 0.5.2
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: 1.0.0
61
+ - !ruby/object:Gem::Dependency
62
+ name: logstash-codec-json
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: logstash-devutils
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ description:
90
+ email: magesh.rajan@zohocorp.com
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - CHANGELOG.md
96
+ - CONTRIBUTORS
97
+ - Gemfile
98
+ - LICENSE
99
+ - README.md
100
+ - lib/logstash/outputs/site24x7.rb
101
+ - logstash-output-site24x7.gemspec
102
+ - spec/outputs/site24x7_spec.rb
103
+ homepage: https://github.com/site24x7/logstash-output-site24x7
104
+ licenses:
105
+ - ''
106
+ metadata:
107
+ logstash_plugin: 'true'
108
+ logstash_group: output
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.2.3
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Site24x7 output plugin for Logstash event collector
128
+ test_files:
129
+ - spec/outputs/site24x7_spec.rb