robusta-api 0.46.0__tar.gz → 0.47.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. {robusta_api-0.46.0 → robusta_api-0.47.0}/PKG-INFO +1 -1
  2. {robusta_api-0.46.0 → robusta_api-0.47.0}/pyproject.toml +1 -1
  3. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/_version.py +1 -1
  4. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/blocks.py +116 -22
  5. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/sink_base.py +11 -0
  6. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/slack_sink.py +10 -1
  7. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/slack/sender.py +150 -11
  8. {robusta_api-0.46.0 → robusta_api-0.47.0}/LICENSE +0 -0
  9. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/__init__.py +0 -0
  10. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/api/__init__.py +0 -0
  11. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/clients/robusta_client.py +0 -0
  12. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/__init__.py +0 -0
  13. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/discovery/__init__.py +0 -0
  14. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/discovery/discovery.py +0 -0
  15. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/discovery/resource_names.py +0 -0
  16. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/discovery/top_service_resolver.py +0 -0
  17. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/discovery/utils.py +0 -0
  18. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/exceptions.py +0 -0
  19. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/__init__.py +0 -0
  20. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/base_params.py +0 -0
  21. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/cluster_status.py +0 -0
  22. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/env_vars.py +0 -0
  23. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/events.py +0 -0
  24. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/helm_release.py +0 -0
  25. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/jobs.py +0 -0
  26. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/k8s_operation_type.py +0 -0
  27. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/namespaces.py +0 -0
  28. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/nodes.py +0 -0
  29. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/openshift_group.py +0 -0
  30. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/pods.py +0 -0
  31. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/runner_config.py +0 -0
  32. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/model/services.py +0 -0
  33. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/persistency/__init__.py +0 -0
  34. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/persistency/in_memory.py +0 -0
  35. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/persistency/scheduled_jobs_states_dal.py +0 -0
  36. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/__init__.py +0 -0
  37. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/actions_registry.py +0 -0
  38. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/base_trigger.py +0 -0
  39. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/common.py +0 -0
  40. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/container_playbook_utils.py +0 -0
  41. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/crash_reporter.py +0 -0
  42. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/generation.py +0 -0
  43. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/internal/CRD_API_DOCUMENTATION.md +0 -0
  44. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/internal/__init__.py +0 -0
  45. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/internal/ai_integration.py +0 -0
  46. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/internal/crds.py +0 -0
  47. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/internal/discovery_events.py +0 -0
  48. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/job_utils.py +0 -0
  49. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/node_enrichment_utils.py +0 -0
  50. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/node_playbook_utils.py +0 -0
  51. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/oom_killer_utils.py +0 -0
  52. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/playbook_utils.py +0 -0
  53. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/playbooks_event_handler.py +0 -0
  54. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/playbooks_event_handler_impl.py +0 -0
  55. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/pod_utils/crashloop_utils.py +0 -0
  56. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/pod_utils/imagepull_utils.py +0 -0
  57. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/pod_utils/pending_pod_utils.py +0 -0
  58. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/prometheus_enrichment_utils.py +0 -0
  59. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/playbooks/trigger.py +0 -0
  60. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/pubsub/__init__.py +0 -0
  61. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/pubsub/event_emitter.py +0 -0
  62. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/pubsub/event_subscriber.py +0 -0
  63. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/pubsub/events_pubsub.py +0 -0
  64. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/__init__.py +0 -0
  65. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/action_requests.py +0 -0
  66. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/base.py +0 -0
  67. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/callbacks.py +0 -0
  68. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/consts.py +0 -0
  69. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/custom_rendering.py +0 -0
  70. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/finding_subjects.py +0 -0
  71. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/holmes.py +0 -0
  72. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/url_helpers.py +0 -0
  73. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/reporting/utils.py +0 -0
  74. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/schedule/__init__.py +0 -0
  75. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/schedule/model.py +0 -0
  76. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/schedule/scheduler.py +0 -0
  77. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/__init__.py +0 -0
  78. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/common/__init__.py +0 -0
  79. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/common/channel_transformer.py +0 -0
  80. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/common/html_tools.py +0 -0
  81. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/datadog/__init__.py +0 -0
  82. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/datadog/datadog_sink.py +0 -0
  83. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/datadog/datadog_sink_params.py +0 -0
  84. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/discord/__init__.py +0 -0
  85. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/discord/discord_sink.py +0 -0
  86. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/discord/discord_sink_params.py +0 -0
  87. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/file/__init__.py +0 -0
  88. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/file/file_sink.py +0 -0
  89. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/file/file_sink_params.py +0 -0
  90. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/file/object_traverser.py +0 -0
  91. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/google_chat/__init__.py +0 -0
  92. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/google_chat/google_chat.py +0 -0
  93. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/google_chat/google_chat_params.py +0 -0
  94. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/incidentio/__init__.py +0 -0
  95. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/incidentio/incidentio_api.py +0 -0
  96. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/incidentio/incidentio_client.py +0 -0
  97. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/incidentio/incidentio_sink.py +0 -0
  98. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/incidentio/incidentio_sink_params.py +0 -0
  99. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/jira/__init__.py +0 -0
  100. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/jira/jira_sink.py +0 -0
  101. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/jira/jira_sink_params.py +0 -0
  102. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/kafka/__init__.py +0 -0
  103. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/kafka/kafka_sink.py +0 -0
  104. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/kafka/kafka_sink_params.py +0 -0
  105. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/mail/__init__.py +0 -0
  106. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/mail/mail_sink.py +0 -0
  107. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/mail/mail_sink_params.py +0 -0
  108. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/mattermost/__init__.py +0 -0
  109. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/mattermost/mattermost_sink.py +0 -0
  110. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/mattermost/mattermost_sink_params.py +0 -0
  111. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/msteams/__init__.py +0 -0
  112. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/msteams/msteams_sink.py +0 -0
  113. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/msteams/msteams_sink_params.py +0 -0
  114. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/msteams/msteams_webhook_tranformer.py +0 -0
  115. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/opsgenie/__init__.py +0 -0
  116. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/opsgenie/opsgenie_sink.py +0 -0
  117. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/opsgenie/opsgenie_sink_params.py +0 -0
  118. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pagerduty/__init__.py +0 -0
  119. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pagerduty/pagerduty_sink.py +0 -0
  120. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pagerduty/pagerduty_sink_params.py +0 -0
  121. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pushover/__init__.py +0 -0
  122. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pushover/pushover_client.py +0 -0
  123. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pushover/pushover_sink.py +0 -0
  124. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/pushover/pushover_sink_params.py +0 -0
  125. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/__init__.py +0 -0
  126. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/dal/__init__.py +0 -0
  127. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/dal/model_conversion.py +0 -0
  128. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/dal/supabase_dal.py +0 -0
  129. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/discovery_metrics.py +0 -0
  130. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/prometheus_discovery_utils.py +0 -0
  131. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/robusta_sink.py +0 -0
  132. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/robusta_sink_params.py +0 -0
  133. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/rrm/__init__.py +0 -0
  134. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/rrm/account_resource_fetcher.py +0 -0
  135. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/rrm/base_resource_manager.py +0 -0
  136. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/rrm/prometheus_alert_resource_manager.py +0 -0
  137. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/rrm/rrm.py +0 -0
  138. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/robusta/rrm/types.py +0 -0
  139. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/rocketchat/__init__.py +0 -0
  140. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/rocketchat/rocketchat_sink.py +0 -0
  141. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/rocketchat/rocketchat_sink_params.py +0 -0
  142. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/servicenow/__init__.py +0 -0
  143. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/servicenow/servicenow_sink.py +0 -0
  144. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/servicenow/servicenow_sink_params.py +0 -0
  145. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/sink_base_params.py +0 -0
  146. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/sink_config.py +0 -0
  147. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/sink_factory.py +0 -0
  148. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/__init__.py +0 -0
  149. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/preview/slack_sink_preview.py +0 -0
  150. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/preview/slack_sink_preview_params.py +0 -0
  151. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/slack_sink_params.py +0 -0
  152. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/templates/header.j2 +0 -0
  153. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/slack/templates/template_loader.py +0 -0
  154. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/telegram/__init__.py +0 -0
  155. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/telegram/telegram_client.py +0 -0
  156. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/telegram/telegram_sink.py +0 -0
  157. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/telegram/telegram_sink_params.py +0 -0
  158. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/timing.py +0 -0
  159. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/transformer.py +0 -0
  160. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/victorops/__init__.py +0 -0
  161. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/victorops/victorops_sink.py +0 -0
  162. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/victorops/victorops_sink_params.py +0 -0
  163. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/webex/__init__.py +0 -0
  164. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/webex/webex_sink.py +0 -0
  165. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/webex/webex_sink_params.py +0 -0
  166. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/webhook/__init__.py +0 -0
  167. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/webhook/webhook_sink.py +0 -0
  168. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/webhook/webhook_sink_params.py +0 -0
  169. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/yamessenger/__init__.py +0 -0
  170. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/yamessenger/yamessenger_client.py +0 -0
  171. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/yamessenger/yamessenger_sink.py +0 -0
  172. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/yamessenger/yamessenger_sink_params.py +0 -0
  173. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/zulip/__init__.py +0 -0
  174. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/zulip/zulip_sink.py +0 -0
  175. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/sinks/zulip/zulip_sink_params.py +0 -0
  176. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/stream/utils.py +0 -0
  177. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/container_oom_killed_trigger.py +0 -0
  178. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/custom_triggers.py +0 -0
  179. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/error_event_trigger.py +0 -0
  180. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/helm_releases_triggers.py +0 -0
  181. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/job_failed_trigger.py +0 -0
  182. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/multi_resources_trigger.py +0 -0
  183. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/oom_killed_trigger_base.py +0 -0
  184. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/pod_crash_loop_trigger.py +0 -0
  185. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/pod_evicted_trigger.py +0 -0
  186. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/pod_image_pull_backoff.py +0 -0
  187. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/core/triggers/pod_oom_killed_trigger.py +0 -0
  188. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/__init__.py +0 -0
  189. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/argocd/argocd_client.py +0 -0
  190. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/common/__init__.py +0 -0
  191. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/common/requests.py +0 -0
  192. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/discord/__init__.py +0 -0
  193. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/discord/sender.py +0 -0
  194. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/git/__init__.py +0 -0
  195. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/git/git_repo.py +0 -0
  196. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/git/well_known_hosts.py +0 -0
  197. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/google_chat/__init__.py +0 -0
  198. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/google_chat/sender.py +0 -0
  199. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/grafana.py +0 -0
  200. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/helper.py +0 -0
  201. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/jira/__init__.py +0 -0
  202. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/jira/client.py +0 -0
  203. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/jira/sender.py +0 -0
  204. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/__init__.py +0 -0
  205. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/api_client_utils.py +0 -0
  206. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/autogenerated/__init__.py +0 -0
  207. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/autogenerated/events.py +0 -0
  208. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/autogenerated/models.py +0 -0
  209. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/autogenerated/triggers.py +0 -0
  210. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/autogenerated/v1/__init__.py +0 -0
  211. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/autogenerated/v1/models.py +0 -0
  212. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/base_event.py +0 -0
  213. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/base_triggers.py +0 -0
  214. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/custom_crds.py +0 -0
  215. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/custom_models.py +0 -0
  216. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/model_not_found_exception.py +0 -0
  217. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/process_utils.py +0 -0
  218. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/kubernetes/templates.py +0 -0
  219. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/mail/__init__.py +0 -0
  220. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/mail/sender.py +0 -0
  221. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/mattermost/__init__.py +0 -0
  222. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/mattermost/client.py +0 -0
  223. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/mattermost/sender.py +0 -0
  224. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/__init__.py +0 -0
  225. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files.py +0 -0
  226. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files_image.py +0 -0
  227. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_adaptive_card_files_text.py +0 -0
  228. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/__init__.py +0 -0
  229. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_action.py +0 -0
  230. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_base.py +0 -0
  231. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_card.py +0 -0
  232. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_column.py +0 -0
  233. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_container.py +0 -0
  234. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_images.py +0 -0
  235. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_table.py +0 -0
  236. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_elements/msteams_text_block.py +0 -0
  237. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_mark_down_fix_url.py +0 -0
  238. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/msteams_msg.py +0 -0
  239. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/msteams/sender.py +0 -0
  240. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/openshift/__init__.py +0 -0
  241. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/openshift/token.py +0 -0
  242. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/prometheus/__init__.py +0 -0
  243. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/prometheus/models.py +0 -0
  244. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/prometheus/trigger.py +0 -0
  245. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/prometheus/utils.py +0 -0
  246. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/receiver.py +0 -0
  247. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/resource_analysis/__init__.py +0 -0
  248. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/resource_analysis/cpu_analyzer.py +0 -0
  249. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/resource_analysis/memory_analyzer.py +0 -0
  250. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/resource_analysis/node_cpu_analyzer.py +0 -0
  251. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/resource_analysis/prometheus_analyzer.py +0 -0
  252. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/rocketchat/__init__.py +0 -0
  253. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/rocketchat/sender.py +0 -0
  254. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/__init__.py +0 -0
  255. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/event.py +0 -0
  256. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/models.py +0 -0
  257. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/playbook_scheduler.py +0 -0
  258. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/playbook_scheduler_manager.py +0 -0
  259. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/playbook_scheduler_manager_impl.py +0 -0
  260. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/scheduled/trigger.py +0 -0
  261. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/servicenow/__init__.py +0 -0
  262. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/servicenow/sender.py +0 -0
  263. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/slack/__init__.py +0 -0
  264. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/webex/__init__.py +0 -0
  265. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/webex/sender.py +0 -0
  266. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/zulip/__init__.py +0 -0
  267. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/integrations/zulip/sender.py +0 -0
  268. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/model/alert_relabel_config.py +0 -0
  269. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/model/config.py +0 -0
  270. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/model/playbook_action.py +0 -0
  271. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/model/playbook_definition.py +0 -0
  272. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/patch/patch.py +0 -0
  273. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/__init__.py +0 -0
  274. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/config_loader.py +0 -0
  275. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/log_init.py +0 -0
  276. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/main.py +0 -0
  277. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/not_found_exception.py +0 -0
  278. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/object_updater.py +0 -0
  279. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/process_setup.py +0 -0
  280. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/ssl_utils.py +0 -0
  281. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/telemetry.py +0 -0
  282. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/telemetry_service.py +0 -0
  283. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/web.py +0 -0
  284. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/runner/web_api.py +0 -0
  285. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/__init__.py +0 -0
  286. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/auth_provider.py +0 -0
  287. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/base64_utils.py +0 -0
  288. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/cluster_provider_discovery.py +0 -0
  289. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/common.py +0 -0
  290. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/decorators.py +0 -0
  291. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/docs.py +0 -0
  292. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/documented_pydantic.py +0 -0
  293. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/error_codes.py +0 -0
  294. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/file_system_watcher.py +0 -0
  295. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/function_hashes.py +0 -0
  296. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/json_schema.py +0 -0
  297. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/parsing.py +0 -0
  298. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/rate_limiter.py +0 -0
  299. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/scope.py +0 -0
  300. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/server_start.py +0 -0
  301. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/service_discovery.py +0 -0
  302. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/silence_utils.py +0 -0
  303. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/stack_tracer.py +0 -0
  304. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/task_queue.py +0 -0
  305. {robusta_api-0.46.0 → robusta_api-0.47.0}/src/robusta/utils/time_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robusta-api
3
- Version: 0.46.0
3
+ Version: 0.47.0
4
4
  Summary:
5
5
  Author: Arik Alon
6
6
  Author-email: arikalon1@users.noreply.github.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "robusta-api"
3
- version = "0.46.0"
3
+ version = "0.47.0"
4
4
  description = ""
5
5
  authors = ["Arik Alon <arikalon1@users.noreply.github.com>"]
6
6
  packages = [
@@ -1,2 +1,2 @@
1
1
  # this is updated by .github/workflows/release.yaml
2
- __version__ = "0.46.0"
2
+ __version__ = "0.47.0"
@@ -38,6 +38,9 @@ from robusta.core.reporting.custom_rendering import render_value
38
38
 
39
39
  BLOCK_SIZE_LIMIT = 2997 # due to slack block size limit of 3000
40
40
 
41
+ # Don't shrink a text column below this, or its values become unreadable.
42
+ TABLE_MIN_COLUMN_WIDTH = 4
43
+
41
44
 
42
45
  class MarkdownBlock(BaseBlock):
43
46
  """
@@ -364,25 +367,53 @@ class TableBlock(BaseBlock):
364
367
  if table_format:
365
368
  self.metadata["format"] = TableBlockFormat.vertical.value
366
369
 
370
+ @classmethod
371
+ def __is_number(cls, value) -> bool:
372
+ try:
373
+ float(str(value))
374
+ return True
375
+ except (TypeError, ValueError):
376
+ return False
377
+
378
+ @classmethod
379
+ def __numeric_column_indices(cls, rendered_rows, num_columns: int) -> set:
380
+ # Numeric columns (e.g. counters) are kept at full width.
381
+ numeric_indices = set()
382
+ for idx in range(num_columns):
383
+ non_empty = [str(row[idx]) for row in rendered_rows if idx < len(row) and str(row[idx]).strip()]
384
+ if non_empty and all(cls.__is_number(v) for v in non_empty):
385
+ numeric_indices.add(idx)
386
+ return numeric_indices
387
+
367
388
  @classmethod
368
389
  def __calc_max_width(cls, headers, rendered_rows, table_max_width: int) -> List[int]:
369
- # We need to make sure the total table width, doesn't exceed the max width,
370
- # otherwise, the table is printed corrupted
371
- columns_max_widths = [len(header) for header in headers]
390
+ # Keep the total table width within the max, otherwise it renders corrupted.
391
+ num_columns = max(len(headers), max((len(row) for row in rendered_rows), default=0))
392
+ columns_max_widths = [0] * num_columns
393
+ for idx, header in enumerate(headers):
394
+ columns_max_widths[idx] = len(str(header))
372
395
  for row in rendered_rows:
373
396
  for idx, val in enumerate(row):
374
397
  columns_max_widths[idx] = max(len(str(val)), columns_max_widths[idx])
375
398
 
376
- if sum(columns_max_widths) > table_max_width: # We want to limit the widest column
377
- largest_width = max(columns_max_widths)
378
- widest_column_idx = columns_max_widths.index(largest_width)
379
- diff = sum(columns_max_widths) - table_max_width
380
- columns_max_widths[widest_column_idx] = largest_width - diff
381
- if columns_max_widths[widest_column_idx] < 0: # in case the diff is bigger than the largest column
382
- # just divide equally
383
- columns_max_widths = [
384
- int(table_max_width / len(columns_max_widths)) for i in range(0, len(columns_max_widths))
385
- ]
399
+ if sum(columns_max_widths) <= table_max_width:
400
+ return columns_max_widths
401
+
402
+ # Only shrink text columns, so numeric columns never get wrapped. If every
403
+ # column is numeric, leave the widths as-is rather than wrapping numbers.
404
+ numeric_indices = cls.__numeric_column_indices(rendered_rows, num_columns)
405
+ shrinkable = [idx for idx in range(num_columns) if idx not in numeric_indices]
406
+ if not shrinkable:
407
+ return columns_max_widths
408
+
409
+ # Water-fill: trim the widest shrinkable column by one char at a time (fair
410
+ # distribution), never below TABLE_MIN_COLUMN_WIDTH.
411
+ while sum(columns_max_widths) > table_max_width:
412
+ candidates = [idx for idx in shrinkable if columns_max_widths[idx] > TABLE_MIN_COLUMN_WIDTH]
413
+ if not candidates:
414
+ break
415
+ widest = max(candidates, key=lambda idx: columns_max_widths[idx])
416
+ columns_max_widths[widest] -= 1
386
417
 
387
418
  return columns_max_widths
388
419
 
@@ -410,25 +441,88 @@ class TableBlock(BaseBlock):
410
441
 
411
442
  return "\n".join(lines[:lines_to_include]) + truncator
412
443
 
444
+ @staticmethod
445
+ def default_omission_note(omitted_rows: List[List[str]]) -> str:
446
+ return f"... {len(omitted_rows)} more rows not shown"
447
+
448
+ def __render_within_budget(self, max_chars: int, table_max_width: int, table_fmt: str, omission_note) -> str:
449
+ """Render the table, dropping whole rows to fit max_chars.
450
+
451
+ Values themselves are never cut - long ones wrap onto extra lines - so rows must be
452
+ dropped as complete units. Cutting by physical line would leave a wrapped value's
453
+ continuation behind and show a partial value as if it were the whole thing.
454
+ """
455
+ rows = self.__to_strings_rows(self.render_rows())
456
+ # Column widths are computed once from all rows, so dropping rows never reflows
457
+ # the columns and the rendered size shrinks monotonically with the row count.
458
+ col_max_width = self.__calc_max_width(self.headers, rows, table_max_width)
459
+
460
+ def render(subset: List[List[str]]) -> str:
461
+ if not subset: # tabulate raises IndexError on maxcolwidths with no rows
462
+ return tabulate(subset, headers=self.headers, tablefmt=table_fmt)
463
+ return tabulate(subset, headers=self.headers, tablefmt=table_fmt, maxcolwidths=col_max_width)
464
+
465
+ full = render(rows)
466
+ if len(full) <= max_chars:
467
+ return full
468
+
469
+ # Largest number of whole rows that fits alongside the "N more rows" note.
470
+ best = None
471
+ low, high = 0, len(rows)
472
+ while low <= high:
473
+ mid = (low + high) // 2
474
+ candidate = render(rows[:mid])
475
+ if mid < len(rows):
476
+ candidate = f"{candidate}\n{omission_note(rows[mid:])}"
477
+ if len(candidate) <= max_chars:
478
+ best = candidate
479
+ low = mid + 1
480
+ else:
481
+ high = mid - 1
482
+
483
+ # Fall back to the line-based trim if not even a header-only table fits.
484
+ return best if best is not None else self.__trim_rows(full, max_chars)
485
+
413
486
  @classmethod
414
487
  def __to_strings_rows(cls, rows):
415
488
  # This is just to assert all row column values are strings. Tabulate might fail on other types
416
489
  return [list(map(lambda column_value: str(column_value), row)) for row in rows]
417
490
 
418
- def to_markdown(self, max_chars=None, add_table_header: bool = True) -> MarkdownBlock:
419
- table_header = f"{self.table_name}\n" if self.table_name else ""
420
- table_header = "" if not add_table_header else table_header
421
- prefix = f"{table_header}```\n"
422
- suffix = "\n```"
423
- table_contents = self.to_table_string()
424
- if max_chars is not None:
425
- max_chars = max_chars - len(prefix) - len(suffix)
426
- table_contents = self.__trim_rows(table_contents, max_chars)
491
+ def to_markdown(self, max_chars=None, add_table_header: bool = True, omission_note=None) -> MarkdownBlock:
492
+ """:param omission_note: builds the note shown when rows are dropped to fit the size limit.
493
+ Receives the dropped rows, so callers can summarise what was left out (e.g. residual counts)."""
494
+ # Stay under BLOCK_SIZE_LIMIT even when no caller-supplied limit: MarkdownBlock
495
+ # would otherwise blindly cut the text and lose the closing ``` fence, which
496
+ # makes the whole table render as unformatted (non-monospace) text.
497
+ budget = BLOCK_SIZE_LIMIT - 1 if max_chars is None else min(max_chars, BLOCK_SIZE_LIMIT - 1)
498
+
499
+ table_header = f"{self.table_name}\n" if self.table_name and add_table_header else ""
500
+ opening_fence, suffix = "```\n", "\n```"
501
+ # The table name is caller-supplied and can be arbitrarily long, so trim it rather than
502
+ # let the envelope alone push the block past the limit and cost us the closing fence.
503
+ table_header = table_header[: max(0, budget - len(opening_fence) - len(suffix))]
504
+ prefix = f"{table_header}{opening_fence}"
505
+
506
+ content_budget = budget - len(prefix) - len(suffix)
507
+ # With no room left for content, emit an empty (but properly closed) block rather than
508
+ # an overflow marker that wouldn't fit either.
509
+ table_contents = (
510
+ self.__render_within_budget(
511
+ content_budget,
512
+ PRINTED_TABLE_MAX_WIDTH,
513
+ "presto",
514
+ omission_note or self.default_omission_note,
515
+ )
516
+ if content_budget > 0
517
+ else ""
518
+ )
427
519
 
428
520
  return MarkdownBlock(f"{prefix}{table_contents}{suffix}")
429
521
 
430
522
  def to_table_string(self, table_max_width: int = PRINTED_TABLE_MAX_WIDTH, table_fmt: str = "presto") -> str:
431
523
  rendered_rows = self.__to_strings_rows(self.render_rows())
524
+ if not rendered_rows: # tabulate raises IndexError on maxcolwidths with no rows
525
+ return tabulate(rendered_rows, headers=self.headers, tablefmt=table_fmt)
432
526
  col_max_width = self.__calc_max_width(self.headers, rendered_rows, table_max_width)
433
527
  return tabulate(
434
528
  rendered_rows,
@@ -36,6 +36,11 @@ class NotificationSummary(BaseModel):
36
36
  start_ts: float = Field(default_factory=lambda: time.time()) # Timestamp of the first notification
37
37
  # Keys for the table are determined by grouping.notification_mode.summary.by
38
38
  summary_table: DefaultDict[KeyT, List[int]] = None
39
+ # The full summary table is attached as a file when it doesn't fit in the message. Files
40
+ # can't be edited in place, so the previous one is deleted whenever a new one is uploaded.
41
+ attachment_file_id: Optional[str] = None
42
+ attachment_permalink: Optional[str] = None
43
+ attachment_ts: float = 0 # when the attachment was last refreshed
39
44
 
40
45
  def register_notification(self, summary_key: KeyT, resolved: bool, interval: int):
41
46
  now_ts = time.time()
@@ -45,6 +50,12 @@ class NotificationSummary(BaseModel):
45
50
  self.summary_table = defaultdict(lambda: [0, 0])
46
51
  self.start_ts = now_ts
47
52
  self.message_id = None
53
+ # Forget the previous interval's attachment entirely. Its summary message stays in
54
+ # the channel and keeps linking that file, so it must not be deleted later - only
55
+ # files superseded within the same message may be.
56
+ self.attachment_file_id = None
57
+ self.attachment_permalink = None
58
+ self.attachment_ts = 0
48
59
  self.summary_table[summary_key][idx] += 1
49
60
 
50
61
 
@@ -1,4 +1,6 @@
1
+ import json
1
2
  import logging
3
+ from urllib.parse import urlencode
2
4
 
3
5
  from robusta.core.model.env_vars import ROBUSTA_UI_DOMAIN
4
6
  from robusta.core.reporting.base import Finding, FindingStatus
@@ -89,6 +91,7 @@ class SlackSink(SinkBase):
89
91
  investigate_uri=investigate_uri,
90
92
  grouping_interval=self.params.grouping.interval,
91
93
  channel=resolved_channel,
94
+ summary_state=notification_summary,
92
95
  )
93
96
  notification_summary.message_id = slack_thread_ts
94
97
  should_send_notification = self.params.grouping.notification_mode.summary.threaded
@@ -104,7 +107,13 @@ class SlackSink(SinkBase):
104
107
  )
105
108
 
106
109
  def get_timeline_uri(self, account_id: str, cluster_name: str) -> str:
107
- return f"{ROBUSTA_UI_DOMAIN}/graphs?account_id={account_id}&cluster={cluster_name}"
110
+ # The timeline reads its filters as JSON-encoded query params, and expects "clusters"
111
+ # (a list) rather than a single "cluster" - passing the latter left the link unfiltered.
112
+ params = {
113
+ "account_id": account_id,
114
+ "clusters": json.dumps([cluster_name]),
115
+ }
116
+ return f"{ROBUSTA_UI_DOMAIN}/graphs?{urlencode(params)}"
108
117
 
109
118
  def __replace_callback_with_string(self, slack_message, block_id, message_string):
110
119
  """
@@ -1,11 +1,12 @@
1
1
  import copy
2
2
  import logging
3
+ import time
3
4
  import ssl
4
5
  import tempfile
5
6
  import re
6
7
  from datetime import datetime, timedelta
7
8
  from itertools import chain
8
- from typing import Any, Dict, List, Optional, Set, Union
9
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
9
10
  import certifi
10
11
  import humanize
11
12
  from dateutil import tz
@@ -56,6 +57,11 @@ ACTION_TRIGGER_PLAYBOOK = "trigger_playbook"
56
57
  ACTION_LINK = "link"
57
58
  SlackBlock = Dict[str, Any]
58
59
  MAX_BLOCK_CHARS = 3000
60
+ # A file can hold a far wider table than a message can.
61
+ SUMMARY_ATTACHMENT_TABLE_WIDTH = 250
62
+ # The summary message is rewritten on every notification; don't re-upload the attachment
63
+ # that often.
64
+ SUMMARY_ATTACHMENT_REFRESH_SECONDS = 60
59
65
  MENTION_PATTERN = re.compile(r"<[^>]+>")
60
66
 
61
67
 
@@ -860,6 +866,98 @@ class SlackSender:
860
866
  output_blocks=slack_blocks,
861
867
  )
862
868
 
869
+ @staticmethod
870
+ def __summary_table_block(table_block: TableBlock) -> Tuple[BaseBlock, int]:
871
+ """Render the summary table, noting the totals of any rows dropped to fit the size limit.
872
+
873
+ Without the residual counts the numbers in the table wouldn't add up to the notification
874
+ count in the message header, making the summary look wrong.
875
+
876
+ Returns the block and the number of rows that didn't fit.
877
+ """
878
+ if len(table_block.headers) <= 2:
879
+ return table_block, 0 # rendered as a bullet list rather than a table, see __to_slack_table
880
+
881
+ omitted_count = 0
882
+
883
+ def omission_note(omitted_rows: List[List[str]]) -> str:
884
+ nonlocal omitted_count
885
+ omitted_count = len(omitted_rows)
886
+ fired = resolved = 0
887
+ for row in omitted_rows:
888
+ try:
889
+ fired += int(row[-2])
890
+ resolved += int(row[-1])
891
+ except (IndexError, ValueError): # not the expected numeric columns - just count rows
892
+ return TableBlock.default_omission_note(omitted_rows)
893
+ return f"... {len(omitted_rows)} more groups ({fired} fired, {resolved} resolved) not shown"
894
+
895
+ block = table_block.to_markdown(omission_note=omission_note)
896
+ return block, omitted_count
897
+
898
+ def __refresh_summary_attachment(
899
+ self, table_block: TableBlock, summary_state, channel_id: Optional[str], thread_ts: Optional[str]
900
+ ) -> Tuple[Optional[str], Optional[str]]:
901
+ """Attach the complete table as a file, since the message can only show part of it.
902
+
903
+ Slack files are immutable, but the summary message is rewritten on every notification, so
904
+ a fresh file is uploaded rather than edited. That is throttled - re-uploading on every
905
+ single notification would be wasteful.
906
+
907
+ Returns the permalink and the id of the file it supersedes. The caller must only delete
908
+ that file once the message actually points at the new one, otherwise a failed update
909
+ leaves the message linking a file that no longer exists.
910
+ """
911
+ if summary_state is None:
912
+ return None, None
913
+
914
+ if not channel_id or not thread_ts:
915
+ # The file is shared into the summary message's own thread, so it needs that message
916
+ # to exist first. This only defers the attachment past the very first summary - the
917
+ # message is updated on the next notification, and the file appears then.
918
+ logging.debug("Summary message not posted yet, deferring the attachment")
919
+ return None, None
920
+
921
+ now = time.time()
922
+ if summary_state.attachment_permalink and now - summary_state.attachment_ts < SUMMARY_ATTACHMENT_REFRESH_SECONDS:
923
+ return summary_state.attachment_permalink, None
924
+
925
+ # A much wider table fits in a file than in a message, so nothing wraps there.
926
+ contents = table_block.to_table_string(table_max_width=SUMMARY_ATTACHMENT_TABLE_WIDTH)
927
+ file_block = FileBlock("alerts-summary.txt", contents.encode("utf-8"))
928
+ try:
929
+ resp = self.slack_client.files_upload_v2(
930
+ # Without a channel the file is only linkable, not shared: the permalink opens
931
+ # an empty preview that other members can't read or download. Sharing it into
932
+ # the summary's thread rather than the channel keeps the channel readable when
933
+ # there are many groups - otherwise every group adds a file message to it.
934
+ channel=channel_id,
935
+ thread_ts=thread_ts,
936
+ title=file_block.filename,
937
+ filename=file_block.filename,
938
+ content=file_block.contents,
939
+ )
940
+ permalink = resp["file"]["permalink"]
941
+ new_file_id = resp["file"]["id"]
942
+ except Exception:
943
+ logging.exception("Failed to upload the full summary table to Slack")
944
+ return summary_state.attachment_permalink, None
945
+
946
+ superseded_file_id = summary_state.attachment_file_id
947
+ summary_state.attachment_file_id = new_file_id
948
+ summary_state.attachment_permalink = permalink
949
+ summary_state.attachment_ts = now
950
+ return permalink, superseded_file_id
951
+
952
+ def __delete_superseded_attachment(self, file_id: Optional[str]) -> None:
953
+ if not file_id:
954
+ return
955
+ # Best effort - a leftover file is preferable to failing the summary update.
956
+ try:
957
+ self.slack_client.files_delete(file=file_id)
958
+ except Exception as e:
959
+ logging.warning(f"Could not delete the superseded summary attachment: {e}")
960
+
863
961
  def send_or_update_summary_message(
864
962
  self,
865
963
  group_by_classification_header: List[str],
@@ -873,12 +971,23 @@ class SlackSender:
873
971
  investigate_uri: str = None,
874
972
  grouping_interval: int = None, # in seconds
875
973
  channel: str = None, # pre-resolved channel (when channel_override uses labels/annotations)
974
+ summary_state=None, # NotificationSummary, for tracking the attachment across updates
876
975
  ):
877
976
  """Create or update a summary message with tabular information about the amount of events
878
977
  fired/resolved and a header describing the event group that this information concerns."""
879
978
  rows = []
880
979
  n_total_alerts = 0
881
- for key, value in sorted(summary_table.items()):
980
+ # Sort by firing count, then resolved count (both descending), so that when the table
981
+ # is too long to fit in a message the rows that get dropped are the least significant
982
+ # ones rather than whichever groups happen to sort last alphabetically. The key is only
983
+ # a final tie-break, to keep the order stable between updates of the same message. It is
984
+ # normalised to strings first: keys hold raw attribute values, which may mix None with
985
+ # strings (e.g. "workload" is None for a finding with no service) and aren't comparable.
986
+ def sort_key(item):
987
+ group_key, (fired, resolved) = item
988
+ return -fired, -resolved, tuple(str(value) for value in group_key)
989
+
990
+ for key, value in sorted(summary_table.items(), key=sort_key):
882
991
  # key is a tuple of attribute names; value is a 2-element list with
883
992
  # the number of firing and resolved notifications.
884
993
  row = list(str(e) for e in chain(key, value))
@@ -924,21 +1033,17 @@ class SlackSender:
924
1033
  else:
925
1034
  blocks.append(MarkdownBlock(text=cluster_txt))
926
1035
 
1036
+ summary_block, omitted_count = self.__summary_table_block(table_block)
927
1037
  blocks.extend(
928
1038
  [
929
1039
  MarkdownBlock(f"*Matching criteria*: {group_by_criteria_str}"),
930
1040
  MarkdownBlock(text=time_text),
931
- table_block,
1041
+ summary_block,
932
1042
  ]
933
1043
  )
934
1044
 
935
- if threaded:
936
- blocks.append(MarkdownBlock(text="See thread for individual alerts"))
937
-
938
- output_blocks = []
939
- for block in blocks:
940
- output_blocks.extend(self.__to_slack(block, sink_params.name))
941
-
1045
+ # Resolve the target before building the rest of the message, so that bailing out
1046
+ # doesn't leave an uploaded attachment behind.
942
1047
  if not channel:
943
1048
  # Fallback: resolve channel without labels/annotations (only cluster_name will work)
944
1049
  channel = ChannelTransformer.template(
@@ -948,6 +1053,7 @@ class SlackSender:
948
1053
  {},
949
1054
  {},
950
1055
  )
1056
+ channel_name = channel
951
1057
  if msg_ts is not None:
952
1058
  method = self.slack_client.chat_update
953
1059
  kwargs = {"ts": msg_ts}
@@ -964,14 +1070,47 @@ class SlackSender:
964
1070
  method = self.slack_client.chat_postMessage
965
1071
  kwargs = {}
966
1072
 
1073
+ attachment_permalink = None
1074
+ superseded_file_id = None
1075
+ if omitted_count:
1076
+ # Too many groups to show inline - attach the complete table as a file. The link has
1077
+ # to sit outside the table's code block, Slack doesn't render links inside one.
1078
+ # Only on the update path do we have both the channel id and the summary message to
1079
+ # thread the file under - `channel` has already been swapped for the id there.
1080
+ attachment_permalink, superseded_file_id = self.__refresh_summary_attachment(
1081
+ table_block, summary_state, channel if msg_ts is not None else None, msg_ts
1082
+ )
1083
+ if attachment_permalink:
1084
+ blocks.append(MarkdownBlock(text=f"<{attachment_permalink}|See all {len(rows)} groups> (attached)"))
1085
+
1086
+ if threaded:
1087
+ blocks.append(MarkdownBlock(text="See thread for individual alerts"))
1088
+
1089
+ output_blocks = []
1090
+ for block in blocks:
1091
+ output_blocks.extend(self.__to_slack(block, sink_params.name))
1092
+
1093
+ message_text = "Summary for: " + ", ".join(group_by_classification_header)
1094
+ if attachment_permalink:
1095
+ # Uploaded files have to be referenced from the message text, not just the blocks,
1096
+ # for Slack to actually share them (see prepare_slack_text).
1097
+ message_text = f"{message_text}\n<{attachment_permalink}|alerts-summary.txt>"
1098
+
967
1099
  try:
968
1100
  resp = method(
969
1101
  channel=channel,
970
- text="Summary for: " + ", ".join(group_by_classification_header),
1102
+ text=message_text,
971
1103
  blocks=output_blocks,
972
1104
  display_as_bot=True,
973
1105
  **kwargs,
974
1106
  )
1107
+ # Updating this message later needs the channel id, and a summary-only sink never
1108
+ # sends anything else that would record it (individual alerts, which do, are only
1109
+ # sent in threaded mode). Without this the message can never be updated, and a new
1110
+ # summary gets posted for every notification instead.
1111
+ self.channel_name_to_id[channel_name] = resp["channel"]
1112
+ # Only now that the message points at the new file is the old one safe to remove.
1113
+ self.__delete_superseded_attachment(superseded_file_id)
975
1114
  return resp["ts"]
976
1115
  except Exception as e:
977
1116
  logging.exception(f"error sending message to slack\n{e}\nchannel={channel}\n")
File without changes