syslogstash 2.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +7 -0
- data/.travis.yml +11 -0
- data/Dockerfile +3 -6
- data/README.md +38 -29
- data/bin/syslogstash +3 -55
- data/lib/syslogstash.rb +49 -44
- data/lib/syslogstash/syslog_reader.rb +212 -180
- data/syslogstash.gemspec +27 -26
- metadata +25 -14
- data/Makefile +0 -14
- data/lib/syslogstash/config.rb +0 -118
- data/lib/syslogstash/logstash_writer.rb +0 -202
- data/lib/syslogstash/prometheus_exporter.rb +0 -47
data/syslogstash.gemspec
CHANGED
@@ -1,40 +1,41 @@
|
|
1
1
|
begin
|
2
|
-
|
2
|
+
require 'git-version-bump'
|
3
3
|
rescue LoadError
|
4
|
-
|
4
|
+
nil
|
5
5
|
end
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
|
-
|
8
|
+
s.name = "syslogstash"
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
s.version = GVB.version rescue "0.0.0.1.NOGVB"
|
11
|
+
s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
|
12
12
|
|
13
|
-
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
14
|
|
15
|
-
|
15
|
+
s.summary = "Send messages from syslog UNIX sockets to logstash"
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
s.authors = ["Matt Palmer"]
|
18
|
+
s.email = ["matt.palmer@discourse.org"]
|
19
|
+
s.homepage = "https://github.com/discourse/syslogstash"
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
|
22
|
+
s.executables = ["syslogstash"]
|
23
23
|
|
24
|
-
|
24
|
+
s.required_ruby_version = ">= 2.4.0"
|
25
25
|
|
26
|
-
|
27
|
-
|
26
|
+
s.add_runtime_dependency 'config_skeleton'
|
27
|
+
s.add_runtime_dependency 'frankenstein'
|
28
|
+
s.add_runtime_dependency 'logstash_writer'
|
29
|
+
s.add_runtime_dependency 'rack'
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
s.add_development_dependency 'yard'
|
31
|
+
s.add_development_dependency 'bundler'
|
32
|
+
s.add_development_dependency 'github-release'
|
33
|
+
s.add_development_dependency 'guard-rspec'
|
34
|
+
s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
|
35
|
+
# Needed for guard
|
36
|
+
s.add_development_dependency 'rb-inotify', '~> 0.9'
|
37
|
+
s.add_development_dependency 'redcarpet'
|
38
|
+
s.add_development_dependency 'rspec'
|
39
|
+
s.add_development_dependency 'webmock'
|
40
|
+
s.add_development_dependency 'yard'
|
40
41
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syslogstash
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Palmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-03-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: config_skeleton
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: frankenstein
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -25,7 +39,7 @@ dependencies:
|
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
42
|
+
name: logstash_writer
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - ">="
|
@@ -39,13 +53,13 @@ dependencies:
|
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
56
|
+
name: rack
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - ">="
|
46
60
|
- !ruby/object:Gem::Version
|
47
61
|
version: '0'
|
48
|
-
type: :
|
62
|
+
type: :runtime
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
@@ -53,7 +67,7 @@ dependencies:
|
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: bundler
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - ">="
|
@@ -67,7 +81,7 @@ dependencies:
|
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
84
|
+
name: github-release
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
72
86
|
requirements:
|
73
87
|
- - ">="
|
@@ -192,16 +206,14 @@ executables:
|
|
192
206
|
extensions: []
|
193
207
|
extra_rdoc_files: []
|
194
208
|
files:
|
209
|
+
- ".editorconfig"
|
195
210
|
- ".gitignore"
|
211
|
+
- ".travis.yml"
|
196
212
|
- Dockerfile
|
197
213
|
- LICENCE
|
198
|
-
- Makefile
|
199
214
|
- README.md
|
200
215
|
- bin/syslogstash
|
201
216
|
- lib/syslogstash.rb
|
202
|
-
- lib/syslogstash/config.rb
|
203
|
-
- lib/syslogstash/logstash_writer.rb
|
204
|
-
- lib/syslogstash/prometheus_exporter.rb
|
205
217
|
- lib/syslogstash/syslog_reader.rb
|
206
218
|
- syslogstash.gemspec
|
207
219
|
homepage: https://github.com/discourse/syslogstash
|
@@ -215,15 +227,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
215
227
|
requirements:
|
216
228
|
- - ">="
|
217
229
|
- !ruby/object:Gem::Version
|
218
|
-
version: 2.
|
230
|
+
version: 2.4.0
|
219
231
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
220
232
|
requirements:
|
221
233
|
- - ">="
|
222
234
|
- !ruby/object:Gem::Version
|
223
235
|
version: '0'
|
224
236
|
requirements: []
|
225
|
-
|
226
|
-
rubygems_version: 2.7.7
|
237
|
+
rubygems_version: 3.0.1
|
227
238
|
signing_key:
|
228
239
|
specification_version: 4
|
229
240
|
summary: Send messages from syslog UNIX sockets to logstash
|
data/Makefile
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
IMAGE := discourse/syslogstash
|
2
|
-
TAG := $(shell date -u +%Y%m%d.%H%M%S)
|
3
|
-
|
4
|
-
.PHONY: default
|
5
|
-
default: push
|
6
|
-
@printf "${IMAGE}:${TAG} ready\n"
|
7
|
-
|
8
|
-
.PHONY: push
|
9
|
-
push: build
|
10
|
-
docker push ${IMAGE}:${TAG}
|
11
|
-
|
12
|
-
.PHONY: build
|
13
|
-
build:
|
14
|
-
docker build --build-arg=http_proxy=${http_proxy} -t ${IMAGE}:${TAG} .
|
data/lib/syslogstash/config.rb
DELETED
@@ -1,118 +0,0 @@
|
|
1
|
-
require 'logger'
|
2
|
-
|
3
|
-
class Syslogstash::Config
|
4
|
-
class ConfigurationError < StandardError; end
|
5
|
-
|
6
|
-
# Raised if any problems were found with the config
|
7
|
-
class InvalidEnvironmentError < StandardError; end
|
8
|
-
|
9
|
-
attr_reader :logstash_server,
|
10
|
-
:syslog_socket,
|
11
|
-
:backlog_size,
|
12
|
-
:stats_server,
|
13
|
-
:add_fields,
|
14
|
-
:relay_sockets
|
15
|
-
|
16
|
-
attr_reader :logger
|
17
|
-
|
18
|
-
attr_accessor :relay_to_stdout
|
19
|
-
|
20
|
-
# Create a new syslogstash config based on environment variables.
|
21
|
-
#
|
22
|
-
# Examines the environment passed in, and then creates a new config
|
23
|
-
# object if all is well.
|
24
|
-
#
|
25
|
-
# @param env [Hash] the set of environment variables to use.
|
26
|
-
#
|
27
|
-
# @param logger [Logger] the logger to which all diagnostic and error
|
28
|
-
# data will be sent.
|
29
|
-
#
|
30
|
-
# @raise [ConfigurationError] if any problems are detected with the
|
31
|
-
# environment variables found.
|
32
|
-
#
|
33
|
-
def initialize(env, logger:)
|
34
|
-
@logger = logger
|
35
|
-
|
36
|
-
parse_env(env)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def parse_env(env)
|
42
|
-
@logger.info("config") { "Parsing environment:\n" + env.map { |k, v| "#{k}=#{v.inspect}" }.join("\n") }
|
43
|
-
|
44
|
-
@logstash_server = pluck_string(env, "LOGSTASH_SERVER")
|
45
|
-
@syslog_socket = pluck_string(env, "SYSLOG_SOCKET")
|
46
|
-
@relay_to_stdout = pluck_boolean(env, "RELAY_TO_STDOUT", default: false)
|
47
|
-
@stats_server = pluck_boolean(env, "STATS_SERVER", default: false)
|
48
|
-
@backlog_size = pluck_integer(env, "BACKLOG_SIZE", valid_range: 0..(2**31 - 1), default: 1_000_000)
|
49
|
-
@add_fields = pluck_prefix_list(env, "ADD_FIELD_")
|
50
|
-
@relay_sockets = pluck_path_list(env, "RELAY_SOCKETS", default: [])
|
51
|
-
end
|
52
|
-
|
53
|
-
def pluck_string(env, key, default: nil)
|
54
|
-
maybe_default(env, key, default) { env[key] }
|
55
|
-
end
|
56
|
-
|
57
|
-
def pluck_boolean(env, key, default: nil)
|
58
|
-
maybe_default(env, key, default) do
|
59
|
-
case env[key]
|
60
|
-
when /\A(no|off|0|false)\z/
|
61
|
-
false
|
62
|
-
when /\A(yes|on|1|true)\z/
|
63
|
-
true
|
64
|
-
else
|
65
|
-
raise ConfigurationError,
|
66
|
-
"Value for #{key} (#{env[key].inspect}) is not a valid boolean"
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def pluck_integer(env, key, valid_range: nil, default: nil)
|
72
|
-
maybe_default(env, key, default) do
|
73
|
-
if env[key] !~ /\A\d+\z/
|
74
|
-
raise InvalidEnvironmentError,
|
75
|
-
"Value for #{key} (#{env[key].inspect}) is not an integer"
|
76
|
-
end
|
77
|
-
|
78
|
-
env[key].to_i.tap do |v|
|
79
|
-
unless valid_range.nil? || !valid_range.include?(v)
|
80
|
-
raise InvalidEnvironmentError,
|
81
|
-
"Value for #{key} (#{env[key]}) out of range (must be between #{valid_range.first} and #{valid_range.last} inclusive)"
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def pluck_prefix_list(env, prefix)
|
88
|
-
{}.tap do |list|
|
89
|
-
env.each do |k, v|
|
90
|
-
next unless k.start_with? prefix
|
91
|
-
key = k.sub(prefix, '')
|
92
|
-
list[key] = v
|
93
|
-
end
|
94
|
-
|
95
|
-
@logger.debug("config") { "Prefix list for #{prefix.inspect} is #{list.inspect}" }
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def pluck_path_list(env, key, default: nil)
|
100
|
-
maybe_default(env, key, default) do
|
101
|
-
env[key].split(":")
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def maybe_default(env, key, default)
|
106
|
-
if env[key].nil? || env[key].empty?
|
107
|
-
if default.nil?
|
108
|
-
raise ConfigurationError,
|
109
|
-
"Required environment variable #{key} not specified"
|
110
|
-
else
|
111
|
-
@logger.debug("config") { "Using default value #{default.inspect} for config parameter #{key}" }
|
112
|
-
default
|
113
|
-
end
|
114
|
-
else
|
115
|
-
yield.tap { |v| @logger.debug("config") { "Using plucked value #{v.inspect} for config parameter #{key}" } }
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
@@ -1,202 +0,0 @@
|
|
1
|
-
require 'resolv'
|
2
|
-
require 'ipaddr'
|
3
|
-
|
4
|
-
# Write messages to a logstash server.
|
5
|
-
#
|
6
|
-
class Syslogstash::LogstashWriter
|
7
|
-
Target = Struct.new(:hostname, :port)
|
8
|
-
|
9
|
-
attr_reader :thread
|
10
|
-
|
11
|
-
# Create a new logstash writer.
|
12
|
-
#
|
13
|
-
# Once the object is created, you're ready to give it messages by
|
14
|
-
# calling #send_entry. No messages will actually be *delivered* to
|
15
|
-
# logstash, though, until you call #run.
|
16
|
-
#
|
17
|
-
def initialize(cfg, stats)
|
18
|
-
@server_name, @logger, @backlog, @stats = cfg.logstash_server, cfg.logger, cfg.backlog_size, stats
|
19
|
-
|
20
|
-
@entries = []
|
21
|
-
@entries_mutex = Mutex.new
|
22
|
-
@cs_mutex = Mutex.new
|
23
|
-
end
|
24
|
-
|
25
|
-
# Add an entry to the list of messages to be sent to logstash. Actual
|
26
|
-
# message delivery will happen in a worker thread that is started with
|
27
|
-
# #run.
|
28
|
-
#
|
29
|
-
def send_entry(e)
|
30
|
-
@entries_mutex.synchronize do
|
31
|
-
@entries << { content: e, arrival_timestamp: Time.now }
|
32
|
-
while @entries.length > @backlog
|
33
|
-
@entries.shift
|
34
|
-
@stats.dropped
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
@thread.run if @thread
|
39
|
-
end
|
40
|
-
|
41
|
-
# Start sending messages to logstash servers. This method will return
|
42
|
-
# almost immediately, and actual message sending will occur in a
|
43
|
-
# separate thread.
|
44
|
-
#
|
45
|
-
def run
|
46
|
-
@thread = Thread.new { send_messages }
|
47
|
-
end
|
48
|
-
|
49
|
-
# Cause the writer to disconnect from the currently-active server.
|
50
|
-
#
|
51
|
-
def force_disconnect!
|
52
|
-
@cs_mutex.synchronize do
|
53
|
-
@logger.info("writer") { "Forced disconnect from #{server_id(@current_server) }" }
|
54
|
-
@current_server.close if @current_server
|
55
|
-
@current_server = nil
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
def send_messages
|
62
|
-
loop do
|
63
|
-
if @entries_mutex.synchronize { @entries.empty? }
|
64
|
-
sleep 1
|
65
|
-
else
|
66
|
-
begin
|
67
|
-
entry = @entries_mutex.synchronize { @entries.shift }
|
68
|
-
|
69
|
-
current_server do |s|
|
70
|
-
s.puts entry[:content]
|
71
|
-
@stats.sent(server_id(s), entry[:arrival_timestamp])
|
72
|
-
end
|
73
|
-
|
74
|
-
# If we got here, we sent successfully, so we don't want
|
75
|
-
# to put the entry back on the queue in the ensure block
|
76
|
-
entry = nil
|
77
|
-
rescue StandardError => ex
|
78
|
-
@logger.error("writer") { (["Unhandled exception while writing entry: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ") }
|
79
|
-
ensure
|
80
|
-
@entries_mutex.synchronize { @entries.unshift if entry }
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# *Yield* a TCPSocket connected to the server we currently believe to
|
87
|
-
# be accepting log entries, so that something can send log entries to
|
88
|
-
# it.
|
89
|
-
#
|
90
|
-
# The yielding is very deliberate: it allows us to centralise all
|
91
|
-
# error detection and handling within this one method, and retry
|
92
|
-
# sending just by calling `yield` again when we've connected to
|
93
|
-
# another server.
|
94
|
-
#
|
95
|
-
def current_server
|
96
|
-
# I could handle this more cleanly with recursion, but I don't want
|
97
|
-
# to fill the stack if we have to retry a lot of times
|
98
|
-
done = false
|
99
|
-
|
100
|
-
until done
|
101
|
-
@cs_mutex.synchronize do
|
102
|
-
if @current_server
|
103
|
-
begin
|
104
|
-
@logger.debug("writer") { "Using current server #{server_id(@current_server)}" }
|
105
|
-
yield @current_server
|
106
|
-
done = true
|
107
|
-
rescue SystemCallError => ex
|
108
|
-
# Something went wrong during the send; disconnect from this
|
109
|
-
# server and recycle
|
110
|
-
@logger.debug("writer") { "Error while writing to current server: #{ex.message} (#{ex.class})" }
|
111
|
-
@current_server.close
|
112
|
-
@current_server = nil
|
113
|
-
sleep 0.1
|
114
|
-
end
|
115
|
-
else
|
116
|
-
candidates = resolve_server_name
|
117
|
-
@logger.debug("writer") { "Server candidates: #{candidates.inspect}" }
|
118
|
-
|
119
|
-
begin
|
120
|
-
next_server = candidates.shift
|
121
|
-
|
122
|
-
if next_server
|
123
|
-
@logger.debug("writer") { "Trying to connect to #{next_server.to_s}" }
|
124
|
-
@current_server = TCPSocket.new(next_server.hostname, next_server.port)
|
125
|
-
else
|
126
|
-
@logger.debug("writer") { "Could not connect to any server; pausing before trying again" }
|
127
|
-
@current_server = nil
|
128
|
-
sleep 5
|
129
|
-
end
|
130
|
-
rescue SystemCallError => ex
|
131
|
-
# Connection failed for any number of reasons; try the next one in the list
|
132
|
-
@logger.warn("writer") { "Failed to connect to #{next_server.to_s}: #{ex.message} (#{ex.class})" }
|
133
|
-
sleep 0.1
|
134
|
-
retry
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def server_id(s)
|
142
|
-
pa = s.peeraddr
|
143
|
-
if pa[0] == "AF_INET6"
|
144
|
-
"[#{pa[3]}]:#{pa[1]}"
|
145
|
-
else
|
146
|
-
"#{pa[3]}:#{pa[1]}"
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def resolve_server_name
|
151
|
-
return [static_target] if static_target
|
152
|
-
|
153
|
-
# The IPv6 literal case should have been taken care of by
|
154
|
-
# static_target, so the only two cases we have to deal with
|
155
|
-
# here are specified-port (assume A/AAAA) or no port (assume SRV).
|
156
|
-
if @server_name =~ /:/
|
157
|
-
host, port = @server_name.split(":", 2)
|
158
|
-
addrs = Resolv::DNS.new.getaddresses(host)
|
159
|
-
if addrs.empty?
|
160
|
-
@logger.warn("writer") { "No addresses resolved for server_name #{host.inspect}" }
|
161
|
-
end
|
162
|
-
addrs.sort_by { rand }.map { |a| Target.new(a.to_s, port.to_i) }
|
163
|
-
else
|
164
|
-
# SRV records ftw
|
165
|
-
[].tap do |list|
|
166
|
-
left = Resolv::DNS.new.getresources(@server_name, Resolv::DNS::Resource::IN::SRV)
|
167
|
-
if left.empty?
|
168
|
-
@logger.warn("writer") { "No SRV records found for server_name #{@server_name.inspect}" }
|
169
|
-
end
|
170
|
-
until left.empty?
|
171
|
-
prio = left.map { |rr| rr.priority }.uniq.min
|
172
|
-
candidates = left.select { |rr| rr.priority == prio }
|
173
|
-
left -= candidates
|
174
|
-
candidates.sort_by! { |rr| [rr.weight, rr.target.to_s] }
|
175
|
-
until candidates.empty?
|
176
|
-
selector = rand(candidates.inject(1) { |n, rr| n + rr.weight })
|
177
|
-
chosen = candidates.inject(0) do |n, rr|
|
178
|
-
break rr if n + rr.weight >= selector
|
179
|
-
n + rr.weight
|
180
|
-
end
|
181
|
-
candidates.delete(chosen)
|
182
|
-
list << Target.new(chosen.target.to_s, chosen.port)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
def static_target
|
190
|
-
@static_target ||= begin
|
191
|
-
if @server_name =~ /\A(.*):(\d+)\z/
|
192
|
-
begin
|
193
|
-
Target.new(IPAddr.new($1).to_s, $2.to_i)
|
194
|
-
rescue ArgumentError
|
195
|
-
# Whatever is on the LHS isn't a recognisable address;
|
196
|
-
# assume hostname and continue
|
197
|
-
nil
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
end
|