11#!/usr/bin/env python3
22
33import argparse
4+ import collections
45import json
56import os
67import sys
2021)
2122from common .logger import get_logger
2223from grafana_client import GrafanaApi
23- from grafana_client .client import (
24- GrafanaBadInputError ,
25- GrafanaClientError ,
26- GrafanaException ,
27- GrafanaServerError ,
28- )
2924from tenacity import before_sleep_log , retry , stop_after_attempt , wait_fixed
3025
3126# Global logger (initialized in alert_builder function)
@@ -69,7 +64,6 @@ def create_alert_rule(
6964 title : str ,
7065 folder_uid : str ,
7166 rule_group : str ,
72- interval_sec : int ,
7367 _for : str ,
7468 expr : str ,
7569 conditions : list [dict [str , any ]],
@@ -82,7 +76,6 @@ def create_alert_rule(
8276 alert_rule ["title" ] = title
8377 alert_rule ["folderUID" ] = folder_uid
8478 alert_rule ["ruleGroup" ] = rule_group
85- alert_rule ["intervalSec" ] = interval_sec
8679 alert_rule ["for" ] = _for
8780 alert_rule ["labels" ] = labels
8881 alert_rule ["data" ] = [
@@ -100,6 +93,12 @@ def create_alert_rule(
10093 return alert_rule
10194
10295
96+ def create_rule_group (
97+ name : str , interval_sec : int , rules : list [dict [str , any ]]
98+ ) -> dict [str , any ]:
99+ return {"name" : name , "interval" : interval_sec , "rules" : rules }
100+
101+
103102def get_all_folders (client : GrafanaApi ) -> list [dict [str , any ]]:
104103 logger .debug ("Getting all folders" )
105104 return client .folder .get_all_folders ()
@@ -128,25 +127,14 @@ def create_folder_return_uid(client: GrafanaApi, title: str) -> str:
128127 return folder ["uid" ]
129128
130129
131- def dump_alert (output_dir : str , alert : dict [str , any ]) -> None :
132- alert_full_path = f"{ output_dir } /{ alert ['name' ]} .json" .lower ().replace (" " , "_" )
130+ def dump_rule_group (output_dir : str , rule_group : dict [str , any ]) -> None :
131+ group_full_path = f"{ output_dir } /{ rule_group ['name' ]} .json" .lower ().replace (" " , "_" )
133132 os .makedirs (output_dir , exist_ok = True )
134- ordered = {"title" : alert ["title" ], ** {k : v for k , v in alert .items () if k != "title" }}
135- with open (alert_full_path , "w" ) as f :
136- json .dump (ordered , f , indent = 2 )
137- # Format with professional colors: Alert (white bold), name (cyan), saved to (white bold), path (dim cyan)
133+ with open (group_full_path , "w" ) as f :
134+ json .dump (rule_group , f , indent = 2 )
138135 logger .info (
139- f'[bold white]Alert[/bold white] "[blue]{ alert ["name" ]} [/blue]" [bold white]saved to[/bold white] [dim white]{ alert_full_path } [/dim white]'
140- )
141-
142-
143- def get_alert_rule_group (client : GrafanaApi , folder_uid : str , group_uid : str ) -> str :
144- logger .debug (f'Getting alert rule group "{ group_uid } "' )
145- rule_group = client .alertingprovisioning .get_rule_group (
146- folder_uid = folder_uid , group_uid = group_uid
136+ f'[bold white]Rule group[/bold white] "[blue]{ rule_group ["name" ]} [/blue]" [bold white]saved to[/bold white] [dim white]{ group_full_path } [/dim white]'
147137 )
148- logger .debug (f"Got alert group: { rule_group } " )
149- return rule_group
150138
151139
152140@retry (
@@ -296,7 +284,10 @@ def alert_builder(args: argparse.Namespace):
296284 # Exit cleanly without traceback
297285 sys .exit (1 )
298286
299- alerts = []
287+ interval_sec = dev_alerts ["intervalSec" ]
288+
289+ # group_name -> list of alert rules (preserving insertion order within each group)
290+ groups : dict [str , list [dict [str , any ]]] = collections .defaultdict (list )
300291
301292 for dev_alert in dev_alerts ["alerts" ]:
302293 # Apply config overrides to replace placeholders
@@ -315,13 +306,14 @@ def alert_builder(args: argparse.Namespace):
315306 )
316307 else :
317308 expr = remove_expr_placeholder (expr = dev_alert ["expr" ])
318- alerts .append (
309+
310+ group_name = dev_alert ["ruleGroup" ]
311+ groups [group_name ].append (
319312 create_alert_rule (
320313 name = dev_alert ["name" ],
321314 title = dev_alert ["title" ],
322315 folder_uid = folder_uid ,
323- interval_sec = dev_alert ["intervalSec" ],
324- rule_group = dev_alert ["ruleGroup" ],
316+ rule_group = group_name ,
325317 _for = dev_alert ["for" ],
326318 expr = expr ,
327319 conditions = dev_alert ["conditions" ],
@@ -333,66 +325,31 @@ def alert_builder(args: argparse.Namespace):
333325 )
334326 )
335327
336- alerts .sort (key = lambda a : a ["title" ])
328+ rule_groups = [
329+ create_rule_group (
330+ name = group_name ,
331+ interval_sec = interval_sec ,
332+ rules = sorted (rules , key = lambda a : a ["title" ]),
333+ )
334+ for group_name , rules in sorted (groups .items ())
335+ ]
337336
338- for alert in alerts :
337+ for rule_group in rule_groups :
339338 if args .debug :
340- logger .debug (json .dumps (alert ))
339+ logger .debug (json .dumps (rule_group ))
341340 if not args .dry_run :
342- alert_created_or_exists = False
343341 try :
344- client .alertingprovisioning .create_alertrule (
345- alertrule = alert ,
346- disable_provenance = True ,
347- )
348- logger .info (f'Alert "{ alert ["name" ]} " uploaded to Grafana successfully' )
349- alert_created_or_exists = True
350-
351- except GrafanaBadInputError as e :
352- if "alerting.alert-rule.conflict" in e .message :
353- logger .info (f'Alert "{ alert ["name" ]} " already exists. Skipping creation.' )
354- alert_created_or_exists = True
355- else :
356- # Handle other bad input errors
357- logger .error (
358- f'Failed to create alert "{ alert ["name" ]} ". Bad input: { e .message } '
359- )
360- except GrafanaClientError as e :
361- # Handle other client-side errors (e.g., invalid request)
362- logger .error (f'Failed to create alert "{ alert ["name" ]} ". Client error: { e .message } ' )
363- except GrafanaServerError as e :
364- # Handle server-side errors (5xx errors)
365- logger .error (f'Failed to create alert "{ alert ["name" ]} ". Server error: { e .message } ' )
366- except GrafanaException as e :
367- # Catch any other Grafana-related exceptions
368- logger .error (
369- f'Failed to create alert "{ alert ["name" ]} ". Grafana error: { e .message } '
342+ update_alert_rule_group (
343+ client = client ,
344+ folder_uid = folder_uid ,
345+ group_uid = rule_group ["name" ],
346+ alertrule_group = rule_group ,
370347 )
371348 except Exception as e :
372- # Catch any other exceptions (non-Grafana-related)
373- logger .error (f'Failed to create alert "{ alert ["name" ]} ". Unexpected error: { e } ' )
374-
375- # Only update rule group interval if alert was successfully created or already exists
376- if alert_created_or_exists :
377- try :
378- group_uid = alert ["ruleGroup" ]
379- rule_group = get_alert_rule_group (
380- client = client , folder_uid = folder_uid , group_uid = group_uid
381- )
382- if rule_group ["interval" ] != alert ["intervalSec" ]:
383- rule_group ["interval" ] = alert ["intervalSec" ]
384- update_alert_rule_group (
385- client = client ,
386- folder_uid = folder_uid ,
387- group_uid = group_uid ,
388- alertrule_group = rule_group ,
389- )
390- logger .info (f'Alert rule group "{ group_uid } " updated successfully' )
391- except Exception as e :
392- logger .error (f'Failed to update alert rule group "{ alert ["ruleGroup" ]} ". { e } ' )
349+ logger .error (f'Failed to update rule group "{ rule_group ["name" ]} ". { e } ' )
393350
394351 if args .out_dir :
395352 output_dir = f"{ args .out_dir } /alerts"
396- dump_alert (output_dir = output_dir , alert = alert )
353+ dump_rule_group (output_dir = output_dir , rule_group = rule_group )
397354
398355 logger .info ("Done building grafana alerts" )
0 commit comments