logstash-input-multirds 0.0.2 → 0.1.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 +12 -3
- data/lib/logstash/inputs/multirds.rb +142 -152
- data/logstash-input-multirds.gemspec +3 -3
- metadata +12 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93c7bd1bdecf5dcf2dfa932257933cdb60ba75aa
|
4
|
+
data.tar.gz: c8900191f3a7f8b48de91d4f1d3cd88670a70295
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f95d31355e93ba96590b29d1559d5102373df4bcf1f07830e85260dbfc3580d208db6e8a84f51292f5642de56bb4809490c9c17144b45121cd9fed8574be1df2
|
7
|
+
data.tar.gz: a5a5a0327183bac93810f10b6180400c82846fdfe82f803deae5961431590921c0e088c1f07da861901dfd19944e40704e96ad1c239c6ee548bb22d19659a34f
|
data/README.md
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
# Logstash Input Multi-RDS
|
2
2
|
|
3
|
-
|
3
|
+
RDS Postgres instances do not support cloudwatch, logs must be polled using the AWS API. This logstash input plugin polls RDS logs at a configured interval with support for multiple instances of logstash and multiple RDS databases (thus the name: multirds). Locking and marker tracking are done with DynamoDB.
|
4
|
+
|
5
|
+
The plugin will automatically create a DynamoDB table, but if you want to do it manually, the name must match the configured `group_name` and the primary key is `id`.
|
6
|
+
|
7
|
+
## Special thanks
|
8
|
+
|
9
|
+
* This is a fork of logstash input RDS (https://github.com/discourse/logstash-input-rds) though I think I made too many changes to ever merge it back in
|
10
|
+
* Using DynamoDB for locking and marker tracking was stolen from the Kinesis input plugin (https://github.com/logstash-plugins/logstash-input-kinesis) (which we're also using for Cloudwatch logs)
|
11
|
+
* The lock code was taken from the Dynalock gem (https://github.com/tourlane/dynalock) which works fine, but I needed to store extra data on the lock record so I just copied their code
|
12
|
+
|
13
|
+
## Configuration
|
14
|
+
|
4
15
|
```
|
5
16
|
input {
|
6
17
|
multirds {
|
@@ -13,8 +24,6 @@ Inputs RDS logs because Postgres doesn't support cloudwatch. Forked from discour
|
|
13
24
|
}
|
14
25
|
```
|
15
26
|
|
16
|
-
## Configuration
|
17
|
-
|
18
27
|
* `region`: The AWS region for RDS. The AWS SDK reads this info from the usual places, so it's not required, but if you don't set it somewhere the plugin won't run
|
19
28
|
* **required**: false
|
20
29
|
|
@@ -1,136 +1,154 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require "socket"
|
1
|
+
require 'logstash/inputs/base'
|
2
|
+
require 'logstash/namespace'
|
3
|
+
require 'stud/interval'
|
4
|
+
require 'aws-sdk'
|
5
|
+
require 'logstash/inputs/multirds/patch'
|
6
|
+
require 'logstash/plugin_mixins/aws_config'
|
7
|
+
require 'time'
|
8
|
+
require 'socket'
|
10
9
|
Aws.eager_autoload!
|
11
10
|
|
12
11
|
class LogStash::Inputs::Multirds < LogStash::Inputs::Base
|
13
12
|
include LogStash::PluginMixins::AwsConfig::V2
|
14
13
|
|
15
|
-
config_name
|
14
|
+
config_name 'multirds'
|
16
15
|
milestone 1
|
17
|
-
default :codec,
|
18
|
-
|
19
|
-
config :instance_name_pattern, :
|
20
|
-
config :log_file_name_pattern, :
|
21
|
-
config :polling_frequency, :
|
22
|
-
config :group_name, :
|
23
|
-
config :client_id, :
|
24
|
-
|
16
|
+
default :codec, 'plain'
|
17
|
+
|
18
|
+
config :instance_name_pattern, validate: :string, default: '.*'
|
19
|
+
config :log_file_name_pattern, validate: :string, default: '.*'
|
20
|
+
config :polling_frequency, validate: :number, default: 600
|
21
|
+
config :group_name, validate: :string, required: true
|
22
|
+
config :client_id, validate: :string
|
23
|
+
|
25
24
|
def ensure_lock_table(db, table)
|
26
25
|
begin
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
rescue => e
|
61
|
-
|
62
|
-
|
26
|
+
tables = db.list_tables({
|
27
|
+
|
28
|
+
})
|
29
|
+
return true if tables.to_h[:table_names].to_a.include?(table)
|
30
|
+
# TODO: there is a potential race condition here where a table could come back in list_tables but not be in ACTIVE state we should check this better
|
31
|
+
db.create_table(
|
32
|
+
table_name: table,
|
33
|
+
key_schema: [
|
34
|
+
{
|
35
|
+
attribute_name: 'id',
|
36
|
+
key_type: 'HASH'
|
37
|
+
}
|
38
|
+
],
|
39
|
+
attribute_definitions: [
|
40
|
+
{
|
41
|
+
attribute_name: 'id',
|
42
|
+
attribute_type: 'S'
|
43
|
+
}
|
44
|
+
],
|
45
|
+
provisioned_throughput: {
|
46
|
+
read_capacity_units: 10,
|
47
|
+
write_capacity_units: 10
|
48
|
+
}
|
49
|
+
)
|
50
|
+
|
51
|
+
# wait here for the table to be ready
|
52
|
+
(1..10).each do |i|
|
53
|
+
sleep i
|
54
|
+
rsp = db.describe_table(
|
55
|
+
table_name: table
|
56
|
+
)
|
57
|
+
return true if rsp.to_h[:table][:table_status] == 'ACTIVE'
|
58
|
+
end
|
59
|
+
rescue StandardError => e
|
60
|
+
@logger.error "logstash-input-multirds ensure_lock_table exception\n #{e}"
|
61
|
+
return false
|
63
62
|
end
|
64
|
-
|
63
|
+
false
|
65
64
|
end
|
66
|
-
def acquire_lock(db, table, id, lock_owner, expire_time
|
65
|
+
def acquire_lock(db, table, id, lock_owner, expire_time)
|
67
66
|
begin
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
67
|
+
db.update_item(
|
68
|
+
key: {
|
69
|
+
id: id
|
70
|
+
},
|
71
|
+
table_name: table,
|
72
|
+
update_expression: 'SET lock_owner = :lock_owner, expires = :expires',
|
73
|
+
expression_attribute_values: {
|
74
|
+
':lock_owner' => lock_owner,
|
75
|
+
':expires' => Time.now.utc.to_i + expire_time
|
76
|
+
},
|
77
|
+
return_values: 'UPDATED_NEW',
|
78
|
+
# condition_expression: 'attribute_not_exists(lock_owner) OR lock_owner = :lock_owner OR expires < :expires'
|
79
|
+
condition_expression: 'attribute_not_exists(lock_owner) OR expires < :expires'
|
80
|
+
)
|
81
|
+
# TODO: handle condition exception else throw error
|
82
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
83
|
+
return false
|
84
|
+
rescue StandardError => e
|
82
85
|
@logger.error "logstash-input-multirds acquire_lock exception\n #{e}"
|
86
|
+
false
|
83
87
|
end
|
88
|
+
true
|
84
89
|
end
|
90
|
+
|
85
91
|
def get_logfile_list(rds, instance_pattern, logfile_pattern)
|
86
92
|
log_files = []
|
87
93
|
begin
|
88
94
|
dbs = rds.describe_db_instances
|
89
95
|
dbs.to_h[:db_instances].each do |db|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
96
|
+
next unless db[:db_instance_identifier] =~ /#{instance_pattern}/
|
97
|
+
logs = rds.describe_db_log_files(
|
98
|
+
db_instance_identifier: db[:db_instance_identifier]
|
99
|
+
)
|
100
|
+
|
101
|
+
logs.to_h[:describe_db_log_files].each do |log|
|
102
|
+
next unless log[:log_file_name] =~ /#{logfile_pattern}/
|
103
|
+
log[:db_instance_identifier] = db[:db_instance_identifier]
|
104
|
+
log_files.push(log)
|
105
|
+
end
|
100
106
|
end
|
101
|
-
rescue => e
|
107
|
+
rescue StandardError => e
|
102
108
|
@logger.error "logstash-input-multirds get_logfile_list instance_pattern: #{instance_pattern} logfile_pattern:#{logfile_pattern} exception \n#{e}"
|
103
109
|
end
|
104
110
|
log_files
|
105
|
-
end
|
106
|
-
|
107
|
-
|
108
|
-
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_logfile_record(db, id, tablename)
|
114
|
+
out = {}
|
115
|
+
begin
|
116
|
+
res = db.get_item(
|
117
|
+
key: {
|
109
118
|
id: id
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
+
},
|
120
|
+
table_name: tablename
|
121
|
+
)
|
122
|
+
extra_fields = { 'marker' => '0:0' }
|
123
|
+
out = extra_fields.merge(res.item)
|
124
|
+
rescue StandardError => e
|
125
|
+
@logger.error "logstash-input-multirds get_logfile_record exception \n#{e}"
|
126
|
+
end
|
127
|
+
out
|
128
|
+
end
|
129
|
+
|
130
|
+
def set_logfile_record(db, id, tablename, key, value)
|
131
|
+
begin
|
132
|
+
out = db.update_item(
|
133
|
+
key: {
|
119
134
|
id: id
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
135
|
+
},
|
136
|
+
table_name: tablename,
|
137
|
+
update_expression: "SET #{key} = :v",
|
138
|
+
expression_attribute_values: {
|
124
139
|
':v' => value
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
140
|
+
},
|
141
|
+
return_values: 'UPDATED_NEW'
|
142
|
+
)
|
143
|
+
rescue StandardError => e
|
144
|
+
@logger.error "logstash-input-multirds set_logfile_record exception \n#{e}"
|
145
|
+
end
|
146
|
+
out
|
147
|
+
end
|
130
148
|
|
131
149
|
def register
|
132
|
-
@client_id
|
133
|
-
@logger.info
|
150
|
+
@client_id ||= "#{Socket.gethostname}:#{java.util::UUID.randomUUID}"
|
151
|
+
@logger.info 'Registering multi-rds input', instance_name_pattern: @instance_name_pattern, log_file_name_pattern: @log_file_name_pattern, group_name: @group_name, region: @region, client_id: @client_id
|
134
152
|
|
135
153
|
@db = Aws::DynamoDB::Client.new aws_options_hash
|
136
154
|
@rds = Aws::RDS::Client.new aws_options_hash
|
@@ -139,8 +157,8 @@ end
|
|
139
157
|
end
|
140
158
|
|
141
159
|
def run(queue)
|
142
|
-
|
143
|
-
@logger.warn
|
160
|
+
unless @ready
|
161
|
+
@logger.warn 'multi-rds dynamodb lock table not ready, unable to proceed'
|
144
162
|
return false
|
145
163
|
end
|
146
164
|
@thread = Thread.current
|
@@ -149,67 +167,39 @@ end
|
|
149
167
|
|
150
168
|
logs.each do |log|
|
151
169
|
id = "#{log[:db_instance_identifier]}:#{log[:log_file_name]}"
|
152
|
-
lock = acquire_lock @db, @group_name, id, @client_id
|
170
|
+
lock = acquire_lock @db, @group_name, id, @client_id, (@polling_frequency - 1)
|
153
171
|
next unless lock # we won't do anything with the data unless we get a lock on the file
|
154
|
-
|
172
|
+
|
155
173
|
rec = get_logfile_record @db, id, @group_name
|
156
174
|
next unless rec['marker'].split(':')[1].to_i < log[:size].to_i # No new data in the log file so just continue
|
157
175
|
|
158
176
|
# start reading log data at the marker
|
159
177
|
more = true
|
160
178
|
marker = rec[:marker]
|
161
|
-
while more
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
end
|
179
|
+
while more
|
180
|
+
rsp = @rds.download_db_log_file_portion(
|
181
|
+
db_instance_identifier: log[:db_instance_identifier],
|
182
|
+
log_file_name: log[:log_file_name],
|
183
|
+
marker: rec[:marker]
|
184
|
+
)
|
185
|
+
rsp[:log_file_data].lines.each do |line|
|
186
|
+
@codec.decode(line) do |event|
|
187
|
+
decorate event
|
188
|
+
event.set 'rds_instance', log[:db_instance_identifier]
|
189
|
+
event.set 'log_file', log[:log_file_name]
|
190
|
+
queue << event
|
174
191
|
end
|
175
|
-
|
176
|
-
|
192
|
+
end
|
193
|
+
more = rsp[:additional_data_pending]
|
194
|
+
marker = rsp[:marker]
|
177
195
|
end
|
178
196
|
# set the marker back in the lock table
|
179
197
|
set_logfile_record @db, id, @group_name, 'marker', marker
|
180
198
|
end
|
181
199
|
end
|
182
|
-
|
183
200
|
end
|
184
201
|
|
185
202
|
def stop
|
186
203
|
Stud.stop! @thread
|
187
204
|
end
|
188
|
-
|
189
|
-
def filename2datetime(name)
|
190
|
-
parts = name.match /(\d{4})-(\d{2})-(\d{2})-(\d{2})$/
|
191
|
-
Time.utc parts[1], parts[2], parts[3], parts[4]
|
192
|
-
end
|
193
|
-
|
194
|
-
private
|
195
|
-
module SinceDB
|
196
|
-
class File
|
197
|
-
def initialize(file)
|
198
|
-
@db = file
|
199
|
-
end
|
200
|
-
|
201
|
-
def read
|
202
|
-
if ::File.exists?(@db)
|
203
|
-
content = ::File.read(@db).chomp.strip
|
204
|
-
return content.empty? ? Time.new : Time.parse(content)
|
205
|
-
else
|
206
|
-
return Time.new("1999-01-01")
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
def write(time)
|
211
|
-
::File.open(@db, 'w') { |file| file.write time.to_s }
|
212
|
-
end
|
213
|
-
end
|
214
|
-
end
|
215
205
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-input-multirds'
|
3
|
-
s.version = '0.0
|
3
|
+
s.version = '0.1.0'
|
4
4
|
s.summary = 'Ingest RDS log files to Logstash with competing consumers and multiple databases'
|
5
5
|
|
6
6
|
s.authors = ['Robert Labrie']
|
@@ -8,15 +8,15 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.homepage = 'https://github.com/robertlabrie/logstash-input-multi-rds'
|
9
9
|
|
10
10
|
s.require_paths = ['lib']
|
11
|
-
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
|
11
|
+
s.files = Dir['lib/**/*', 'spec/**/*', 'vendor/**/*', '*.gemspec', '*.md', 'CONTRIBUTORS', 'Gemfile', 'LICENSE', 'NOTICE.TXT']
|
12
12
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
13
13
|
|
14
14
|
# Special flag to let us know this is actually a logstash plugin
|
15
15
|
s.metadata = { 'logstash_plugin' => 'true', 'logstash_group' => 'input' }
|
16
16
|
|
17
17
|
# Gem dependencies
|
18
|
-
s.add_runtime_dependency 'logstash-core-plugin-api', '~> 2.0'
|
19
18
|
s.add_runtime_dependency 'logstash-codec-plain'
|
19
|
+
s.add_runtime_dependency 'logstash-core-plugin-api', '~> 2.0'
|
20
20
|
s.add_runtime_dependency 'logstash-mixin-aws'
|
21
21
|
s.add_runtime_dependency 'stud', '>= 0.0.22'
|
22
22
|
s.add_development_dependency 'logstash-devutils', '>= 0.0.16'
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-input-multirds
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Labrie
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-11-
|
11
|
+
date: 2018-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: logstash-
|
14
|
+
name: logstash-codec-plain
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: logstash-
|
28
|
+
name: logstash-core-plugin-api
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
33
|
+
version: '2.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
40
|
+
version: '2.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: logstash-mixin-aws
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|