66from botocore .exceptions import BotoCoreError , ClientError
77import urllib .request
88import logging
9- from typing import Any , Dict , List , Optional , final
9+ from typing import Any , Dict , List , Optional
1010
1111TEAMS_WEBHOOK_SECRET_NAME : str = 'TEAMS_WEBHOOK_SECRET_NAME'
12- TEAMS_WEBHOOK_SECRET_KEY : str = 'TEAMS_WEBHOOK_SECRET_KEY'
12+ TEAMS_WEBHOOK_SECRET_KEY_IMPORTANT_MESSAGES : str = 'TEAMS_WEBHOOK_SECRET_KEY'
13+ TEAMS_WEBHOOK_SECRET_KEY_UNIMPORTANT_MESSAGES : str = 'TEAMS_WEBHOOK_SECRET_KEY_UNIMPORTANT'
1314
1415
1516logger : logging .Logger = logging .getLogger ()
1819secrets_client = boto3 .client ('secretsmanager' )
1920
2021
21-
22-
23- def get_teams_webhook_url () -> str :
24- if TEAMS_WEBHOOK_SECRET_NAME not in os .environ :
25- raise ValueError ("Missing environment variable: TEAMS_WEBHOOK_SECRET_NAME" )
26- secret_name : Optional [str ] = os .environ .get (TEAMS_WEBHOOK_SECRET_NAME )
22+ def get_secret_by_name_and_key (env_var_with_name : str , env_var_with_key : str ) -> str :
23+ if env_var_with_name not in os .environ :
24+ raise ValueError (f"Missing environment variable: { env_var_with_name } " )
25+ secret_name : Optional [str ] = os .environ .get (env_var_with_name )
2726 if not secret_name or secret_name .strip () == "" :
28- raise ValueError ("Environment variable empty: TEAMS_WEBHOOK_SECRET_NAME " )
29- if TEAMS_WEBHOOK_SECRET_KEY not in os .environ :
30- raise ValueError ("Missing environment variable: TEAMS_WEBHOOK_SECRET_KEY " )
31- secret_key : Optional [str ] = os .environ .get (TEAMS_WEBHOOK_SECRET_KEY )
27+ raise ValueError (f "Environment variable empty: { env_var_with_name } " )
28+ if env_var_with_key not in os .environ :
29+ raise ValueError (f "Missing environment variable: { env_var_with_key } " )
30+ secret_key : Optional [str ] = os .environ .get (env_var_with_key )
3231 if not secret_key or secret_key .strip () == "" :
33- raise ValueError ("Environment variable empty: TEAMS_WEBHOOK_SECRET_KEY " )
32+ raise ValueError (f "Environment variable empty: { env_var_with_key } " )
3433 try :
3534 secret_str = secrets_client .get_secret_value (SecretId = secret_name )['SecretString' ]
3635 secret_dict = json .loads (secret_str )
3736 return secret_dict [secret_key ]
3837 except (ClientError , BotoCoreError ) as e :
39- logger .error ("Failed to fetch secret {TEAMS_WEBHOOK_SECRET_NAME}: %s " , e , exc_info = True )
38+ logger .error (f "Failed to fetch secret { env_var_with_name } with key { env_var_with_key } " , e , exc_info = True )
4039 raise
4140
4241
@@ -46,18 +45,16 @@ def parse_eventbridge_json_to_readable_message(msg: Dict[str,Any]) -> str:
4645 msg = json .loads (msg )
4746 final_message : str = ""
4847 if "time" in msg :
49- final_message += f"GMT time: { msg ['time' ]} \n "
48+ final_message += f"GMT time: { msg ['time' ]} \n \n "
5049 dt_utc = datetime .fromisoformat (msg ['time' ])
5150 dt_swiss = dt_utc .astimezone (ZoneInfo ("Europe/Zurich" ))
52- final_message += f"Swiss time: { dt_swiss } \n "
53- final_message += f"Message: \n ```\n { json .dumps (msg , indent = 2 )} \n ```\n "
51+ final_message += f"Swiss time: { dt_swiss } \n \n "
52+ final_message += f"Message:\n \n ```\n { json .dumps (msg , indent = 2 )} \n ```\n "
5453 return final_message
5554
5655
57- def parse_ecs_json_to_readable_message (msg : Dict [str ,Any ]) -> str :
56+ def parse_log_event_json_to_readable_message (msg : Dict [str ,Any ]) -> str :
5857 """Parse the raw SNS message from ECS and convert it to a readable format."""
59- if isinstance (msg , str ):
60- msg = json .loads (msg )
6158 # { "time": "2025-10-22T11:26:49.598302+00:00",
6259 # "level": "ERROR",
6360 # "name": "notebook_service.backend.eks.kubernetes_client",
@@ -66,24 +63,31 @@ def parse_ecs_json_to_readable_message(msg: Dict[str,Any]) -> str:
6663 # "exception": null }",
6764 final_message : str = ""
6865 if "time" in msg :
69- final_message += f"GMT time: { msg ['time' ]} \n "
66+ final_message += f"GMT time: { msg ['time' ]} \n \n "
7067 dt_utc = datetime .fromisoformat (msg ['time' ])
7168 dt_swiss = dt_utc .astimezone (ZoneInfo ("Europe/Zurich" ))
72- final_message += f"Swiss time: { dt_swiss } \n "
69+ final_message += f"Swiss time: { dt_swiss } \n \n "
7370 if "name" in msg :
74- final_message += f"Name: { msg ['name' ]} \n "
71+ final_message += f"Name: { msg ['name' ]} \n \n "
7572 if "message" in msg :
76- final_message += f"Message: { msg ['message' ]} \n "
73+ final_message += f"Message: { msg ['message' ]} \n \n "
7774 if "exception" in msg :
78- final_message += f"Exception: { msg ['exception' ]} \n "
75+ final_message += f"Exception: { msg ['exception' ]} \n \n "
7976 return final_message
8077
78+ def handle_eventbridge_cost_anomaly_event (event : Dict [str , Any ], _ ) -> Dict [str , Any ]:
79+ """Main Lambda handler for processing EventBridge AWS cost anomaly events."""
80+ return generic_handle_eventbridge_event_with_single_channel (event )
81+
8182
8283def handle_eventbridge_aws_error_event (event : Dict [str , Any ], _ ) -> Dict [str , Any ]:
8384 """Main Lambda handler for processing EventBridge AWS error events."""
85+ return generic_handle_eventbridge_event_with_single_channel (event )
86+
87+ def generic_handle_eventbridge_event_with_single_channel (event : Dict [str , Any ]) -> Dict [str , Any ]:
8488 logger .info ("Received event: %s" , json .dumps (event ))
8589 try :
86- webhook_url = get_teams_webhook_url ( )
90+ webhook_url = get_secret_by_name_and_key ( TEAMS_WEBHOOK_SECRET_NAME , TEAMS_WEBHOOK_SECRET_KEY_IMPORTANT_MESSAGES )
8791 records : List [Dict [str , Any ]] = event .get ("Records" , [])
8892
8993 for record in records :
@@ -99,20 +103,37 @@ def handle_eventbridge_aws_error_event(event: Dict[str, Any], _) -> Dict[str, An
99103 return {"statusCode" : 500 , "body" : "Failed to process SNS messages." }
100104
101105
106+ def is_important_sns_message (sns_message : Dict [str , Any ], log_source_name : str ) -> bool :
107+ """Check if the SNS message is important."""
108+ if 'message' in sns_message and 'installHook.js.map' in sns_message ['message' ]:
109+ return False
110+ if 'message' in sns_message and 'NOT_AUTHENTICATED' in sns_message ['message' ] and log_source_name == 'entity_core' :
111+ return False
112+ return True
113+
102114
103115def handle_log_event (event : Dict [str , Any ], _ ) -> Dict [str , Any ]:
104116 """Main Lambda handler for processing SNS events."""
105117 logger .info ("Received event: %s" , json .dumps (event ))
106118
107119 try :
108- webhook_url = get_teams_webhook_url ()
120+ webhook_url_important = get_secret_by_name_and_key (TEAMS_WEBHOOK_SECRET_NAME , TEAMS_WEBHOOK_SECRET_KEY_IMPORTANT_MESSAGES )
121+ webhook_url_unimportant = get_secret_by_name_and_key (TEAMS_WEBHOOK_SECRET_NAME , TEAMS_WEBHOOK_SECRET_KEY_UNIMPORTANT_MESSAGES )
122+ if 'UNIQUE_SHORT_NAME' not in os .environ :
123+ raise ValueError ("Missing environment variable: UNIQUE_SHORT_NAME" )
124+ log_source_name = os .environ .get ('UNIQUE_SHORT_NAME' , "" )
109125 records : List [Dict [str , Any ]] = event .get ("Records" , [])
110126
111127 for record in records :
112128 raw_sns_message = record .get ("Sns" , {}).get ("Message" , {})
113- sns_message = parse_ecs_json_to_readable_message (raw_sns_message )
129+ if isinstance (raw_sns_message , str ):
130+ raw_sns_message = json .loads (raw_sns_message )
131+ sns_message = parse_log_event_json_to_readable_message (raw_sns_message )
114132 logger .info ("Processing SNS message: %s" , sns_message [:500 ]) # limit log size
115- send_to_teams (sns_message , webhook_url )
133+ if is_important_sns_message (raw_sns_message , log_source_name ):
134+ send_to_teams (sns_message , webhook_url_important )
135+ else :
136+ send_to_teams (sns_message , webhook_url_unimportant )
116137
117138 return {"statusCode" : 200 , "body" : "Messages sent to Teams." }
118139
@@ -123,8 +144,6 @@ def handle_log_event(event: Dict[str, Any], _) -> Dict[str, Any]:
123144def send_to_teams (message : str , webhook_url : str ) -> None :
124145 """Send a simple text message to a Microsoft Teams channel."""
125146 headers = {'Content-Type' : 'application/json' }
126- # TODO ugly hack: apparently teams expects markdown, so 2 newlines to get separate lines
127- message = message .replace ('\n ' , '\n \n ' )
128147 teams_payload = {
129148 'text' : f"{ message } "
130149 }
0 commit comments