Skip to content

Commit 0f6bf5f

Browse files
Update the lambdas which deal with the SNS notifications to Teams channels
1 parent f7aff6b commit 0f6bf5f

File tree

2 files changed

+54
-33
lines changed

2 files changed

+54
-33
lines changed

sns_entries_to_teams/lambda.tf

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ resource "aws_lambda_function" "function" {
1919

2020
environment {
2121
variables = {
22-
TEAMS_WEBHOOK_SECRET_NAME = var.webhook_secret_arn
23-
TEAMS_WEBHOOK_SECRET_KEY = var.webhook_secret_key
22+
TEAMS_WEBHOOK_SECRET_NAME = var.webhook_secret_arn
23+
TEAMS_WEBHOOK_SECRET_KEY = var.webhook_secret_key
24+
TEAMS_WEBHOOK_SECRET_KEY_UNIMPORTANT = "${var.webhook_secret_key}_unimportant"
25+
UNIQUE_SHORT_NAME = var.unique_short_name
2426
}
2527
}
2628
architectures = ["arm64"] # should be cheaper

sns_entries_to_teams/src/aws_json_log_sns_to_teams.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
from botocore.exceptions import BotoCoreError, ClientError
77
import urllib.request
88
import logging
9-
from typing import Any, Dict, List, Optional, final
9+
from typing import Any, Dict, List, Optional
1010

1111
TEAMS_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

1516
logger: logging.Logger = logging.getLogger()
@@ -18,25 +19,23 @@
1819
secrets_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

8283
def 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

103115
def 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]:
123144
def 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

Comments
 (0)