Repository: alerta/python-alerta-client

Issue:
`alerta heartbeats --alert` broken
**Issue Summary**
after last update got exception on alerta heartbeats --alert

**Environment**
- OS: linux ubuntu 16.04
- API version: 7.4.4
- Deployment: self-hosted
- For self-hosted, WSGI environment: nginx/uwsgi

- Database: postgresql

- Server config:
  Auth enabled? Yes
  Auth provider? Basic
  Customer views? No

**To Reproduce**
Steps to reproduce the behavior:
1. pip install alerta==7.4.4
2. ALERTA_ENDPOINT=http://localhost/api ALERTA_API_KEY=<skipped> alerta heartbeats --timeout 600 --alert
```
Traceback (most recent call last):
  File "/srv/projects/venv3/bin/alerta", line 11, in <module>
    sys.exit(cli())
  File "/srv/projects/venv3/lib/python3.5/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/srv/projects/venv3/lib/python3.5/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/srv/projects/venv3/lib/python3.5/site-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/srv/projects/venv3/lib/python3.5/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/srv/projects/venv3/lib/python3.5/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/srv/projects/venv3/lib/python3.5/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context().obj, *args, **kwargs)
  File "/srv/projects/venv3/lib/python3.5/site-packages/alertaclient/commands/cmd_heartbeats.py", line 100, in cli
    customer=b.customer
  File "/srv/projects/venv3/lib/python3.5/site-packages/alertaclient/api.py", line 65, in send_alert
    r = self.http.post('/alert', data)
  File "/srv/projects/venv3/lib/python3.5/site-packages/alertaclient/api.py", line 519, in post
    response = self.session.post(url, data=json.dumps(data, cls=CustomJsonEncoder),
  File "/usr/lib/python3.5/json/__init__.py", line 237, in dumps
    **kw).encode(obj)
  File "/usr/lib/python3.5/json/encoder.py", line 198, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python3.5/json/encoder.py", line 256, in iterencode
    return _iterencode(o, 0)
  File "/srv/projects/venv3/lib/python3.5/site-packages/alertaclient/utils.py", line 18, in default
    return json.JSONEncoder.default(self, o)
  File "/usr/lib/python3.5/json/encoder.py", line 179, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: <function origin at 0x7f5cba65ad90> is not JSON serializable
```

**Expected behavior**
```
Alerting heartbeats  [####################################]  100%
```

**Additional context**
temporary workaround:
1. pip install alerta==7.4.0

Discussion:

Patch:
diff --git a/alertaclient/commands/cmd_heartbeats.py b/alertaclient/commands/cmd_heartbeats.py
index b9964ff..5776b2a 100644
--- a/alertaclient/commands/cmd_heartbeats.py
+++ b/alertaclient/commands/cmd_heartbeats.py
@@ -58,7 +58,7 @@ def cli(obj, alert, severity, timeout, purge):
                         text='Heartbeat not received in {} seconds'.format(b.timeout),
                         tags=b.tags,
                         attributes=b.attributes,
-                        origin=origin,
+                        origin=origin(),
                         type='heartbeatAlert',
                         timeout=timeout,
                         customer=b.customer
@@ -76,7 +76,7 @@ def cli(obj, alert, severity, timeout, purge):
                         text='Heartbeat took more than {}ms to be processed'.format(b.max_latency),
                         tags=b.tags,
                         attributes=b.attributes,
-                        origin=origin,
+                        origin=origin(),
                         type='heartbeatAlert',
                         timeout=timeout,
                         customer=b.customer
@@ -94,7 +94,7 @@ def cli(obj, alert, severity, timeout, purge):
                         text='Heartbeat OK',
                         tags=b.tags,
                         attributes=b.attributes,
-                        origin=origin,
+                        origin=origin(),
                         type='heartbeatAlert',
                         timeout=timeout,
                         customer=b.customer


Test patch:
diff --git a/tests/test_commands.py b/tests/test_commands.py
index c0de243..40ed1b2 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -6,6 +6,7 @@ from click.testing import CliRunner
 
 from alertaclient.api import Client
 from alertaclient.commands.cmd_heartbeat import cli as heartbeat_cmd
+from alertaclient.commands.cmd_heartbeats import cli as heartbeats_cmd
 from alertaclient.commands.cmd_whoami import cli as whoami_cmd
 from alertaclient.config import Config
 
@@ -22,14 +23,14 @@ class CommandsTestCase(unittest.TestCase):
         self.runner = CliRunner(echo_stdin=True)
 
     @requests_mock.mock()
-    def test_send_cmd(self, m):
+    def test_heartbeat_cmd(self, m):
 
         config_response = """
         {}
         """
         m.get('/config', text=config_response)
 
-        send_response = """
+        heartbeat_response = """
         {
           "heartbeat": {
             "attributes": {
@@ -58,11 +59,111 @@ class CommandsTestCase(unittest.TestCase):
         }
         """
 
-        m.post('/heartbeat', text=send_response)
+        m.post('/heartbeat', text=heartbeat_response)
         result = self.runner.invoke(heartbeat_cmd, ['-E', 'Production', '-S', 'Web', '-s', 'major'], obj=self.obj)
         UUID(result.output.strip())
         self.assertEqual(result.exit_code, 0)
 
+    @requests_mock.mock()
+    def test_heartbeats_cmd(self, m):
+
+        config_response = """
+        {}
+        """
+        m.get('/config', text=config_response)
+
+        heartbeats_response = """
+        {
+          "heartbeats": [
+            {
+              "attributes": {
+                "environment": "infrastructure"
+              },
+              "createTime": "2020-03-10T20:25:54.541Z",
+              "customer": null,
+              "href": "http://127.0.0.1/heartbeat/52c202e8-d949-45ed-91e0-cdad4f37de73",
+              "id": "52c202e8-d949-45ed-91e0-cdad4f37de73",
+              "latency": 0,
+              "maxLatency": 2000,
+              "origin": "monitoring-01",
+              "receiveTime": "2020-03-10T20:25:54.541Z",
+              "since": 204,
+              "status": "expired",
+              "tags": [],
+              "timeout": 90,
+              "type": "Heartbeat"
+            }
+          ],
+          "status": "ok",
+          "total": 1
+        }
+        """
+
+        heartbeat_alert_response = """
+        {
+          "alert": {
+            "attributes": {
+              "environment": "infrastructure"
+            },
+            "correlate": [
+              "HeartbeatFail",
+              "HeartbeatSlow",
+              "HeartbeatOK"
+            ],
+            "createTime": "2020-03-10T21:55:07.884Z",
+            "customer": null,
+            "duplicateCount": 0,
+            "environment": "infrastructure",
+            "event": "HeartbeatSlow",
+            "group": "System",
+            "history": [
+              {
+                "event": "HeartbeatSlow",
+                "href": "http://api.local.alerta.io:8080/alert/6cfbc30f-c2d6-4edf-b672-841070995206",
+                "id": "6cfbc30f-c2d6-4edf-b672-841070995206",
+                "severity": "High",
+                "status": "open",
+                "text": "new alert",
+                "type": "new",
+                "updateTime": "2020-03-10T21:55:07.884Z",
+                "user": null,
+                "value": "22ms"
+              }
+            ],
+            "href": "http://api.local.alerta.io:8080/alert/6cfbc30f-c2d6-4edf-b672-841070995206",
+            "id": "6cfbc30f-c2d6-4edf-b672-841070995206",
+            "lastReceiveId": "6cfbc30f-c2d6-4edf-b672-841070995206",
+            "lastReceiveTime": "2020-03-10T21:55:07.916Z",
+            "origin": "alerta/macbook.lan",
+            "previousSeverity": "Not classified",
+            "rawData": null,
+            "receiveTime": "2020-03-10T21:55:07.916Z",
+            "repeat": false,
+            "resource": "monitoring-01",
+            "service": [
+              "Alerta"
+            ],
+            "severity": "High",
+            "status": "open",
+            "tags": [],
+            "text": "Heartbeat took more than 2ms to be processed",
+            "timeout": 86000,
+            "trendIndication": "moreSevere",
+            "type": "heartbeatAlert",
+            "updateTime": "2020-03-10T21:55:07.916Z",
+            "value": "22ms"
+          },
+          "id": "6cfbc30f-c2d6-4edf-b672-841070995206",
+          "status": "ok"
+        }
+        """
+
+        m.get('/heartbeats', text=heartbeats_response)
+        m.post('/alert', text=heartbeat_alert_response)
+        result = self.runner.invoke(heartbeats_cmd, ['--alert'], obj=self.obj)
+        self.assertEqual(result.exit_code, 0, result.exception)
+        self.assertIn('monitoring-01', result.output)
+
     @requests_mock.mock()
     def test_whoami_cmd(self, m):