From 924ac5cc902f9d6750a8e3c41f0d214163b569c5 Mon Sep 17 00:00:00 2001 From: sarveshpatil-plivo Date: Thu, 9 Jul 2026 03:06:44 +0530 Subject: [PATCH 1/2] Add Plivo SMS sink Adds a Plivo notification sink that sends alert findings as SMS via the Plivo Messages API, registered in sink_factory and runner_config, with a docs page. Mirrors the pushover sink. --- docs/configuration/sinks/index.rst | 5 ++ docs/configuration/sinks/plivo.rst | 51 +++++++++++++++++++ src/robusta/core/model/runner_config.py | 2 + src/robusta/core/sinks/plivo/__init__.py | 2 + src/robusta/core/sinks/plivo/plivo_client.py | 23 +++++++++ src/robusta/core/sinks/plivo/plivo_sink.py | 38 ++++++++++++++ .../core/sinks/plivo/plivo_sink_params.py | 22 ++++++++ src/robusta/core/sinks/sink_factory.py | 2 + 8 files changed, 145 insertions(+) create mode 100644 docs/configuration/sinks/plivo.rst create mode 100644 src/robusta/core/sinks/plivo/__init__.py create mode 100644 src/robusta/core/sinks/plivo/plivo_client.py create mode 100644 src/robusta/core/sinks/plivo/plivo_sink.py create mode 100644 src/robusta/core/sinks/plivo/plivo_sink_params.py diff --git a/docs/configuration/sinks/index.rst b/docs/configuration/sinks/index.rst index 3d9be6734..4bdabf6e6 100644 --- a/docs/configuration/sinks/index.rst +++ b/docs/configuration/sinks/index.rst @@ -129,6 +129,11 @@ Click a sink for setup instructions. :link: pushover :link-type: doc + .. grid-item-card:: :octicon:`cpu;1em;` Plivo + :class-card: sd-bg-light sd-bg-text-light + :link: plivo + :link-type: doc + .. grid-item-card:: :octicon:`cpu;1em;` ServiceNow :class-card: sd-bg-light sd-bg-text-light :link: ServiceNow diff --git a/docs/configuration/sinks/plivo.rst b/docs/configuration/sinks/plivo.rst new file mode 100644 index 000000000..768596f90 --- /dev/null +++ b/docs/configuration/sinks/plivo.rst @@ -0,0 +1,51 @@ +Plivo +################# + +.. admonition:: This page documents a legacy sink in Robusta classic + :class: warning + + For new setups, we recommend `HolmesGPT `_ instead. + + HolmesGPT triages your alerts instead of just forwarding them. Sinks are deterministic: they send every notification, unchanged, to a fixed destination, leaving you to read and prioritize each one yourself. + + HolmesGPT instead uses AI to investigate each alert, surface the likely root cause, and escalate only what needs attention — so you get fewer, more actionable notifications. Set this up with `Alerts Triage `_ for alerts, or :ref:`Triggered Workflows ` for custom events. + + +Robusta can send SMS notifications about issues and events in your Kubernetes cluster to a phone number using Plivo. + +Getting your Auth ID and Auth Token +------------------------------------------------ +Log in to the Plivo console at `cx.plivo.com `_. Your Auth ID and Auth Token are shown on the dashboard. + +Getting a sender number +------------------------------------------------ +You need an SMS enabled Plivo phone number to send from. Buy or view your numbers in the Plivo console under Phone Numbers. + +Configuring the Plivo sink +------------------------------------------------ +Now we're ready to configure the Plivo sink. + +.. admonition:: Add this to your generated_values.yaml + + .. code-block:: yaml + + sinksConfig: + - plivo_sink: + name: plivo_sink + auth_id: + auth_token: + from_number: # E.164 format, e.g. +14155551234 + to_number: # E.164 format; for several recipients join them with '<' + +.. note:: + + Numbers use E.164 format. To send to more than one recipient, join the numbers with ``<`` (for example ``+14155551234<+14155555678``). + +Save the file and run + +.. code-block:: bash + :name: cb-add-plivo-sink + + helm upgrade robusta robusta/robusta --values=generated_values.yaml + +You should now get playbook results as SMS messages from Plivo. diff --git a/src/robusta/core/model/runner_config.py b/src/robusta/core/model/runner_config.py index 5edb08b8e..db5ca0b83 100644 --- a/src/robusta/core/model/runner_config.py +++ b/src/robusta/core/model/runner_config.py @@ -25,6 +25,7 @@ from robusta.core.sinks.webex.webex_sink_params import WebexSinkConfigWrapper from robusta.core.sinks.webhook.webhook_sink_params import WebhookSinkConfigWrapper from robusta.core.sinks.yamessenger.yamessenger_sink_params import YaMessengerSinkConfigWrapper +from robusta.core.sinks.plivo.plivo_sink_params import PlivoSinkConfigWrapper from robusta.core.sinks.pushover.pushover_sink_params import PushoverSinkConfigWrapper from robusta.core.sinks.zulip.zulip_sink_params import ZulipSinkConfigWrapper from robusta.model.alert_relabel_config import AlertRelabel @@ -74,6 +75,7 @@ class RunnerConfig(BaseModel): FileSinkConfigWrapper, MailSinkConfigWrapper, PushoverSinkConfigWrapper, + PlivoSinkConfigWrapper, GoogleChatSinkConfigWrapper, ServiceNowSinkConfigWrapper, ZulipSinkConfigWrapper, diff --git a/src/robusta/core/sinks/plivo/__init__.py b/src/robusta/core/sinks/plivo/__init__.py new file mode 100644 index 000000000..efbe7af25 --- /dev/null +++ b/src/robusta/core/sinks/plivo/__init__.py @@ -0,0 +1,2 @@ +from robusta.core.sinks.plivo.plivo_sink import PlivoSink +from robusta.core.sinks.plivo.plivo_sink_params import PlivoSinkConfigWrapper, PlivoSinkParams diff --git a/src/robusta/core/sinks/plivo/plivo_client.py b/src/robusta/core/sinks/plivo/plivo_client.py new file mode 100644 index 000000000..65f0381fe --- /dev/null +++ b/src/robusta/core/sinks/plivo/plivo_client.py @@ -0,0 +1,23 @@ +import logging + +import requests + +PLIVO_API_BASE = "https://api.plivo.com/v1/Account" + + +class PlivoClient: + def __init__(self, auth_id: str, auth_token: str): + self.auth_id = str(auth_id) + self.auth_token = str(auth_token) + + def send_message(self, src: str, dst: str, text: str): + url = f"{PLIVO_API_BASE}/{self.auth_id}/Message/" + payload = {"src": src, "dst": dst, "text": text, "type": "sms"} + + response = requests.post( + url, json=payload, auth=(self.auth_id, self.auth_token), timeout=(5, 15) + ) + if not response.ok: + logging.error( + f"Failed to send Plivo SMS to {dst}: {response.status_code} {response.reason} {response.text}" + ) diff --git a/src/robusta/core/sinks/plivo/plivo_sink.py b/src/robusta/core/sinks/plivo/plivo_sink.py new file mode 100644 index 000000000..e73306d49 --- /dev/null +++ b/src/robusta/core/sinks/plivo/plivo_sink.py @@ -0,0 +1,38 @@ +from typing import List + +from robusta.core.reporting.base import Finding +from robusta.core.sinks.plivo.plivo_client import PlivoClient +from robusta.core.sinks.plivo.plivo_sink_params import PlivoSinkConfigWrapper +from robusta.core.sinks.sink_base import SinkBase + +MAX_SMS_LENGTH = 1600 + + +class PlivoSink(SinkBase): + def __init__(self, sink_config: PlivoSinkConfigWrapper, registry): + super().__init__(sink_config.plivo_sink, registry) + + self.client = PlivoClient( + sink_config.plivo_sink.auth_id, + sink_config.plivo_sink.auth_token.get_secret_value(), + ) + self.from_number = sink_config.plivo_sink.from_number + self.to_number = sink_config.plivo_sink.to_number + + def write_finding(self, finding: Finding, platform_enabled: bool): + message = self.__build_message(finding, platform_enabled) + self.client.send_message(src=self.from_number, dst=self.to_number, text=message) + + def __build_message(self, finding: Finding, platform_enabled: bool) -> str: + lines: List[str] = [f"{finding.severity.name} - {finding.title}"] + + if platform_enabled: + lines.append(f"Investigate: {finding.get_investigate_uri(self.account_id, self.cluster_name)}") + + lines.append(f"Source: {self.cluster_name}") + + if finding.description: + lines.append(finding.description) + + message = "\n".join(line for line in lines if line) + return message[:MAX_SMS_LENGTH] diff --git a/src/robusta/core/sinks/plivo/plivo_sink_params.py b/src/robusta/core/sinks/plivo/plivo_sink_params.py new file mode 100644 index 000000000..1f39e54c5 --- /dev/null +++ b/src/robusta/core/sinks/plivo/plivo_sink_params.py @@ -0,0 +1,22 @@ +from pydantic import SecretStr + +from robusta.core.sinks.sink_base_params import SinkBaseParams +from robusta.core.sinks.sink_config import SinkConfigBase + + +class PlivoSinkParams(SinkBaseParams): + auth_id: str + auth_token: SecretStr + from_number: str + to_number: str + + @classmethod + def _get_sink_type(cls): + return "plivo" + + +class PlivoSinkConfigWrapper(SinkConfigBase): + plivo_sink: PlivoSinkParams + + def get_params(self) -> SinkBaseParams: + return self.plivo_sink diff --git a/src/robusta/core/sinks/sink_factory.py b/src/robusta/core/sinks/sink_factory.py index 96ec8f3d3..76375b0ef 100644 --- a/src/robusta/core/sinks/sink_factory.py +++ b/src/robusta/core/sinks/sink_factory.py @@ -29,6 +29,7 @@ from robusta.core.sinks.webex import WebexSink, WebexSinkConfigWrapper from robusta.core.sinks.webhook import WebhookSink, WebhookSinkConfigWrapper from robusta.core.sinks.yamessenger import YaMessengerSink, YaMessengerSinkConfigWrapper +from robusta.core.sinks.plivo import PlivoSink, PlivoSinkConfigWrapper from robusta.core.sinks.pushover import PushoverSink, PushoverSinkConfigWrapper from robusta.core.sinks.zulip import ZulipSink, ZulipSinkConfigWrapper from robusta.core.sinks.incidentio.incidentio_sink import IncidentioSink @@ -56,6 +57,7 @@ class SinkFactory: FileSinkConfigWrapper: FileSink, MailSinkConfigWrapper: MailSink, PushoverSinkConfigWrapper: PushoverSink, + PlivoSinkConfigWrapper: PlivoSink, GoogleChatSinkConfigWrapper: GoogleChatSink, ServiceNowSinkConfigWrapper: ServiceNowSink, ZulipSinkConfigWrapper: ZulipSink, From 97707e40ea516c18d8178618e5cbb74ea38612c2 Mon Sep 17 00:00:00 2001 From: Sarvesh Patil Date: Tue, 14 Jul 2026 13:07:11 +0530 Subject: [PATCH 2/2] Error handling Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/robusta/core/sinks/plivo/plivo_client.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/robusta/core/sinks/plivo/plivo_client.py b/src/robusta/core/sinks/plivo/plivo_client.py index 65f0381fe..d77960f58 100644 --- a/src/robusta/core/sinks/plivo/plivo_client.py +++ b/src/robusta/core/sinks/plivo/plivo_client.py @@ -14,10 +14,13 @@ def send_message(self, src: str, dst: str, text: str): url = f"{PLIVO_API_BASE}/{self.auth_id}/Message/" payload = {"src": src, "dst": dst, "text": text, "type": "sms"} - response = requests.post( - url, json=payload, auth=(self.auth_id, self.auth_token), timeout=(5, 15) - ) - if not response.ok: - logging.error( - f"Failed to send Plivo SMS to {dst}: {response.status_code} {response.reason} {response.text}" + try: + response = requests.post( + url, json=payload, auth=(self.auth_id, self.auth_token), timeout=(5, 15) ) + if not response.ok: + logging.error( + f"Failed to send Plivo SMS to {dst}: {response.status_code} {response.reason} {response.text}" + ) + except Exception as e: + logging.error(f"Error sending Plivo SMS to {dst}: {e}", exc_info=True)