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