44import tempfile
55from datetime import datetime , timedelta
66from itertools import chain
7- from typing import Any , Dict , List , Set
7+ from typing import Any , Dict , List , Optional , Set
88
99import certifi
1010import humanize
1111from dateutil import tz
1212from slack_sdk import WebClient
13- from slack_sdk .http_retry import all_builtin_retry_handlers
1413from slack_sdk .errors import SlackApiError
14+ from slack_sdk .http_retry import all_builtin_retry_handlers
1515
1616from robusta .core .model .base_params import AIInvestigateParams , ResourceInfo
1717from robusta .core .model .env_vars import (
1818 ADDITIONAL_CERTIFICATE ,
19+ HOLMES_ASK_SLACK_BUTTON_ENABLED ,
1920 HOLMES_ENABLED ,
2021 SLACK_REQUEST_TIMEOUT ,
21- SLACK_TABLE_COLUMNS_LIMIT , HOLMES_ASK_SLACK_BUTTON_ENABLED ,
22+ SLACK_TABLE_COLUMNS_LIMIT ,
2223)
2324from robusta .core .playbooks .internal .ai_integration import ask_holmes
2425from robusta .core .reporting .base import Emojis , EnrichmentType , Finding , FindingStatus , LinkType
@@ -69,7 +70,12 @@ def __init__(self, slack_token: str, account_id: str, cluster_name: str, signing
6970 except Exception as e :
7071 logging .exception (f"Failed to use custom certificate. { e } " )
7172
72- self .slack_client = WebClient (token = slack_token , ssl = ssl_context , timeout = SLACK_REQUEST_TIMEOUT , retry_handlers = all_builtin_retry_handlers ())
73+ self .slack_client = WebClient (
74+ token = slack_token ,
75+ ssl = ssl_context ,
76+ timeout = SLACK_REQUEST_TIMEOUT ,
77+ retry_handlers = all_builtin_retry_handlers (),
78+ )
7379 self .signing_key = signing_key
7480 self .account_id = account_id
7581 self .cluster_name = cluster_name
@@ -213,37 +219,74 @@ def __to_slack(self, block: BaseBlock, sink_name: str) -> List[SlackBlock]:
213219 logging .warning (f"cannot convert block of type { type (block )} to slack format block: { block } " )
214220 return [] # no reason to crash the entire report
215221
216- def __upload_file_to_slack (self , block : FileBlock , max_log_file_limit_kb : int ) -> str :
222+ def _upload_temp_file (self , f , file_reference , truncated_content : bytes , filename : str ) -> Optional [str ]:
223+ """Helper to upload a file-like or file path to Slack."""
224+ f .write (truncated_content )
225+ f .flush ()
226+ f .seek (0 )
227+
228+ result = self .slack_client .files_upload_v2 (
229+ title = filename ,
230+ file_uploads = [{"file" : file_reference , "filename" : filename , "title" : filename }],
231+ )
232+ return result ["file" ]["permalink" ]
233+
234+ def __upload_file_to_slack (self , block : FileBlock , max_log_file_limit_kb : int ) -> Optional [str ]:
235+ """Upload a file to Slack and return a permalink to it."""
217236 truncated_content = block .truncate_content (max_file_size_bytes = max_log_file_limit_kb * 1000 )
237+ filename = block .filename
218238
219- """Upload a file to slack and return a link to it"""
220- with tempfile .NamedTemporaryFile () as f :
221- f .write (truncated_content )
222- f .flush ()
223- result = self .slack_client .files_upload_v2 (title = block .filename , file = f .name , filename = block .filename )
224- return result ["file" ]["permalink" ]
239+ try :
240+ with tempfile .NamedTemporaryFile () as f :
241+ logging .debug ("Trying NamedTemporaryFile for Slack upload" )
242+ return self ._upload_temp_file (f , f .name , truncated_content , filename )
243+ except Exception as e :
244+ logging .debug (f"NamedTemporaryFile failed: { e } " )
245+ try :
246+ SPOOLED_FILE_SIZE = 10 * 1000 * 1000 # 10MB to protect against OOM
247+ with tempfile .SpooledTemporaryFile (max_size = SPOOLED_FILE_SIZE ) as f :
248+ logging .debug ("Trying SpooledTemporaryFile for Slack upload" )
249+ return self ._upload_temp_file (f , f , truncated_content , filename )
250+ except Exception as e2 :
251+ logging .exception (f"SpooledTemporaryFile also failed: { e2 } " )
252+ return None
225253
226254 def prepare_slack_text (self , message : str , max_log_file_limit_kb : int , files : List [FileBlock ] = []):
255+ error_files = []
256+
227257 if files :
228258 # it's a little annoying but it seems like files need to be referenced in `title` and not just `blocks`
229259 # in order to be actually shared. well, I'm actually not sure about that, but when I tried adding the files
230260 # to a separate block and not including them in `title` or the first block then the link was present but
231261 # the file wasn't actually shared and the link was broken
232262 uploaded_files = []
263+
233264 for file_block in files :
234265 # slack throws an error if you write empty files, so skip it
235266 if len (file_block .contents ) == 0 :
236267 continue
237268 permalink = self .__upload_file_to_slack (file_block , max_log_file_limit_kb = max_log_file_limit_kb )
238- uploaded_files .append (f"* <{ permalink } | { file_block .filename } >" )
269+ if permalink :
270+ uploaded_files .append (f"* <{ permalink } | { file_block .filename } >" )
271+ else :
272+ error_files .append (file_block .filename )
239273
240274 file_references = "\n " .join (uploaded_files )
241275 message = f"{ message } \n { file_references } "
242276
243277 if len (message ) == 0 :
244278 return "empty-message" # blank messages aren't allowed
279+ message = Transformer .apply_length_limit (message , MAX_BLOCK_CHARS )
245280
246- return Transformer .apply_length_limit (message , MAX_BLOCK_CHARS )
281+ if error_files :
282+ error_msg = (
283+ "_Failed to send file(s) "
284+ + ", " .join (error_files )
285+ + " to slack._\n _See robusta-runner logs for details._"
286+ )
287+ return message , error_msg
288+
289+ return message , None
247290
248291 def __send_blocks_to_slack (
249292 self ,
@@ -266,9 +309,12 @@ def __send_blocks_to_slack(
266309 file_blocks .extend (Transformer .tableblock_to_fileblocks (other_blocks , SLACK_TABLE_COLUMNS_LIMIT ))
267310 file_blocks .extend (Transformer .tableblock_to_fileblocks (report_attachment_blocks , SLACK_TABLE_COLUMNS_LIMIT ))
268311
269- message = self .prepare_slack_text (
312+ message , error_msg = self .prepare_slack_text (
270313 title , max_log_file_limit_kb = sink_params .max_log_file_limit_kb , files = file_blocks
271314 )
315+ if error_msg :
316+ other_blocks .append (MarkdownBlock (error_msg ))
317+
272318 output_blocks = []
273319 for block in other_blocks :
274320 output_blocks .extend (self .__to_slack (block , sink_params .name ))
@@ -310,15 +356,14 @@ def __send_blocks_to_slack(
310356 f"error sending message to slack\n e={ e } \n text={ message } \n channel={ channel } \n blocks={ * output_blocks ,} \n attachment_blocks={ * attachment_blocks ,} "
311357 )
312358
313-
314359 def __limit_labels_size (self , labels : dict , max_size : int = 1000 ) -> dict :
315360 # slack can only send 2k tokens in a callback so the labels are limited in size
316361
317362 low_priority_labels = ["job" , "prometheus" , "severity" , "service" ]
318363 current_length = len (str (labels ))
319364 if current_length <= max_size :
320365 return labels
321-
366+
322367 limited_labels = copy .deepcopy (labels )
323368
324369 # first remove the low priority labels if needed
@@ -348,7 +393,7 @@ def __create_holmes_callback(self, finding: Finding) -> CallbackBlock:
348393 "robusta_issue_id" : str (finding .id ),
349394 "issue_type" : finding .aggregation_key ,
350395 "source" : finding .source .name ,
351- "labels" : self .__limit_labels_size (labels = finding .subject .labels )
396+ "labels" : self .__limit_labels_size (labels = finding .subject .labels ),
352397 }
353398
354399 return CallbackBlock (
@@ -714,12 +759,7 @@ def update_slack_message(self, channel: str, ts: str, blocks: list, text: str =
714759 return
715760
716761 # Call Slack's chat_update method
717- resp = self .slack_client .chat_update (
718- channel = channel ,
719- ts = ts ,
720- text = text ,
721- blocks = blocks
722- )
762+ resp = self .slack_client .chat_update (channel = channel , ts = ts , text = text , blocks = blocks )
723763 logging .debug (f"Message updated successfully: { resp ['ts' ]} " )
724764 return resp ["ts" ]
725765
0 commit comments