Skip to content

Commit fae1aeb

Browse files
committed
feature: Add syslog scoping
Allow administrators to toggle between viewing all syslog events from inbound messages, regardless of scope, or restricting the syslog viewer to nodes belonging to the selected cluster. Fixes: #387 Sponsored-by: credativ GmbH (https://credativ.de)
1 parent d5affc1 commit fae1aeb

7 files changed

Lines changed: 176 additions & 11 deletions

File tree

pegaprox/api/helpers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def load_server_settings():
8080
# MK Apr 2026 — when true, /api/metrics needs no auth. Useful for setups
8181
# where a reverse proxy/mutual-TLS already gates scrapes. Default off.
8282
'metrics_public': False,
83+
# When enabled, the Syslog viewer only shows hostnames belonging to
84+
# the currently selected cluster instead of all collected syslog rows.
85+
'syslog_filter_by_selected_cluster': False,
8386
# Webhook alert channels (Slack, Discord, Teams, ntfy, generic)
8487
# Each: {id, name, type, url, enabled, ...type-specific fields}
8588
'alert_webhooks': [],

pegaprox/api/reports.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
import sqlite3
99
import threading
10+
from urllib.parse import urlparse
1011
from datetime import datetime, timedelta
1112
from flask import Blueprint, jsonify, request
1213

@@ -16,7 +17,7 @@
1617

1718
from pegaprox.utils.auth import require_auth, load_users
1819
from pegaprox.utils.rbac import get_user_clusters
19-
from pegaprox.api.helpers import check_cluster_access
20+
from pegaprox.api.helpers import check_cluster_access, load_server_settings
2021
from pegaprox.background.metrics import load_metrics_history, start_metrics_collector
2122
from pegaprox.background.syslog_server import DB_FILE, SEVERITY_MAP
2223
from pegaprox.api.schedules import start_scheduler
@@ -57,6 +58,53 @@ def _syslog_like_clause(search_text):
5758
[like, like, like, like, like, like],
5859
)
5960

61+
62+
def _syslog_hostname_tokens(value):
63+
value = str(value or '').strip().lower()
64+
if not value:
65+
return set()
66+
if '://' in value:
67+
parsed = urlparse(value)
68+
value = parsed.hostname or value
69+
value = value.split('/')[0].split('@')[-1]
70+
if value.startswith('[') and ']' in value:
71+
value = value[1:value.index(']')]
72+
elif ':' in value and value.count(':') == 1:
73+
value = value.rsplit(':', 1)[0]
74+
value = value.strip('.')
75+
if not value:
76+
return set()
77+
tokens = {value}
78+
if '.' in value:
79+
tokens.add(value.split('.', 1)[0])
80+
return tokens
81+
82+
83+
def _syslog_cluster_hostnames(cluster_id):
84+
manager = cluster_managers.get(cluster_id)
85+
if not manager:
86+
return set()
87+
88+
hostnames = set()
89+
config = getattr(manager, 'config', None)
90+
for value in (
91+
getattr(manager, 'host', ''),
92+
getattr(config, 'host', '') if config else '',
93+
getattr(config, 'name', '') if config else '',
94+
):
95+
hostnames.update(_syslog_hostname_tokens(value))
96+
97+
try:
98+
node_status = manager.get_node_status() or {}
99+
for node_name in node_status.keys():
100+
hostnames.update(_syslog_hostname_tokens(node_name))
101+
except Exception as exc:
102+
logging.debug(f"[Syslog] Could not load nodes for cluster filter {cluster_id}: {exc}")
103+
for node_name in getattr(manager, 'ha_node_status', {}).keys():
104+
hostnames.update(_syslog_hostname_tokens(node_name))
105+
106+
return hostnames
107+
60108
@bp.route('/api/reports/summary', methods=['GET'])
61109
@require_auth()
62110
def get_reports_summary():
@@ -180,6 +228,7 @@ def get_integrated_syslog_events():
180228
hostname = (request.args.get('hostname') or '').strip()
181229
source_ip = (request.args.get('source_ip') or '').strip()
182230
facility = (request.args.get('facility') or '').strip()
231+
cluster_id = (request.args.get('cluster_id') or '').strip()
183232

184233
sort_map = {
185234
'id': 'logs.id',
@@ -267,6 +316,22 @@ def get_integrated_syslog_events():
267316
except ValueError:
268317
pass
269318

319+
if cluster_id and load_server_settings().get('syslog_filter_by_selected_cluster', False):
320+
ok, err = check_cluster_access(cluster_id)
321+
if not ok:
322+
return err
323+
cluster_hostnames = sorted(_syslog_cluster_hostnames(cluster_id))
324+
if cluster_hostnames:
325+
cluster_hostname_where = []
326+
for value in cluster_hostnames:
327+
cluster_hostname_where.append("LOWER(logs.hostname) = ?")
328+
params.append(value)
329+
cluster_hostname_where.append("LOWER(logs.hostname) LIKE ?")
330+
params.append(f"{value}.%")
331+
where.append(f"({' OR '.join(cluster_hostname_where)})")
332+
else:
333+
where.append("1 = 0")
334+
270335
where_sql = f"WHERE {' AND '.join(where)}" if where else ''
271336
joins_sql = f"{' '.join(joins)}" if joins else ''
272337
offset = (page - 1) * per_page

pegaprox/api/settings.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,12 @@ def update_server_settings():
11521152
settings['alert_update_available'] = bool(data['alert_update_available'])
11531153
if 'metrics_public' in data:
11541154
settings['metrics_public'] = bool(data['metrics_public'])
1155+
if 'syslog_filter_by_selected_cluster' in data:
1156+
old_val = settings.get('syslog_filter_by_selected_cluster', False)
1157+
settings['syslog_filter_by_selected_cluster'] = bool(data['syslog_filter_by_selected_cluster'])
1158+
if old_val != settings['syslog_filter_by_selected_cluster']:
1159+
log_audit(request.session.get('user', 'admin'), 'settings.syslog',
1160+
f"Syslog cluster hostname filter {'enabled' if settings['syslog_filter_by_selected_cluster'] else 'disabled'}")
11551161
if 'strict_session_ip' in data:
11561162
settings['strict_session_ip'] = bool(data['strict_session_ip'])
11571163

@@ -1381,6 +1387,12 @@ def update_server_settings():
13811387
settings['alert_cooldown'] = max(60, min(86400, int(request.form['alert_cooldown'])))
13821388
if 'alert_update_available' in request.form:
13831389
settings['alert_update_available'] = request.form['alert_update_available'] in ('true', '1', 'on')
1390+
if 'syslog_filter_by_selected_cluster' in request.form:
1391+
old_syslog_filter = settings.get('syslog_filter_by_selected_cluster', False)
1392+
settings['syslog_filter_by_selected_cluster'] = request.form['syslog_filter_by_selected_cluster'] in ('true', '1', 'on')
1393+
if old_syslog_filter != settings['syslog_filter_by_selected_cluster']:
1394+
log_audit(request.session.get('user', 'admin'), 'settings.syslog',
1395+
f"Syslog cluster hostname filter {'enabled' if settings['syslog_filter_by_selected_cluster'] else 'disabled'}")
13841396

13851397
# Handle certificate upload
13861398
if 'ssl_cert' in request.files:
@@ -4574,4 +4586,3 @@ def test_ldap():
45744586
return jsonify(results)
45754587

45764588

4577-

0 commit comments

Comments
 (0)