syslogstash 2.2.0 → 3.0.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/.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
|