From 19643a74ddafce9ba68f87101bc550ff890e1e5c Mon Sep 17 00:00:00 2001 From: byrsapty Date: Thu, 23 Jul 2026 16:34:32 +0300 Subject: [PATCH] Fix false-positive "config changed" on every alert, every run The previous fix compared the whole live 'config' object against the whole desired one - but Graylog's GET response fills in extra defaults (query_parameters, filters, use_cron_scheduling, cron_expression, cron_timezone, ...) that never appear in our alert JSON files. That made every single alert compare as "different" on every single run, not just the one actually edited - confirmed live: a re-run PUT-updated all 7 alerts when only alert7's query had changed. Fixed by comparing only the keys our own files actually author (project the live config down to just those keys before comparing), and switched from interpolating JSON into python -c string literals to writing to temp files and reading them - the former is fragile the moment a value contains a quote or backslash. Co-Authored-By: Claude Sonnet 5 --- install-graylog.sh | 60 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/install-graylog.sh b/install-graylog.sh index 7c996f0..501ad54 100644 --- a/install-graylog.sh +++ b/install-graylog.sh @@ -498,39 +498,71 @@ step_alerts() { # Same "compare content, PUT-update if changed" fix as step_pipeline_rules()/ # step_pipelines() - an alert's title never changes when its query/threshold # does, so a plain title-exists check would silently skip the update - # forever. Confirmed live this session: alert7's query was fixed in git - # (vendor:accel-ppp -> vendor:accel-ppp AND event_type:*) but a re-run - # kept reporting "already exists" with the old, buggy query still live. - local existing_defs - existing_defs="$(gcurl GET /events/definitions)" - local f title id body current_config desired_config + # forever. + # + # Uses files instead of interpolating JSON into python -c strings (fragile + # with quotes/backslashes), and compares only the keys OUR file authors in + # "config" - not the whole object. Confirmed live this is necessary: + # Graylog's GET response fills in extra defaults (query_parameters, filters, + # use_cron_scheduling, cron_expression, cron_timezone, ...) that never + # appear in our alert JSON files, so a naive whole-object comparison always + # reports "changed" and PUTs every single alert on every single run, not + # just the one actually edited. + local existing_defs_file + existing_defs_file="$(mktemp)" + gcurl GET /events/definitions > "$existing_defs_file" + + local f title id body_file for f in "$SCRIPT_DIR"/alerts/alert*.json; do [ -f "$f" ] || continue title="$(python3 -c "import json;print(json.load(open('$f'))['title'])")" - body="$(sed \ + body_file="$(mktemp)" + sed \ -e "s/__NETWORK_STREAM_ID__/$NETWORK_STREAM_ID/" \ -e "s/__SERVERS_STREAM_ID__/$SERVERS_STREAM_ID/" \ -e "s/__DISCORD_NOTIFICATION_ID__/$notif_id/" \ - "$f")" - id="$(echo "$existing_defs" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((e['id'] for e in d['event_definitions'] if e['title']=='$title'),''))")" + "$f" > "$body_file" + + id="$(python3 -c " +import json +d = json.load(open('$existing_defs_file')) +print(next((e['id'] for e in d['event_definitions'] if e['title'] == '$title'), '')) +")" + if [ -n "$id" ]; then - current_config="$(echo "$existing_defs" | python3 -c "import json,sys;d=json.load(sys.stdin);print(json.dumps(next(e['config'] for e in d['event_definitions'] if e['id']=='$id'),sort_keys=True))")" - desired_config="$(echo "$body" | python3 -c "import json,sys;print(json.dumps(json.load(sys.stdin)['config'],sort_keys=True))")" - if [ "$current_config" = "$desired_config" ]; then + if python3 -c " +import json, sys +existing = json.load(open('$existing_defs_file')) +current_full = next(e['config'] for e in existing['event_definitions'] if e['id'] == '$id') +desired = json.load(open('$body_file'))['config'] +filtered_current = {k: current_full.get(k) for k in desired} +sys.exit(0 if filtered_current == desired else 1) +"; then skip "alert '$title' already up to date ($id)" + rm -f "$body_file" continue fi - gcurl PUT "/events/definitions/$id" "$(echo "$body" | python3 -c "import json,sys;d=json.load(sys.stdin);d['id']='$id';print(json.dumps(d))")" >/dev/null + python3 -c " +import json +d = json.load(open('$body_file')) +d['id'] = '$id' +print(json.dumps(d)) +" > "${body_file}.put" + gcurl PUT "/events/definitions/$id" "$(cat "${body_file}.put")" >/dev/null gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null ok "updated alert '$title' ($id) - config changed" + rm -f "$body_file" "${body_file}.put" continue fi - id="$(gcurl POST /events/definitions "{\"entity\": ${body}, \"share_request\": {\"selected_grantee_capabilities\": {}}}" \ + + id="$(gcurl POST /events/definitions "{\"entity\": $(cat "$body_file"), \"share_request\": {\"selected_grantee_capabilities\": {}}}" \ | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")" [ -n "$id" ] || die "alert '$title' failed to create" gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null ok "created and enabled alert '$title' ($id)" + rm -f "$body_file" done + rm -f "$existing_defs_file" } # ---------------------------------------------------------------------------