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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f27f4b8bc47949fc01e4cfd9bcb596fdea9143516326e8eb4d129bc8e16b9897
4
- data.tar.gz: 176a5842fcee9deb2acc965f5886ce2c36a71a6b8482d37179e63b12e50bf7b1
3
+ metadata.gz: 24900387c20afb629c78879987193840d830df0973b1a57131306ae01f47a0cf
4
+ data.tar.gz: 2236f3b6bca37894f160688e0cc7432e4877f753548b6eea93fcc270df0eebfc
5
5
  SHA512:
6
- metadata.gz: ad2ebf46bfcc956a0cc88f67bd651856843aed2ca7b4714cc275951a4dd8443d4c674d50dc0b302ed0a50357fdbf4f444acce710b92d02b0cc1b8cec9279d4d1
7
- data.tar.gz: a154a28707c4120e0ea4ff920f76557beaa103d52d6121db3973f6a52d819ee8a83bb3e396ab68f536424c8182e4842756131dbfe595d2a3260c7ed42b38c749
6
+ metadata.gz: e82d6d53e887428310b2c03e6e3d11f5f16f55b62485595f20650b4061c1e59469f9955a03b9c885a1d5f1d843c4687f1b0c8b87b844a8a3dd1892b1d067252d
7
+ data.tar.gz: b9b16ddc853c60d08302af3eeb4965fdd1c2a29ea91f4ca695b6c30765e4c12bca5bf64de3645a094c39004af181f5db394bbd6f8978704b0c250d42c0286770
data/.editorconfig ADDED
@@ -0,0 +1,7 @@
1
+ [*.rb]
2
+ indent_style = space
3
+ indent_size = 2
4
+ end_of_line = lf
5
+ insert_final_newline = true
6
+ charset = utf-8
7
+ trim_trailing_whitespace = true
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+
5
+ rvm:
6
+ - 2.4
7
+ - 2.5
8
+ - 2.6
9
+
10
+ gemfile:
11
+ - Gemfile
data/Dockerfile CHANGED
@@ -1,13 +1,10 @@
1
- FROM ruby:2.3-alpine
1
+ FROM ruby:2.6
2
2
 
3
3
  ARG GEM_VERSION="> 0"
4
4
 
5
5
  COPY pkg/syslogstash-$GEM_VERSION.gem /tmp/syslogstash.gem
6
6
 
7
- RUN apk update \
8
- && apk add build-base \
9
- && gem install /tmp/syslogstash.gem \
10
- && apk del build-base \
11
- && rm -f /var/cache/apk/* /tmp/syslogstash.gem
7
+ RUN gem install /tmp/syslogstash.gem \
8
+ && rm -f /tmp/syslogstash.gem
12
9
 
13
10
  ENTRYPOINT ["/usr/local/bundle/bin/syslogstash"]
data/README.md CHANGED
@@ -49,8 +49,8 @@ least, `syslogstash` needs to know where logstash is (`LOGSTASH_SERVER`),
49
49
  and the socket to listen on for syslog messages (`SYSLOG_SOCKET`). You
50
50
  specify those on the command line, like so:
51
51
 
52
- LOGSTASH_SERVER=logstash-json \
53
- SYSLOG_SOCKET=/dev/log \
52
+ SYSLOGSTASH_LOGSTASH_SERVER=logstash-json \
53
+ SYSLOGSTASH_SYSLOG_SOCKET=/dev/log \
54
54
  syslogstash
55
55
 
56
56
  The full set of environment variables, and their meaning, is described in
@@ -92,6 +92,12 @@ aspects of runtime operation. They are:
92
92
  the logstash server from DNS, then SIGHUP syslogstash to make it switch
93
93
  to another server.
94
94
 
95
+ * **`SIGQUIT`** -- dump thread stacktraces and allocation information to
96
+ `stderr`.
97
+
98
+ * **`SIGINT`** / **`SIGTERM`** -- gracefully terminate. Sending either signal
99
+ twice will cause shutdown to be done somewhat less gracefully.
100
+
95
101
 
96
102
  ## Use with Docker
97
103
 
@@ -139,8 +145,8 @@ All configuration of syslogstash is done by placing values in environment
139
145
  variables. The environment variables that syslogstash recognises are listed
140
146
  below.
141
147
 
142
- * **`LOGSTASH_SERVER`** (required) -- the domain name or address of the
143
- logstash server(s) you wish to send entries to. This can be any of:
148
+ * **`SYSLOGSTASH_LOGSTASH_SERVER`** (required) -- the domain name or address of
149
+ the logstash server(s) you wish to send entries to. This can be any of:
144
150
 
145
151
  * An IPv4 address and port, separated by a colon. For example,
146
152
  `192.0.2.42:5151`. The port *must* be specified.
@@ -161,35 +167,33 @@ below.
161
167
  In all cases, syslogstash respects DNS record TTLs and SRV record
162
168
  weight/priority selection rules. We're not monsters.
163
169
 
164
- * **`SYSLOG_SOCKET`** (required) -- the absolute path to the socket which
165
- syslogstash should create and listen on for syslog format messages.
170
+ * **`SYSLOGSTASH_SYSLOG_SOCKET`** (required) -- the absolute path to the socket
171
+ which syslogstash should create and listen on for syslog format messages.
166
172
 
167
- * **`BACKLOG_SIZE`** (optional; default `"1000000"`) -- the maximum number of
168
- messages to queue if the logstash servers are unavailable. Under normal
169
- operation, syslog messages are immediately relayed to the logstash server
170
- as they are received. However, if no logstash servers are available,
171
- syslogstash will maintain a backlog of up to this many syslog messages,
172
- and will send the entire backlog once a logstash server becomes available
173
- again.
173
+ * **`SYSLOGSTASH_BACKLOG_SIZE`** (optional; default `"1000000"`) -- the maximum
174
+ number of messages to queue if the logstash servers are unavailable. Under
175
+ normal operation, syslog messages are immediately relayed to the logstash
176
+ server as they are received. However, if no logstash servers are available,
177
+ syslogstash will maintain a backlog of up to this many syslog messages, and
178
+ will send the entire backlog once a logstash server becomes available again.
174
179
 
175
180
  In the event that the queue size limit is reached, the oldest messages
176
181
  will be dropped to make way for the new ones.
177
182
 
178
- * **`RELAY_TO_STDOUT`** (optional; default `"no"`) -- if set to a
183
+ * **`SYSLOGSTASH_RELAY_TO_STDOUT`** (optional; default `"no"`) -- if set to a
179
184
  true-ish string (any of `true`, `yes`, `on`, or `1`, compared
180
185
  case-insensitively), then all the syslog messages which are received will
181
186
  be printed to stdout (with the priority/facility prefix removed). This
182
187
  isn't a replacement for a fully-featured syslog server, merely a quick way
183
188
  to dump messages if absolutely required.
184
189
 
185
- * **`STATS_SERVER`** (optional; default `"no"`) -- if set to a true-ish
186
- string (any of `true`, `yes`, `on`, or `1`, compared case-insensitively),
187
- then a Prometheus-compatible statistics exporter will be started,
188
- listening on all interfaces on port 9159.
190
+ * **`SYSLOGSTASH_METRICS_PORT`** (optional; default `""`) -- if set to a
191
+ valid port number (1-65535), a Prometheus-compatible statistics exporter will be
192
+ started, listening on all interfaces on the specified port.
189
193
 
190
- * **`ADD_FIELD_<name>`** (optional) -- if you want to add extra fields to
191
- the entries which are forwarded to logstash, you can specify them here,
192
- for example:
194
+ * **`SYSLOGSTASH_ADD_FIELD_<name>`** (optional) -- if you want to add extra
195
+ fields to the entries which are forwarded to logstash, you can specify them
196
+ here, for example:
193
197
 
194
198
  ADD_FIELD_foo=bar ADD_FIELD_baz=wombat [...] syslogstash
195
199
 
@@ -199,14 +203,19 @@ below.
199
203
  than strings, are not supported. Also, if you specify a field name also
200
204
  used by syslogstash, the results are explicitly undefined.
201
205
 
202
- * **`RELAY_SOCKETS`** (optional; default `""`) -- on the off-chance you want
203
- to feed the syslog messages that syslogstash receives to another
206
+ * **`SYSLOGSTASH_RELAY_SOCKETS`** (optional; default `""`) -- on the off-chance
207
+ you want to feed the syslog messages that syslogstash receives to another
204
208
  syslog-compatible consumer (say, an old-school syslogd) you can specify
205
- additional filenames to use here. Multiple socket filenames can be
206
- specified by separating each file name with a colon. Syslogstash will open
207
- each of the specified sockets, if they exist, and write each received
208
- message to the socket. If the socket does not exist, or the open or write
209
- operations fail, syslogstash **will not** retry.
209
+ additional filenames to use here. Multiple socket filenames can be specified
210
+ by separating each file name with a colon. Syslogstash will open each of the
211
+ specified sockets, if they exist, and write each received message to the
212
+ socket. If the socket does not exist, or the open or write operations fail,
213
+ syslogstash **will not** retry.
214
+
215
+ * **`SYSLOGSTASH_DROP_REGEX`** (optional) -- Regular expression to run on
216
+ input, if it matches then the message will be dropped and not sent to
217
+ logstash. However, it *will* still be sent to stdout and any relay sockets,
218
+ if those options are enabled.
210
219
 
211
220
 
212
221
  # Contributing
@@ -222,7 +231,7 @@ request](https://github.com/discourse/syslogstash/pulls].
222
231
  Unless otherwise stated, everything in this repo is covered by the following
223
232
  copyright notice:
224
233
 
225
- Copyright (C) 2015, 2018 Civilized Discourse Construction Kit Inc.
234
+ Copyright (C) 2015, 2018, 2019 Civilized Discourse Construction Kit Inc.
226
235
 
227
236
  This program is free software: you can redistribute it and/or modify it
228
237
  under the terms of the GNU General Public License version 3, as
data/bin/syslogstash CHANGED
@@ -1,62 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'syslogstash'
4
- require 'logger'
5
-
6
- logger = Logger.new($stderr)
7
- logger.formatter = ->(s, t, p, m) { "#{s[0]} [#{p}] #{m}\n" }
8
- logger.level = Logger.const_get(ENV['SYSLOGSTASH_LOG_LEVEL'] || "INFO")
9
4
 
10
5
  begin
11
- cfg = Syslogstash::Config.new(ENV, logger: logger)
12
- rescue Syslogstash::Config::ConfigurationError => ex
13
- $stderr.puts "Error in configuration: #{ex.message}"
6
+ Syslogstash.new(ENV).start
7
+ rescue ServiceSkeleton::Error::InvalidEnvironmentError => ex
8
+ $stderr.puts "Configuration error: #{ex.message}"
14
9
  exit 1
15
10
  end
16
-
17
- syslogstash = Syslogstash.new(cfg)
18
-
19
- sig_r, sig_w = IO.pipe
20
-
21
- Signal.trap("USR1") do
22
- sig_w.print '1'
23
- end
24
- Signal.trap("USR2") do
25
- sig_w.print '2'
26
- end
27
- Signal.trap("URG") do
28
- sig_w.print 'U'
29
- end
30
- Signal.trap("HUP") do
31
- sig_w.print 'H'
32
- end
33
-
34
- Thread.new do
35
- loop do
36
- begin
37
- c = sig_r.getc
38
- if c == '1'
39
- logger.level -= 1 unless logger.level == Logger::DEBUG
40
- logger.info("SignalHandler") { "Received SIGUSR1; log level is now #{Logger::SEV_LABEL[logger.level]}." }
41
- elsif c == '2'
42
- logger.level += 1 unless logger.level == Logger::ERROR
43
- logger.info("SignalHandler") { "Received SIGUSR2; log level is now #{Logger::SEV_LABEL[logger.level]}." }
44
- elsif c == 'U'
45
- cfg.relay_to_stdout = !cfg.relay_to_stdout
46
- logger.info("SignalHandler") { "Received SIGURG; Relaying to stdout is now #{cfg.relay_to_stdout ? "enabled" : "disabled"}" }
47
- elsif c== 'H'
48
- logger.info("SignalHandler") { "Received SIGHUP" }
49
- syslogstash.force_disconnect!
50
- else
51
- logger.error("SignalHandler") { "Got an unrecognised character from signal pipe: #{c.inspect}" }
52
- end
53
- rescue StandardError => ex
54
- logger.error("SignalHandler") { (["Exception raised: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ") }
55
- rescue Exception => ex
56
- $stderr.puts (["Fatal exception in syslogstash signal handler: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ")
57
- exit 42
58
- end
59
- end
60
- end
61
-
62
- syslogstash.run
data/lib/syslogstash.rb CHANGED
@@ -2,54 +2,59 @@ require 'uri'
2
2
  require 'socket'
3
3
  require 'json'
4
4
  require 'thwait'
5
+ require 'logstash_writer'
6
+ require 'service_skeleton'
5
7
 
6
8
  # Read syslog messages from one or more sockets, and send it to a logstash
7
9
  # server.
8
10
  #
9
- class Syslogstash
10
- def initialize(cfg)
11
- @cfg = cfg
12
- @stats = PrometheusExporter.new(cfg)
13
- @writer = LogstashWriter.new(cfg, @stats)
14
- @reader = SyslogReader.new(cfg, @writer, @stats)
15
- @logger = cfg.logger
16
- end
17
-
18
- def run
19
- if @cfg.stats_server
20
- @logger.debug("main") { "Running stats server" }
21
- @stats.run
22
- end
23
-
24
- @writer.run
25
- @reader.run
26
-
27
- dead_thread = ThreadsWait.new(@reader.thread, @writer.thread).next_wait
28
-
29
- if dead_thread == @writer.thread
30
- @logger.error("main") { "Writer thread crashed." }
31
- elsif dead_thread == @reader.thread
32
- @logger.error("main") { "Reader thread crashed." }
33
- else
34
- @logger.fatal("main") { "ThreadsWait#next_wait returned unexpected value #{dead_thread.inspect}" }
35
- exit 1
36
- end
37
-
38
- begin
39
- dead_thread.join
40
- rescue Exception => ex
41
- @logger.error("main") { (["Exception in crashed thread was: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ") }
42
- end
43
-
44
- exit 1
45
- end
46
-
47
- def force_disconnect!
48
- @writer.force_disconnect!
49
- end
11
+ class Syslogstash < ServiceSkeleton
12
+ string :SYSLOGSTASH_LOGSTASH_SERVER
13
+ string :SYSLOGSTASH_SYSLOG_SOCKET
14
+ string :SYSLOGSTASH_RELAY_TO_STDOUT, default: false
15
+ string :SYSLOGSTASH_DROP_REGEX, default: nil
16
+ integer :SYSLOGSTASH_BACKLOG_SIZE, default: 1_000_000, range: 0..(2**31-1)
17
+ path_list :SYSLOGSTASH_RELAY_SOCKETS, default: []
18
+ kv_list :SYSLOGSTASH_ADD_FIELDS, default: {}, key_pattern: /\ASYSLOGSTASH_ADD_FIELD_(.*)\z/
19
+
20
+ def initialize(*_)
21
+ super
22
+
23
+ hook_signal("URG") do
24
+ config.relay_to_stdout = !config.relay_to_stdout
25
+ logger.info(logloc) { "SIGURG received; relay_to_stdout is now #{config.relay_to_stdout.inspect}" }
26
+ end
27
+
28
+ @shutdown_reader, @shutdown_writer = IO.pipe
29
+
30
+ metrics.counter(:syslogstash_messages_received_total, "The number of syslog messages received from the log socket")
31
+ metrics.counter(:syslogstash_messages_sent_total, "The number of logstash messages sent to each logstash server")
32
+ metrics.gauge(:syslogstash_last_relayed_message_timestamp, "When the last message that was successfully relayed to logstash was originally received")
33
+ metrics.gauge(:syslogstash_queue_size, "How many messages are currently in the queue to be sent")
34
+ metrics.counter(:syslogstash_dropped_total, "Number of log entries that were not forwarded due to matching the drop regex")
35
+
36
+ @writer = LogstashWriter.new(server_name: config.logstash_server, backlog: config.backlog_size, logger: config.logger, metrics_registry: metrics)
37
+ @reader = SyslogReader.new(config, @writer, metrics)
38
+ end
39
+
40
+ def run
41
+ @writer.run
42
+ @reader.start!
43
+
44
+ @shutdown_reader.getc
45
+ @shutdown_reader.close
46
+ end
47
+
48
+ def shutdown
49
+ @reader.stop!
50
+ @writer.stop
51
+
52
+ @shutdown_writer.close
53
+ end
54
+
55
+ def force_disconnect!
56
+ @writer.force_disconnect!
57
+ end
50
58
  end
51
59
 
52
- require_relative 'syslogstash/config'
53
60
  require_relative 'syslogstash/syslog_reader'
54
- require_relative 'syslogstash/logstash_writer'
55
- require_relative 'syslogstash/prometheus_exporter'
@@ -1,184 +1,216 @@
1
1
  # A single socket reader.
2
2
  #
3
3
  class Syslogstash::SyslogReader
4
- attr_reader :thread
5
-
6
- def initialize(cfg, logstash, stats)
7
- @file, @logstash, @stats = cfg.syslog_socket, logstash, stats
8
-
9
- @add_fields = cfg.add_fields
10
- @relay_to = cfg.relay_sockets
11
- @cfg = cfg
12
- @logger = cfg.logger
13
- end
14
-
15
- # Start reading from the socket file, parsing entries, and flinging
16
- # them at logstash. This method will return, with the operation
17
- # continuing in a separate thread.
18
- #
19
- def run
20
- @logger.debug("reader") { "#run called" }
21
-
22
- begin
23
- socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
24
- socket.bind(Socket.pack_sockaddr_un(@file))
25
- File.chmod(0666, @file)
26
- rescue Errno::EEXIST, Errno::EADDRINUSE
27
- @logger.info("reader") { "socket file #{@file} already exists; deleting" }
28
- File.unlink(@file) rescue nil
29
- retry
30
- rescue StandardError => ex
31
- raise ex.class, "Error while trying to bind to #{@file}: #{ex.message}", ex.backtrace
32
- end
33
-
34
- @thread = Thread.new do
35
- begin
36
- loop do
37
- msg = socket.recvmsg
38
- @logger.debug("reader") { "Message received: #{msg.inspect}" }
39
- @stats.received(@file)
40
- relay_message msg.first
41
- process_message msg.first.chomp
42
- end
43
- ensure
44
- socket.close
45
- @logger.debug("reader") { "removing socket file #{@file}" }
46
- File.unlink(@file) rescue nil
47
- end
48
- end
49
- end
50
-
51
- private
52
-
53
- def process_message(msg)
54
- if msg =~ /^<(\d+)>(\w{3} [ 0-9]{2} [0-9:]{8}) (.*)$/
55
- flags = $1.to_i
56
- timestamp = $2
57
- content = $3
58
-
59
- # Lo! the many ways that syslog messages can be formatted
60
- hostname, program, pid, message = case content
61
- # the gold standard: hostname, program name with optional PID
62
- when /^([a-zA-Z0-9._-]*[^:]) (\S+?)(\[(\d+)\])?: (.*)$/
63
- [$1, $2, $4, $5]
64
- # hostname, no program name
65
- when /^([a-zA-Z0-9._-]+) (\S+[^:] .*)$/
66
- [$1, nil, nil, $2]
67
- # program name, no hostname (yeah, you heard me, non-RFC compliant!)
68
- when /^(\S+?)(\[(\d+)\])?: (.*)$/
69
- [nil, $1, $3, $4]
70
- else
71
- # I have NFI
72
- [nil, nil, nil, content]
73
- end
74
-
75
- severity = flags % 8
76
- facility = flags / 8
77
-
78
- log_entry = log_entry(
79
- syslog_timestamp: timestamp,
80
- severity: severity,
81
- facility: facility,
82
- hostname: hostname,
83
- program: program,
84
- pid: pid.nil? ? nil : pid.to_i,
85
- message: message,
86
- ).to_json
87
-
88
- @logstash.send_entry(log_entry)
89
- else
90
- @logger.warn("reader") { "Unparseable message: #{msg.inspect}" }
91
- end
92
- end
93
-
94
- def log_entry(h)
95
- {}.tap do |e|
96
- e['@version'] = '1'
97
- e['@timestamp'] = Time.now.utc.strftime("%FT%T.%LZ")
98
-
99
- h[:facility_name] = FACILITIES[h[:facility]]
100
- h[:severity_name] = SEVERITIES[h[:severity]]
101
-
102
- e.merge!(h.delete_if { |k,v| v.nil? })
103
- e.merge!(@add_fields)
104
-
105
- @logger.debug("reader") { "Complete log entry is: #{e.inspect}" }
106
- end
107
- end
108
-
109
- def relay_message(msg)
110
- @currently_failed ||= {}
111
-
112
- if @cfg.relay_to_stdout
113
- # This one's easy
114
- puts msg.sub(/\A<\d+>/, '')
115
- $stdout.flush
116
- end
117
-
118
- @relay_to.each do |f|
119
- s = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
120
- begin
121
- s.connect(Socket.pack_sockaddr_un(f))
122
- rescue Errno::ENOENT
123
- # Socket doesn't exist; we don't care enough about this to bother
124
- # reporting it. People will figure it out themselves soon enough.
125
- rescue StandardError => ex
126
- unless @currently_failed[f]
127
- @logger.warn("reader") { "Error while connecting to relay socket #{f}: #{ex.message} (#{ex.class})" }
128
- @currently_failed[f] = true
129
- end
130
- next
131
- end
132
-
133
- begin
134
- # We really, *really* don't want to block the world just because
135
- # whoever's on the other end of the relay socket can't process
136
- # messages quick enough.
137
- s.sendmsg_nonblock(msg)
138
- if @currently_failed[f]
139
- @logger.info("reader") { "Error on socket #{f} has cleared; messages are being delivered again" }
140
- @currently_failed[f] = false
141
- end
142
- rescue Errno::ENOTCONN
143
- unless @currently_failed[f]
144
- @logger.debug("reader") { "Nothing is listening on socket #{f}" }
145
- @currently_failed[f] = true
146
- end
147
- rescue IO::EAGAINWaitWritable
148
- unless @currently_failed[f]
149
- @logger.warn("reader") { "Socket #{f} is currently backlogged; messages to this socket are now being discarded undelivered" }
150
- @currently_failed[f] = true
151
- end
152
- rescue StandardError => ex
153
- @logger.warn("reader") { (["Failed to relay message to socket #{f} from #{@file}: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ") }
154
- end
155
- end
156
- end
157
-
158
- FACILITIES = %w{
159
- kern
160
- user
161
- mail
162
- daemon
163
- auth
164
- syslog
165
- lpr
166
- news
167
- uucp
168
- cron
169
- authpriv
170
- ftp
171
- local0 local1 local2 local3 local4 local5 local6 local7
172
- }
173
-
174
- SEVERITIES = %w{
175
- emerg
176
- alert
177
- crit
178
- err
179
- warning
180
- notice
181
- info
182
- debug
183
- }
4
+ include ServiceSkeleton::BackgroundWorker
5
+
6
+ def initialize(config, logstash, metrics)
7
+ @config, @logstash, @metrics = config, logstash, metrics
8
+
9
+ @logger = config.logger
10
+
11
+ @shutdown_reader, @shutdown_writer = IO.pipe
12
+
13
+ super
14
+ end
15
+
16
+ # Start reading from the socket file, parsing entries, and flinging
17
+ # them at logstash.
18
+ #
19
+ def start
20
+ config.logger.debug(logloc) { "off we go!" }
21
+
22
+ begin
23
+ socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
24
+ socket.bind(Socket.pack_sockaddr_un(config.syslog_socket))
25
+ File.chmod(0666, config.syslog_socket)
26
+ rescue Errno::EEXIST, Errno::EADDRINUSE
27
+ config.logger.info(logloc) { "socket file #{config.syslog_socket} already exists; deleting" }
28
+ File.unlink(config.syslog_socket) rescue nil
29
+ retry
30
+ rescue StandardError => ex
31
+ raise ex.class, "Error while trying to bind to #{config.syslog_socket}: #{ex.message}", ex.backtrace
32
+ end
33
+
34
+ begin
35
+ loop do
36
+ IO.select([@shutdown_reader, socket]).first.each do |fd|
37
+ if fd == socket
38
+ begin
39
+ msg = socket.recvmsg_nonblock
40
+ rescue IO::WaitWritable
41
+ config.logger.debug(logloc) { "select said a message was waiting, but it wasn't. o.O" }
42
+ else
43
+ config.logger.debug(logloc) { "Message received: #{msg.inspect}" }
44
+ @metrics.messages_received_total.increment(socket_path: config.syslog_socket)
45
+ @metrics.queue_size.increment({})
46
+ relay_message msg.first
47
+ process_message msg.first.chomp
48
+ end
49
+ elsif fd == @shutdown_reader
50
+ @shutdown_reader.close
51
+ config.logger.debug(logloc) { "Tripped over shutdown reader" }
52
+ break
53
+ end
54
+ end
55
+ end
56
+ ensure
57
+ socket.close
58
+ config.logger.debug(logloc) { "removing socket file #{config.syslog_socket}" }
59
+ File.unlink(config.syslog_socket) rescue nil
60
+ end
61
+ end
62
+
63
+ def shutdown
64
+ @shutdown_writer.close
65
+ end
66
+
67
+ private
68
+
69
+ attr_reader :config, :logger
70
+
71
+ def process_message(msg)
72
+ if msg =~ /^<(\d+)>(\w{3} [ 0-9]{2} [0-9:]{8}) (.*)$/
73
+ flags = $1.to_i
74
+ timestamp = $2
75
+ content = $3
76
+
77
+ # Lo! the many ways that syslog messages can be formatted
78
+ hostname, program, pid, message = case content
79
+ # the gold standard: hostname, program name with optional PID
80
+ when /^([a-zA-Z0-9._-]*[^:]) (\S+?)(\[(\d+)\])?: (.*)$/
81
+ [$1, $2, $4, $5]
82
+ # hostname, no program name
83
+ when /^([a-zA-Z0-9._-]+) (\S+[^:] .*)$/
84
+ [$1, nil, nil, $2]
85
+ # program name, no hostname (yeah, you heard me, non-RFC compliant!)
86
+ when /^(\S+?)(\[(\d+)\])?: (.*)$/
87
+ [nil, $1, $3, $4]
88
+ else
89
+ # I have NFI
90
+ [nil, nil, nil, content]
91
+ end
92
+
93
+ if config.drop_regex && message && message.match?(config.drop_regex)
94
+ @metrics.dropped_total.increment({})
95
+ config.logger.debug(logloc) { "dropping message #{message}" }
96
+ return
97
+ end
98
+
99
+ severity = flags % 8
100
+ facility = flags / 8
101
+
102
+ log_entry = log_entry(
103
+ syslog_timestamp: timestamp,
104
+ severity: severity,
105
+ facility: facility,
106
+ hostname: hostname,
107
+ program: program,
108
+ pid: pid.nil? ? nil : pid.to_i,
109
+ message: message,
110
+ )
111
+
112
+ @logstash.send_event(log_entry)
113
+ else
114
+ config.logger.warn(logloc) { "Unparseable message: #{msg.inspect}" }
115
+ end
116
+ end
117
+
118
+ def log_entry(h)
119
+ {}.tap do |e|
120
+ e['@version'] = '1'
121
+ e['@timestamp'] = Time.now.utc.strftime("%FT%T.%LZ")
122
+
123
+ h[:facility_name] = FACILITIES[h[:facility]]
124
+ h[:severity_name] = SEVERITIES[h[:severity]]
125
+
126
+ e.merge!(h.delete_if { |k,v| v.nil? })
127
+ e.merge!(config.add_fields)
128
+
129
+ config.logger.debug(logloc) { "Complete log entry is: #{e.inspect}" }
130
+ end
131
+ end
132
+
133
+ def relay_message(msg)
134
+ @currently_failed ||= {}
135
+
136
+ if config.relay_to_stdout
137
+ # This one's easy
138
+ puts msg.sub(/\A<\d+>/, '')
139
+ $stdout.flush
140
+ end
141
+
142
+ config.relay_sockets.each do |f|
143
+ relay_to_socket(f)
144
+ end
145
+ end
146
+
147
+ def relay_to_socket(f)
148
+ begin
149
+ s = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
150
+ begin
151
+ s.connect(Socket.pack_sockaddr_un(f))
152
+ rescue Errno::ENOENT
153
+ # Socket doesn't exist; we don't care enough about this to bother
154
+ # reporting it. People will figure it out themselves soon enough.
155
+ rescue StandardError => ex
156
+ unless @currently_failed[f]
157
+ config.logger.warn(logloc) { "Error while connecting to relay socket #{f}: #{ex.message} (#{ex.class})" }
158
+ @currently_failed[f] = true
159
+ end
160
+ return
161
+ end
162
+
163
+ begin
164
+ # We really, *really* don't want to block the world just because
165
+ # whoever's on the other end of the relay socket can't process
166
+ # messages quick enough.
167
+ s.sendmsg_nonblock(msg)
168
+ if @currently_failed[f]
169
+ config.logger.info(logloc) { "Error on socket #{f} has cleared; messages are being delivered again" }
170
+ @currently_failed[f] = false
171
+ end
172
+ rescue Errno::ENOTCONN
173
+ unless @currently_failed[f]
174
+ config.logger.debug(logloc) { "Nothing is listening on socket #{f}" }
175
+ @currently_failed[f] = true
176
+ end
177
+ rescue IO::EAGAINWaitWritable
178
+ unless @currently_failed[f]
179
+ config.logger.warn(logloc) { "Socket #{f} is currently backlogged; messages to this socket are now being discarded undelivered" }
180
+ @currently_failed[f] = true
181
+ end
182
+ rescue StandardError => ex
183
+ config.logger.warn(logloc) { (["Failed to relay message to socket #{f} from #{config.syslog_socket}: #{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ") }
184
+ end
185
+ ensure
186
+ s.close
187
+ end
188
+ end
189
+
190
+ FACILITIES = %w{
191
+ kern
192
+ user
193
+ mail
194
+ daemon
195
+ auth
196
+ syslog
197
+ lpr
198
+ news
199
+ uucp
200
+ cron
201
+ authpriv
202
+ ftp
203
+ local0 local1 local2 local3 local4 local5 local6 local7
204
+ }
205
+
206
+ SEVERITIES = %w{
207
+ emerg
208
+ alert
209
+ crit
210
+ err
211
+ warning
212
+ notice
213
+ info
214
+ debug
215
+ }
184
216
  end