postfix-exporter 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 +7 -0
- data/.gitignore +5 -0
- data/README.md +50 -0
- data/bin/postfix-exporter +183 -0
- data/postfix-exporter.gemspec +32 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4b3403736d6ac37fd2733f6db98f566a36e426de634fe971b96a332dd13527dc
|
4
|
+
data.tar.gz: b20157262dd52237db592744c80713c260ea6eb224e2feb5eac7a60d5665ca40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8d99493844dfb43b7b15a3742430f50db2c70ec3506b0c1bd93557a4ca4aca45d22c4eca5a95010571eac486519740858e415f9bae2329ff2a630962056bd10d
|
7
|
+
data.tar.gz: a10120c7f4b0b7a31f36b4fd215bb44ea14868e8281491c473e26812e8b2cfbad0352c87666437087875fb3e095c4036e8b0316461ae82b0077567e938040846
|
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
This is a simple exporter for various statistics relating to a Postfix
|
2
|
+
server.
|
3
|
+
|
4
|
+
|
5
|
+
# Features
|
6
|
+
|
7
|
+
* Examines mail queue periodically and exports `postfix_mail_queue_size`;
|
8
|
+
|
9
|
+
* Reads syslog entries as they happen, and exports disposition status
|
10
|
+
counters (`postfix_delivery_delays_count`) per DSN, as well as delay summaries per
|
11
|
+
DSN (`postfix_delivery_delays{quantile="..."}`);
|
12
|
+
|
13
|
+
* Total number of SMTP connections (`postfix_smtpd_connections_total`) and
|
14
|
+
currently-active connections (`postfix_smtpd_active_connections`);
|
15
|
+
|
16
|
+
* Count how many delivery attempts were received
|
17
|
+
(`postfix_incoming_delivery_attempts_total`), split out by whether we
|
18
|
+
accepted or rejected the message (`status`) and the exact DSN provided to
|
19
|
+
the client (`dsn`);
|
20
|
+
|
21
|
+
* Whether or not the Postfix `master` process is running (`postfix_up`);
|
22
|
+
|
23
|
+
* Drinks from the syslog stream directly.
|
24
|
+
|
25
|
+
|
26
|
+
# Deployment
|
27
|
+
|
28
|
+
The default way of rolling out this exporter is as a Docker container. For
|
29
|
+
that, you'll want to setup a volume for the postfix queue (making it
|
30
|
+
available in the container as `/var/spool/postfix`), and some env
|
31
|
+
vars. You can also run it directly (via the gem), with the same env vars,
|
32
|
+
and with the expectation that `/var/spool/postfix` is in the usual place.
|
33
|
+
|
34
|
+
If you configure the postfix-exporter to run in the same PID namespace as
|
35
|
+
whatever it is that's running Postfix itself (either `--pid=host` or in the
|
36
|
+
same NS namespace, a la k8s pods), then the `postfix_up` metric will be
|
37
|
+
valid, otherwise it'll be random (but unlikely to be correct).
|
38
|
+
|
39
|
+
|
40
|
+
## Environment Variables
|
41
|
+
|
42
|
+
* `SYSLOG_SOCKET` -- where to read raw syslog events from. If you're
|
43
|
+
running Postfix in an isolated container, you can probably hook up
|
44
|
+
`/dev/log` directly between containers with a bit of shenanigans,
|
45
|
+
otherwise configure your syslog daemon to write out a copy of all `mail`
|
46
|
+
facility messages to a custom socket. Alternately, if you use logstash,
|
47
|
+
take a look at the [`syslogstash`
|
48
|
+
gem](https://rubygems.org/gems/syslogstash) and
|
49
|
+
[container](https://hub.docker.com/r/discourse/syslogstash/), and go forth
|
50
|
+
and sin no more.
|
@@ -0,0 +1,183 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rack'
|
4
|
+
require 'prometheus/middleware/exporter'
|
5
|
+
require 'socket'
|
6
|
+
require 'rack/handler/webrick'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
prometheus = Prometheus::Client.registry
|
10
|
+
|
11
|
+
prometheus.gauge(:postfix_exporter_start_time_seconds, "When this process started up").set({}, Time.now.to_f)
|
12
|
+
|
13
|
+
oldest = prometheus.gauge(:postfix_oldest_message_timestamp_seconds, "Queue time of the oldest message")
|
14
|
+
mailq = prometheus.gauge(:postfix_queue_size, "Number of messages in the mail queue")
|
15
|
+
q_err = prometheus.counter(:postfix_queue_processing_error_total, "Exceptions raised whilst scanning the Postfix queue")
|
16
|
+
up = prometheus.gauge(:postfix_up, "Whether the master process is running or not")
|
17
|
+
|
18
|
+
Thread.abort_on_exception = true
|
19
|
+
|
20
|
+
Thread.new do
|
21
|
+
loop do
|
22
|
+
begin
|
23
|
+
%w{incoming active corrupt hold}.each do |q|
|
24
|
+
mailq.set({ queue: q }, Dir["/var/spool/postfix/#{q}/*"].size)
|
25
|
+
end
|
26
|
+
|
27
|
+
# deferred is special, because it's often hueg it gets sharded into
|
28
|
+
# multiple subdirectories
|
29
|
+
mailq.set({ queue: 'deferred' }, Dir["/var/spool/postfix/deferred/*/*"].size)
|
30
|
+
rescue StandardError => ex
|
31
|
+
$stderr.puts "Error while monitoring queue sizes: #{ex.message} (#{ex.class})"
|
32
|
+
$stderr.puts ex.backtrace.map { |l| " #{l}" }.join("\n")
|
33
|
+
q_err.increment(class: ex.class.to_s, phase: "scan")
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
master_pid = File.read("/var/spool/postfix/pid/master.pid").to_i
|
38
|
+
|
39
|
+
if master_pid > 1
|
40
|
+
Process.kill(0, master_pid)
|
41
|
+
# If we get here, then the process exists, and
|
42
|
+
# that'll do for our purposes
|
43
|
+
up.set({}, 1)
|
44
|
+
else
|
45
|
+
up.set({}, 0)
|
46
|
+
end
|
47
|
+
rescue Errno::ENOENT, Errno::ESRCH, Errno::EACCES
|
48
|
+
up.set({}, 0)
|
49
|
+
rescue Errno::EPERM
|
50
|
+
# Ironically, we don't need to be able to *actually*
|
51
|
+
# signal the process; EPERM means it exists and is running
|
52
|
+
# as someone more privileged than us, which is enough
|
53
|
+
# for our purposes
|
54
|
+
up.set({}, 1)
|
55
|
+
rescue StandardError => ex
|
56
|
+
$stderr.puts "Error while checking master process: #{ex.message} (#{ex.class})"
|
57
|
+
$stderr.puts ex.backtrace.map { |l| " #{l}" }.join("\n")
|
58
|
+
q_err.increment(class: ex.class.to_s, phase: "up")
|
59
|
+
end
|
60
|
+
|
61
|
+
sleep 5
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Thread.new do
|
67
|
+
earliest_ctime = ->(glob) do
|
68
|
+
# There is seemingly no way to unset or remove a gauge metric in the Ruby
|
69
|
+
# implementation of the prom exporter. As a hack, we return the current
|
70
|
+
# time in cases where there is nothing to sample.
|
71
|
+
now = Time.now.to_i
|
72
|
+
|
73
|
+
Dir[glob].lazy.map do |n|
|
74
|
+
begin
|
75
|
+
File.stat(n).ctime.to_i
|
76
|
+
rescue Errno::ENOENT
|
77
|
+
now
|
78
|
+
end
|
79
|
+
end.min || now
|
80
|
+
end
|
81
|
+
|
82
|
+
loop do
|
83
|
+
begin
|
84
|
+
%w{incoming active corrupt hold}.each do |q|
|
85
|
+
oldest.set({ queue: q }, earliest_ctime["/var/spool/postfix/#{q}/*"])
|
86
|
+
end
|
87
|
+
oldest.set({ queue: 'deferred' }, earliest_ctime["/var/spool/postfix/deferred/*/*"])
|
88
|
+
rescue StandardError => ex
|
89
|
+
$stderr.puts "Error while sampling message ages: #{ex.message} (#{ex.class})"
|
90
|
+
$stderr.puts ex.backtrace.map { |l| " #{l}" }.join("\n")
|
91
|
+
q_err.increment(class: ex.class.to_s, phase: "stat")
|
92
|
+
end
|
93
|
+
|
94
|
+
# stat()ing all the files in a large queue could potentially be quite
|
95
|
+
# expensive, so we sample this data less frequently.
|
96
|
+
sleep 60
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
if ENV["SYSLOG_SOCKET"]
|
102
|
+
delays = prometheus.summary(:postfix_delivery_delays, "Distribution of time taken to deliver (or bounce) messages")
|
103
|
+
connects = prometheus.counter(:postfix_smtpd_connections_total, "Connections to smtpd")
|
104
|
+
active = prometheus.gauge(:postfix_smtpd_active_connections, "Current connections to smtpd")
|
105
|
+
incoming = prometheus.counter(:postfix_incoming_delivery_attempts_total, "Delivery attempts, labelled by dsn and status")
|
106
|
+
messages = prometheus.counter(:postfix_log_messages_total, "Syslog messages received, labelled by how it was handled")
|
107
|
+
log_errors = prometheus.counter(:postfix_log_processing_error_total, "Exceptions raised whilst processing log messages")
|
108
|
+
|
109
|
+
Thread.new do
|
110
|
+
begin
|
111
|
+
s = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
|
112
|
+
s.bind(Socket.pack_sockaddr_un(ENV["SYSLOG_SOCKET"]))
|
113
|
+
rescue Errno::EEXIST, Errno::EADDRINUSE
|
114
|
+
File.unlink ENV["SYSLOG_SOCKET"]
|
115
|
+
retry
|
116
|
+
end
|
117
|
+
|
118
|
+
loop do
|
119
|
+
begin
|
120
|
+
msg = s.recvmsg.first
|
121
|
+
if msg =~ %r{postfix/.* delay=(\d+(\.\d+)?), .* dsn=(\d+\.\d+\.\d+), status=(\w+)}
|
122
|
+
delay = $1.to_f
|
123
|
+
dsn = $3
|
124
|
+
status = $4
|
125
|
+
|
126
|
+
if status == "bounced" or status == "sent"
|
127
|
+
delays.observe({dsn: dsn, status: status}, delay)
|
128
|
+
end
|
129
|
+
|
130
|
+
messages.increment(type: "delay")
|
131
|
+
elsif msg =~ %r{postfix/smtpd\[\d+\]: connect from }
|
132
|
+
connects.increment({})
|
133
|
+
active.send(:synchronize) { active.set({}, active.get({}) || 0 + 1) }
|
134
|
+
messages.increment(type: "connect")
|
135
|
+
elsif msg =~ %r{postfix/smtpd\[\d+\]: disconnect from }
|
136
|
+
active.send(:synchronize) do
|
137
|
+
new = (active.get({}) || 0) - 1
|
138
|
+
# If we start running mid-stream,
|
139
|
+
# we might end up seeing more
|
140
|
+
# disconnects than connections,
|
141
|
+
# which would be confusing
|
142
|
+
new = 0 if new < 0
|
143
|
+
active.set({}, new)
|
144
|
+
end
|
145
|
+
messages.increment(type: "disconnect")
|
146
|
+
elsif msg =~ %r{postfix/smtpd\[\d+\]: [A-F0-9]+: client=}
|
147
|
+
incoming.increment(dsn: "2.0.0", status: "queued")
|
148
|
+
messages.increment(type: "queued")
|
149
|
+
elsif msg =~ %r{postfix/smtpd\[\d+\]: NOQUEUE: reject: RCPT from \S+: \d{3} (\d+\.\d+\.\d+) }
|
150
|
+
incoming.increment(dsn: $1, status: "rejected")
|
151
|
+
messages.increment(type: "noqueue")
|
152
|
+
else
|
153
|
+
messages.increment(type: "ignored")
|
154
|
+
end
|
155
|
+
rescue StandardError => ex
|
156
|
+
$stderr.puts "Error while receiving postfix logs: #{ex.message} (#{ex.class})"
|
157
|
+
$stderr.puts ex.backtrace.map { |l| " #{l}" }.join("\n")
|
158
|
+
log_errors.increment(class: ex.class.to_s)
|
159
|
+
sleep 1
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
app = Rack::Builder.new
|
166
|
+
app.use Rack::Deflater, if: ->(_, _, _, body) { body.any? && body[0].length > 512 }
|
167
|
+
app.use Prometheus::Middleware::Exporter
|
168
|
+
app.run ->(env) { [404, {'Content-Type' => 'text/plain'}, ['NOPE NOPE NOPE NOPE']] }
|
169
|
+
|
170
|
+
logger = Logger.new($stderr)
|
171
|
+
logger.level = Logger::INFO
|
172
|
+
logger.formatter = proc { |s, t, p, m| "WEBrick: #{m}\n" }
|
173
|
+
|
174
|
+
# This is the only way to get the Rack-mediated webrick to listen on both
|
175
|
+
# INADDR_ANY and IN6ADDR_ANY on libcs that don't support getaddrinfo("*")
|
176
|
+
# (ie musl-libc). Setting `Host: '*'` barfs on the above-mentioned buggy(?)
|
177
|
+
# libcs, `Host: '::'` fails on newer rubies (because they use
|
178
|
+
# setsockopt(V6ONLY) by default), and with RACK_ENV at its default of
|
179
|
+
# "development", it only listens on localhost. And even *this* only works
|
180
|
+
# on Rack 2, because before that the non-development default listen address
|
181
|
+
# was "0.0.0.0"!
|
182
|
+
ENV['RACK_ENV'] = "none"
|
183
|
+
Rack::Handler::WEBrick.run app, Port: 9154, Logger: logger, AccessLog: []
|
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'git-version-bump'
|
3
|
+
rescue LoadError
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "postfix-exporter"
|
9
|
+
|
10
|
+
s.version = GVB.version rescue "0.0.0.1.NOGVB"
|
11
|
+
s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
|
12
|
+
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
s.summary = "Export Prometheus statistics for a Postfix server"
|
16
|
+
|
17
|
+
s.authors = ["Matt Palmer"]
|
18
|
+
s.email = ["matt.palmer@discourse.org"]
|
19
|
+
s.homepage = "https://github.com/discourse/postfix-exporter"
|
20
|
+
|
21
|
+
s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Dockerfile|Rakefile)/ }
|
22
|
+
s.executables = ["postfix-exporter"]
|
23
|
+
|
24
|
+
s.required_ruby_version = ">= 2.1.0"
|
25
|
+
|
26
|
+
s.add_runtime_dependency 'prometheus-client', '~> 0.7', '< 0.10'
|
27
|
+
s.add_runtime_dependency 'rack', '~> 2.0'
|
28
|
+
|
29
|
+
s.add_development_dependency 'bundler'
|
30
|
+
s.add_development_dependency 'github-release'
|
31
|
+
s.add_development_dependency 'rake'
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: postfix-exporter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Palmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-10-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: prometheus-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.7'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0.10'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.7'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.10'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rack
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.0'
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: github-release
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
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: rake
|
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:
|
91
|
+
- matt.palmer@discourse.org
|
92
|
+
executables:
|
93
|
+
- postfix-exporter
|
94
|
+
extensions: []
|
95
|
+
extra_rdoc_files: []
|
96
|
+
files:
|
97
|
+
- ".gitignore"
|
98
|
+
- README.md
|
99
|
+
- bin/postfix-exporter
|
100
|
+
- postfix-exporter.gemspec
|
101
|
+
homepage: https://github.com/discourse/postfix-exporter
|
102
|
+
licenses: []
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 2.1.0
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubygems_version: 3.0.3
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Export Prometheus statistics for a Postfix server
|
123
|
+
test_files: []
|