postfix_exporter/postfix_exporter.rb

241 lines
8.3 KiB
Ruby

#!/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