Add auth exporter module: implement Prometheus scrape configuration, exporter logic, and Grafana dashboard

This commit is contained in:
2025-10-03 00:59:58 +02:00
parent eb60d37c2d
commit cd69dbc25a
7 changed files with 303 additions and 0 deletions
@@ -31,6 +31,11 @@ parse_errors = Counter('auth_exporter_parse_errors_total', 'Total parse errors')
uptime = Gauge('auth_exporter_uptime_seconds', 'Exporter uptime in seconds')
user_label_dropped = Counter('auth_exporter_user_labels_dropped_total', 'Number of username labels dropped due to max-user-labels limit')
# Current active sessions (gauge) — safe labels with small domain
active_sessions = Gauge('auth_sessions_active', 'Current active auth sessions', ['source'])
# Optional per-user active gauge (high-cardinality — enabled with --export-usernames)
active_sessions_by_user = Gauge('auth_sessions_active_by_user', 'Current active auth sessions by username (high-cardinality — enable with --export-usernames)', ['source', 'username'])
# more tolerant username pattern (allows -, ., @, digits)
USERNAME = r'[\w\-\.\@]+'
IP = r'[\d\.]+|[\da-fA-F:]+'
@@ -51,6 +56,9 @@ pam_sudo_session = re.compile(
rf'sudo: pam_unix\(sudo[^)]*:session\): session opened for user ({USERNAME})(?:\(uid=\d+\))? by (?:({USERNAME})|uid=\d+|\(uid=\d+\))'
)
# Generic 'session closed' pattern (captures service and username when pam_unix logs a session close)
pam_session_closed = re.compile(rf'pam_unix\((?P<service>[^:]+):session\): session closed for user ({USERNAME})')
# Track seen usernames to limit cardinality if username export is enabled
_seen_usernames = set()
@@ -108,12 +116,62 @@ class TailReader:
def parse_line(line, export_usernames=False, max_user_labels=100):
try:
# First handle session closed lines so we can decrement active gauges
m_close = pam_session_closed.search(line)
if m_close:
service = m_close.group('service')
username = m_close.group(2)
# map service to a source label
if service.startswith('sshd') or service == 'sshd':
source = 'ssh'
elif service.startswith('login') or service == 'login':
source = 'tty'
elif service.startswith('systemd') or service.startswith('systemd-user'):
source = 'systemd-user'
elif service.startswith('sudo'):
source = 'sudo'
else:
source = None
if source:
# decrement the aggregated active gauge (but avoid negative values when possible)
try:
child = active_sessions.labels(source=source)
val = child._value.get()
if val and val > 0:
child.dec()
except Exception:
# fallback: attempt to decrement anyway
try:
active_sessions.labels(source=source).dec()
except Exception:
pass
if export_usernames and username in _seen_usernames:
try:
childu = active_sessions_by_user.labels(source=source, username=username)
v = childu._value.get()
if v and v > 0:
childu.dec()
except Exception:
try:
active_sessions_by_user.labels(source=source, username=username).dec()
except Exception:
pass
# nothing else to do for closed lines
return 'closed'
# ssh (captures method and username)
m = pam_ssh_session.search(line)
if m:
method = m.group(1) # 'password' or 'publickey'
username = m.group(2)
auth_sessions.labels(source='ssh', method=method).inc()
# increment active sessions gauge (aggregated by source)
try:
active_sessions.labels(source='ssh').inc()
except Exception:
pass
if export_usernames:
if username not in _seen_usernames:
if len(_seen_usernames) >= max_user_labels:
@@ -122,6 +180,10 @@ def parse_line(line, export_usernames=False, max_user_labels=100):
_seen_usernames.add(username)
if username in _seen_usernames:
auth_sessions_by_user.labels(source='ssh', method=method, username=username).inc()
try:
active_sessions_by_user.labels(source='ssh', username=username).inc()
except Exception:
pass
return 'ssh'
# tty