init
This commit is contained in:
commit
8db9949a61
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.sw[opqnm]
|
||||
*~
|
6
Gemfile
Normal file
6
Gemfile
Normal file
|
@ -0,0 +1,6 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'systemd-journal' #, '~> 1.3.0'
|
||||
gem 'prometheus-client'
|
||||
gem 'rack'
|
||||
gem 'puma'
|
23
Gemfile.lock
Normal file
23
Gemfile.lock
Normal file
|
@ -0,0 +1,23 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ffi (1.15.5)
|
||||
nio4r (2.5.8)
|
||||
prometheus-client (4.0.0)
|
||||
puma (5.6.4)
|
||||
nio4r (~> 2.0)
|
||||
rack (2.2.3)
|
||||
systemd-journal (1.4.2)
|
||||
ffi (~> 1.9)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
prometheus-client
|
||||
puma
|
||||
rack
|
||||
systemd-journal
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
51
config.ru
Normal file
51
config.ru
Normal file
|
@ -0,0 +1,51 @@
|
|||
require 'rack'
|
||||
require './postfix_exporter'
|
||||
require 'socket'
|
||||
|
||||
collector = Collector.new
|
||||
th = collector.start
|
||||
showqpath = '/var/spool/postfix/public/showq'
|
||||
prometheus = collector.prometheus
|
||||
metrics = OpenStruct.new(
|
||||
queued: prometheus.gauge( :postfix_queued, docstring: "Queued mails per queue and sender/recipient"),
|
||||
)
|
||||
|
||||
def determine_domain str
|
||||
case str
|
||||
when /@([^.]+\.[^.]+)\z/
|
||||
$1
|
||||
when /\.([^.]+\.[^.]+)\z/
|
||||
"any.#$1"
|
||||
when 'MAILER-DAEMON'
|
||||
'MAILER-DAEMON'
|
||||
else
|
||||
'any'
|
||||
end
|
||||
end
|
||||
|
||||
run lambda {|env|
|
||||
req = Rack::Request.new env
|
||||
case req.path
|
||||
when "/metrics"
|
||||
showq = ''
|
||||
UNIXSocket.open showqpath do |s|
|
||||
while '' != (r = s.read)
|
||||
showq += r
|
||||
end
|
||||
end
|
||||
showq =
|
||||
showq.split( "\x00\x00").map do |x|
|
||||
y = x.split "\x00"
|
||||
y.push '' if y.length.odd?
|
||||
Hash[*y]
|
||||
end
|
||||
showq.group_by do |e|
|
||||
{queue: e['queue_name'], sender: determine_domain( e['sender']), recipient: determine_domain( e['recipient'])}
|
||||
end.each do |labels, entries|
|
||||
metrics.queued.set labels, entries.length if labels and labels[:queue]
|
||||
end
|
||||
[200, {"Content-Type" => "text/plain"}, [Prometheus::Client::Formats::Text.marshal( prometheus)]]
|
||||
else
|
||||
[404, {"Content-Type" => "text/plain"}, ["Not found\nYou want to try /metrics?\n"]]
|
||||
end
|
||||
}
|
64
gemset.nix
Normal file
64
gemset.nix
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
ffi = {
|
||||
groups = ["default"];
|
||||
platforms = [];
|
||||
source = {
|
||||
remotes = ["https://rubygems.org"];
|
||||
sha256 = "1862ydmclzy1a0cjbvm8dz7847d9rch495ib0zb64y84d3xd4bkg";
|
||||
type = "gem";
|
||||
};
|
||||
version = "1.15.5";
|
||||
};
|
||||
nio4r = {
|
||||
groups = ["default"];
|
||||
platforms = [];
|
||||
source = {
|
||||
remotes = ["https://rubygems.org"];
|
||||
sha256 = "0xk64wghkscs6bv2n22853k2nh39d131c6rfpnlw12mbjnnv9v1v";
|
||||
type = "gem";
|
||||
};
|
||||
version = "2.5.8";
|
||||
};
|
||||
prometheus-client = {
|
||||
groups = ["default"];
|
||||
platforms = [];
|
||||
source = {
|
||||
remotes = ["https://rubygems.org"];
|
||||
sha256 = "11k1r8mfr0bnd574yy08wmpzbgq8yqw3shx7fn5f6hlmayacc4bh";
|
||||
type = "gem";
|
||||
};
|
||||
version = "4.0.0";
|
||||
};
|
||||
puma = {
|
||||
dependencies = ["nio4r"];
|
||||
groups = ["default"];
|
||||
platforms = [];
|
||||
source = {
|
||||
remotes = ["https://rubygems.org"];
|
||||
sha256 = "0dgr2rybayih2naz3658mbzqwfrg9fxl80zsvhscf6b972kp3jdw";
|
||||
type = "gem";
|
||||
};
|
||||
version = "5.6.4";
|
||||
};
|
||||
rack = {
|
||||
groups = ["default"];
|
||||
platforms = [];
|
||||
source = {
|
||||
remotes = ["https://rubygems.org"];
|
||||
sha256 = "0i5vs0dph9i5jn8dfc6aqd6njcafmb20rwqngrf759c9cvmyff16";
|
||||
type = "gem";
|
||||
};
|
||||
version = "2.2.3";
|
||||
};
|
||||
systemd-journal = {
|
||||
dependencies = ["ffi"];
|
||||
groups = ["default"];
|
||||
platforms = [];
|
||||
source = {
|
||||
remotes = ["https://rubygems.org"];
|
||||
sha256 = "0xr1pljdya7y1b2nlppx1d0vsddhg1xl9j3y727jgdg2c5nsbc5i";
|
||||
type = "gem";
|
||||
};
|
||||
version = "1.4.2";
|
||||
};
|
||||
}
|
240
postfix_exporter.rb
Normal file
240
postfix_exporter.rb
Normal file
|
@ -0,0 +1,240 @@
|
|||
#!/usr/bin/env ruby
|
||||
# vim: set noet sw=2 ts=2 sts=2:
|
||||
|
||||
require 'systemd/journal'
|
||||
require 'prometheus/client'
|
||||
require 'prometheus/client/formats/text'
|
||||
require 'ostruct'
|
||||
|
||||
class Collector
|
||||
attr_reader :journal, :prometheus
|
||||
|
||||
class PrefixProxy
|
||||
attr_reader :prometheus, :prefix
|
||||
def initialize prometheus, prefix
|
||||
@prometheus, @prefix = prometheus, prefix
|
||||
end
|
||||
|
||||
def counter name, docstring, **options
|
||||
@prometheus.counter :"#{prefix}_#{name}", docstring: docstring, **options
|
||||
end
|
||||
end
|
||||
|
||||
class Dovecot
|
||||
attr_reader :logged_in, :logged_out, :connection_closed
|
||||
def initialize prometheus
|
||||
@logged_in = prometheus.counter :logged_in, 'A counter of successfull logins to dovecot'
|
||||
@logged_out = prometheus.counter :logged_out, 'A counter of logouts on dovecot'
|
||||
@connection_closed = prometheus.counter :connection_closed, 'A counter of closed connection on dovecot'
|
||||
end
|
||||
|
||||
def collect entry
|
||||
case entry.message
|
||||
when /\Aimap-login: Login: user=/
|
||||
@logged_in.increment
|
||||
when /\Aimap\([^)]+\): Logged out /
|
||||
@logged_out.increment
|
||||
when /\Aimap\([^)]+\): Connection closed /
|
||||
@connection_closed.increment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Postscreen
|
||||
attr_reader :connect_from, :whitelisted, :pass_old, :dnsbl, :noqueue, :hangup, :disconnect, :unknown
|
||||
def initialize prometheus
|
||||
@connect_from = prometheus.counter :connect_from, 'A counter of connections to postscreen'
|
||||
@whitelisted = prometheus.counter :whitelisted, 'A counter of WHITELISTED connections to postscreen'
|
||||
@pass_old = prometheus.counter :pass_old, 'A counter of PASS OLD connections to postscreen'
|
||||
@dnsbl = prometheus.counter :dnsbl, 'A counter of DNSBL-blocked to postscreen'
|
||||
@noqueue = prometheus.counter :noqueue, 'A counter of NOQUEUE to postscreen', reason: "unknown"
|
||||
@hangup = prometheus.counter :hangup, 'A counter of HANGUP to postscreen'
|
||||
@disconnect = prometheus.counter :disconnect, 'A counter of DISCONNECT to postscreen'
|
||||
@unknown = prometheus.counter :unknown, 'A counter of unknown loglines by postscreen'
|
||||
end
|
||||
|
||||
def collect entry
|
||||
case entry.message
|
||||
when /\ACONNECT from /
|
||||
@connect_from.increment
|
||||
when /\AWHITELISTED /
|
||||
@whitelisted.increment
|
||||
when /\APASS OLD /
|
||||
@pass_old.increment
|
||||
when /\ADISCONNECT /
|
||||
@disconnect.increment
|
||||
when /\ANOQUEUE: /
|
||||
case entry.message
|
||||
when / blocked using /
|
||||
@noqueue.increment reason: 'dnsbl'
|
||||
end
|
||||
when /\AHANGUP /
|
||||
@hangup.increment
|
||||
when /\ADNSBL rank /
|
||||
@dnsbl.increment
|
||||
else
|
||||
@unknown.increment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Smtp
|
||||
def initialize prometheus
|
||||
@connection_refused = prometheus.counter :connection_refused, 'A counter of connection refused on smtp'
|
||||
@connection_timed_out = prometheus.counter :connection_timed_out, 'A counter of timed out connections on smtp'
|
||||
@tls = prometheus.counter :tls, 'A counter of TLS connections on smtp with TLS-version and cipher', labels: %w[trust tls cipher]
|
||||
@status = prometheus.counter :status, 'A counter of message status by status', labels: %w[status]
|
||||
@sent = prometheus.counter :sent, 'A counter of sent messages by smtp'
|
||||
@deferred = prometheus.counter :deferred, 'A counter of deferred messages by smtp'
|
||||
@bounced = prometheus.counter :bounced, 'A counter of bounced messages by smtp'
|
||||
@deliverable = prometheus.counter :deliverable, 'A counter of deliverable messages by smtp'
|
||||
@undeliverable = prometheus.counter :undeliverable, 'A counter of undeliverable messages by smtp'
|
||||
@status_unknown = prometheus.counter :status_unknown, 'A counter of unknown status by smtp'
|
||||
@unknown = prometheus.counter :unknown, 'A counter of unknown loglines by smtp'
|
||||
end
|
||||
|
||||
def collect entry
|
||||
case entry.message
|
||||
when /\Aconnect to /
|
||||
case entry.message
|
||||
when / Connection refused\z/
|
||||
@connection_refused.increment
|
||||
when / Connection timed out\z/
|
||||
@connection_timed_out.increment
|
||||
end
|
||||
when /\A([^ ]+) TLS connection established to .* ([^ ]+) with cipher ([^ ]+)/
|
||||
@tls.increment trust: $1, tls: $2, cipher: $3
|
||||
when /\A\w{8,15}: .*status=([^ ]+)/
|
||||
status = $1
|
||||
@status.increment status: status
|
||||
case status
|
||||
when 'sent'
|
||||
@sent.increment
|
||||
when 'deferred'
|
||||
@deferred.increment
|
||||
when 'bounced'
|
||||
@bounced.increment
|
||||
when 'deliverable'
|
||||
@deliverable.increment
|
||||
when 'undeliverable'
|
||||
@undeliverable.increment
|
||||
else
|
||||
@status_unknown.increment
|
||||
end
|
||||
else
|
||||
@unknown.increment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Smtpd
|
||||
def initialize prometheus
|
||||
@connect_from = prometheus.counter :connect_from, 'A counter of connections to smtpd'
|
||||
@tls = prometheus.counter :tls, 'A counter of TLS connections to smtpd with TLS-version and cipher'
|
||||
@disconnect_from = prometheus.counter :disconnect_from, 'A counter of disconnections to smtpd'
|
||||
@noqueue = prometheus.counter :noqueue, 'A counter of NOQUEUE by smtpd', reason: "uknown"
|
||||
@concurrenty_limit_exceeded = prometheus.counter :concurrenty_limit_exceeded, 'A counter of concurrenty limit exceeded connections to smtpd'
|
||||
@timeout = prometheus.counter :timeout_connection, 'A counter of timedout connections to smtpd'
|
||||
@lost_connection = prometheus.counter :lost_connection, 'A counter of lost connections to smtpd'
|
||||
@accepted = prometheus.counter :accepted, 'A counter of accepted messages to smtpd'
|
||||
@unknown = prometheus.counter :unknown, 'A counter of unknown loglines by smtpd'
|
||||
end
|
||||
|
||||
def collect entry
|
||||
case entry.message
|
||||
when /\Aconnect from unknown/
|
||||
@connect_from.increment unknown: 1
|
||||
when /\Aconnect from /
|
||||
@connect_from.increment unknown: 0
|
||||
when /\A([^ ]+) TLS connection established from .*: ([^ ]+) with cipher ([^ ]+) /
|
||||
@tls.increment trust: $1, tls: $2, cipher: $3
|
||||
when /\ANOQUEUE: /
|
||||
case entry.message
|
||||
when / Client host rejected: cannot find your reverse hostname /
|
||||
@noqueue.increment reason: 'no reverse hostname'
|
||||
when / User doesn't exist: /
|
||||
@noqueue.increment reason: 'user does not exist'
|
||||
else
|
||||
@noqueue.increment reason: 'any'
|
||||
end
|
||||
when /\Adisconnect from / # ehlo=2 starttls=1 auth=1 mail=1 rcpt=1 data=1 commands=8
|
||||
@disconnect_from.increment
|
||||
when /\Awarning: Connection concurrency limit exceeded: /
|
||||
@concurrenty_limit_exceeded.increment
|
||||
when /\Atimeout after ([^ ]+) from /
|
||||
@timeout.increment after: $1
|
||||
when /\Alost connection after ([^ ]+) from /
|
||||
@lost_connection.increment after: $1
|
||||
when /\A\w{8,15}: client=/ # sasl_method=
|
||||
@accepted.increment
|
||||
else
|
||||
@unknown.increment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Postfix
|
||||
def initialize prometheus
|
||||
@postscreen = Postscreen.new PrefixProxy.new( prometheus, :postscreen)
|
||||
@smtp = Smtp.new PrefixProxy.new( prometheus, :smtp)
|
||||
@smtpd = Smtpd.new PrefixProxy.new( prometheus, :smtpd)
|
||||
@submission = Smtpd.new PrefixProxy.new( prometheus, :submission)
|
||||
@qmgr = prometheus.counter :qmgr, 'A counter of qmgr actions'
|
||||
@cleanup = prometheus.counter :cleanup, 'A counter of cleanup actions'
|
||||
end
|
||||
|
||||
def collect entry
|
||||
case entry.syslog_identifier
|
||||
when 'postfix/postscreen'
|
||||
@postscreen.collect entry
|
||||
when 'postfix/smtp'
|
||||
@smtp.collect entry
|
||||
when 'postfix/smtpd'
|
||||
@smtpd.collect entry
|
||||
when 'postfix/submission/smtpd'
|
||||
@submission.collect entry
|
||||
when 'postfix/cleanup'
|
||||
@metrics.cleanup.increment
|
||||
when 'postfix/qmgr'
|
||||
@metrics.qmgr.increment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize prometheus: nil, journal: nil
|
||||
@journal = journal || Systemd::Journal.new( flags: Systemd::Journal::Flags::SYSTEM_ONLY)
|
||||
@prometheus = prometheus || Prometheus::Client.registry
|
||||
|
||||
@dovecot = Dovecot.new PrefixProxy.new( @prometheus, :dovecot)
|
||||
@postfix = Postfix.new PrefixProxy.new( @prometheus, :postfix)
|
||||
end
|
||||
|
||||
def start
|
||||
Thread.abort_on_exception = true
|
||||
Thread.new do
|
||||
begin
|
||||
run
|
||||
rescue Object
|
||||
STDERR.puts "#$! (#{$!.class})", $!.backtrace.map {|x| " in #{x}"}
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
@journal.seek :tail
|
||||
@journal.move_previous
|
||||
@journal.watch do |entry|
|
||||
case entry._systemd_unit
|
||||
when 'dovecot.service'
|
||||
@dovecot.collect entry
|
||||
when 'postfix@-.service'
|
||||
@postfix.collect entry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
run
|
||||
end
|
21
shell.nix
Normal file
21
shell.nix
Normal file
|
@ -0,0 +1,21 @@
|
|||
with (import <nixpkgs> {});
|
||||
let
|
||||
env = bundlerEnv {
|
||||
name = "postfix_exporter-bundler-env";
|
||||
inherit ruby;
|
||||
gemfile = ./Gemfile;
|
||||
lockfile = ./Gemfile.lock;
|
||||
gemset = ./gemset.nix;
|
||||
};
|
||||
in stdenv.mkDerivation {
|
||||
name = "postfix-exporter";
|
||||
buildInputs = [ pkgs.libffi pkgs.systemd pkgs.ruby env env.gems.puma ];
|
||||
buildPhase = ''
|
||||
echo "Build postfix_exporter
|
||||
mkdir -p "$out/lib"
|
||||
for f in ${pkgs.systemd}/lib/libsystemd*
|
||||
do
|
||||
ln -s "$f" "$out/lib/"
|
||||
done
|
||||
'';
|
||||
}
|
Loading…
Reference in a new issue