From a0e56fe0e3b420e948e9b585bebebd021281cc40 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Sep 2026 17:49:06 +0000 Subject: [PATCH 1/4] Document and test arbitrary custom metrics on Monitor.ping Keep the existing metrics dict API (encoded as repeated metric=name:value query params) for built-ins and custom names. Document server name rules, limits, and rejected_metrics. Log a warning when a 200 JSON ping body includes rejected_metrics without changing the return type. Co-authored-by: August Flanagan --- README.md | 41 +++++++++- cronitor/monitor.py | 57 +++++++++++++- cronitor/tests/test_pings.py | 145 +++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a89b506..1a08729 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,41 @@ monitor.ping( metrics={ 'duration': 100, # how long the job ran (complete|fail only). cronitor will calculate this when not provided 'count': 4500, # if your job is processing a number of items you can report a count - 'error_count': 10 # the number of errors that occurred while this job was running + 'error_count': 10, # the number of errors that occurred while this job was running + 'queue_depth': 42, # custom metric: any named numeric measurement + 'quality_score': 0.87 } ) ``` +### Custom / arbitrary metrics + +The `metrics` dict is the public API for both built-in measurements (`duration`, `count`, `error_count`) and arbitrary custom metrics. Each entry is encoded as a repeated `metric=name:value` query parameter — the same Telemetry API wire format, for example `?metric=queue_depth:42&metric=quality_score:0.87`. + +Cronitor's server is the authority for validation (this client does not reject names or values locally): + +- **Names** are normalized to lowercase server-side. Maximum **63** characters; letters, numbers, underscores, and hyphens only. Longer names are rejected, never truncated. +- **Values** must be finite numbers, including `0`, negatives, and fractions. +- **Limits:** at most 10 measurements per ping, and 10 distinct metric names per monitor. + +Invalid metrics do **not** fail the ping. The event is still accepted, and a `200` JSON body may include `rejected_metrics`: + +```json +{"rejected_metrics":[{"input":"bad name:1","reason":"invalid name"}]} +``` + +`Monitor.ping` still returns the `requests.Response`; callers that ignore the body are unchanged. When `rejected_metrics` is present the SDK logs a warning. Typical successful pings are not JSON (for example `ok`), so only inspect the JSON body when you need rejection details: + +```python +response = monitor.ping(state='complete', metrics={'queue_depth': 42, 'quality_score': 0.87}) +try: + rejected = response.json().get('rejected_metrics') +except (ValueError, AttributeError): + rejected = None +``` + +You can assert on custom metrics with the same `metric.` syntax as built-ins, e.g. `metric.queue_depth < 100`. + ## Configuring Monitors ### YAML Configuration File @@ -172,6 +202,12 @@ jobs: - metric.count > 0 - metric.duration < 30 seconds + process-queue: + schedule: every 5 minutes + assertions: + - metric.queue_depth < 100 + - metric.quality_score > 0.8 + checks: cronitor-homepage: request: @@ -214,7 +250,8 @@ monitors = cronitor.Monitor.put([ 'key': 'send-customer-invoices', 'schedule': '0 0 * * *', 'assertions': [ - 'metric.duration < 5 min' + 'metric.duration < 5 min', + 'metric.queue_depth < 100' ], 'notify': ['devops-alerts-slack'] }, diff --git a/cronitor/monitor.py b/cronitor/monitor.py index 657c79d..ad188be 100644 --- a/cronitor/monitor.py +++ b/cronitor/monitor.py @@ -159,11 +159,22 @@ def delete(self): raise cronitor.APIError("An unexpected error occured when deleting '%s'" % self.key) def ping(self, **params): + """Send a telemetry ping. + + ``metrics`` is an optional dict of named numeric measurements (built-ins + and arbitrary custom names). Encoded as repeated ``metric=name:value`` + query params. Returns a ``requests.Response``, or ``None`` if no API key + is set. HTTP 200 JSON may include ``rejected_metrics`` when some + measurements are invalid; the event is still accepted and a warning is + logged. Callers that ignore the response body are unchanged. + """ if not self.api_key: logger.error('No API key detected. Set cronitor.api_key or initialize Monitor with kwarg api_key.') return - return self._req.get(url=self._ping_api_url(), params=self._clean_params(params), timeout=5, headers=self._headers) + resp = self._req.get(url=self._ping_api_url(), params=self._clean_params(params), timeout=5, headers=self._headers) + self._log_rejected_metrics(resp) + return resp def ok(self): self.ping(state=cronitor.State.OK) @@ -192,6 +203,8 @@ def _fetch(self): return resp.json() def _clean_params(self, params): + # Encode metrics dict as repeated metric=name:value query params. + # Built-ins (duration, count, error_count) and custom names share this format. metrics = None if 'metrics' in params and type(params['metrics']) == dict: metrics = ['{}:{}'.format(k,v) for k,v in params['metrics'].items()] @@ -206,6 +219,24 @@ def _clean_params(self, params): 'env': self.env, } + def _log_rejected_metrics(self, resp): + rejected = _rejected_metrics_from_response(resp) + if not rejected: + return + + details = [] + for item in rejected: + if type(item) == dict: + details.append('{} ({})'.format(item.get('input'), item.get('reason'))) + else: + details.append(str(item)) + + logger.warning( + 'Cronitor rejected %s metric(s); the ping was still accepted. %s', + len(rejected), + '; '.join(details), + ) + def _ping_api_url(self): return "https://cronitor.link/p/{}/{}".format(self.api_key, self.key) @@ -294,6 +325,30 @@ def _monitor_api_url(cls, key=None): if not key: return "https://cronitor.io/api/monitors" return "https://cronitor.io/api/monitors/{}".format(key) + +def _rejected_metrics_from_response(resp): + """Return rejected_metrics from a 200 JSON ping body, else None. + + Typical pings are not JSON (e.g. ``ok``). HEAD/empty bodies are ignored. + """ + if resp is None: + return None + try: + if getattr(resp, 'status_code', None) != 200: + return None + if not getattr(resp, 'content', None): + return None + data = resp.json() + except (ValueError, TypeError, AttributeError): + return None + if type(data) != dict: + return None + rejected = data.get('rejected_metrics') + if not rejected: + return None + return rejected + + def _prepare_payload(monitors, rollback=False, request_format=JSON): ret = {} if request_format == JSON: diff --git a/cronitor/tests/test_pings.py b/cronitor/tests/test_pings.py index f53e996..1a72ca2 100644 --- a/cronitor/tests/test_pings.py +++ b/cronitor/tests/test_pings.py @@ -3,6 +3,7 @@ from unittest.mock import patch, ANY, call from unittest.mock import MagicMock import cronitor +from cronitor.monitor import _rejected_metrics_from_response import pytest # a reserved monitorkey for running integration tests against cronitor.link @@ -65,6 +66,150 @@ def test_convert_metrics_hash(self): }}) self.assertListEqual(sorted(clean['metric']), sorted(['count:500', 'duration:100', 'error_count:20' ])) + def test_clean_params_custom_metrics(self): + monitor = cronitor.Monitor(FAKE_KEY) + clean = monitor._clean_params({'metrics': { + 'queue_depth': 42, + 'quality_score': 0.87, + 'retry-count': 3, + }}) + self.assertCountEqual(clean['metric'], [ + 'queue_depth:42', + 'quality_score:0.87', + 'retry-count:3', + ]) + + def test_clean_params_metric_numeric_edges(self): + monitor = cronitor.Monitor(FAKE_KEY) + clean = monitor._clean_params({'metrics': { + 'empty_batch': 0, + 'offset': -3, + 'quality_score': 0.87, + 'duration': 1.5, + }}) + self.assertCountEqual(clean['metric'], [ + 'empty_batch:0', + 'offset:-3', + 'quality_score:0.87', + 'duration:1.5', + ]) + + def test_clean_params_does_not_truncate_or_reject_names(self): + # Server is the authority: the SDK encodes names as given (never truncates). + long_name = 'a' * 64 + monitor = cronitor.Monitor(FAKE_KEY) + clean = monitor._clean_params({'metrics': {long_name: 1, 'bad name': 2}}) + self.assertCountEqual(clean['metric'], [long_name + ':1', 'bad name:2']) + + def test_clean_params_mixed_builtin_and_custom_metrics(self): + monitor = cronitor.Monitor(FAKE_KEY) + clean = monitor._clean_params({'metrics': { + 'count': 10, + 'queue_depth': 42, + 'error_count': 0, + 'quality_score': 0.5, + }}) + self.assertCountEqual(clean['metric'], [ + 'count:10', + 'queue_depth:42', + 'error_count:0', + 'quality_score:0.5', + ]) + + @patch('cronitor.Monitor._req.get') + def test_ping_encodes_custom_metrics(self, ping): + monitor = cronitor.Monitor(FAKE_KEY) + monitor.ping(state='complete', metrics={ + 'queue_depth': 42, + 'quality_score': 0.87, + 'offset': -1, + 'empty_batch': 0, + }) + params = ping.call_args[1]['params'] + self.assertCountEqual(params['metric'], [ + 'queue_depth:42', + 'quality_score:0.87', + 'offset:-1', + 'empty_batch:0', + ]) + + @patch('cronitor.monitor.logger.warning') + @patch('cronitor.Monitor._req.get') + def test_ping_logs_rejected_metrics(self, ping, warning): + rejected = [{'input': 'bad name:1', 'reason': 'invalid name'}] + resp = MagicMock() + resp.status_code = 200 + resp.content = b'{"rejected_metrics":[{"input":"bad name:1","reason":"invalid name"}]}' + resp.json.return_value = {'rejected_metrics': rejected} + ping.return_value = resp + + monitor = cronitor.Monitor(FAKE_KEY) + result = monitor.ping(metrics={'bad name': 1}) + + self.assertIs(result, resp) + warning.assert_called_once() + self.assertIn('rejected', warning.call_args[0][0]) + self.assertEqual(warning.call_args[0][1], 1) + self.assertIn('bad name:1', warning.call_args[0][2]) + self.assertIn('invalid name', warning.call_args[0][2]) + + @patch('cronitor.monitor.logger.warning') + @patch('cronitor.Monitor._req.get') + def test_ping_ok_text_body_does_not_log_rejected_metrics(self, ping, warning): + resp = MagicMock() + resp.status_code = 200 + resp.content = b'ok' + resp.json.side_effect = ValueError('not json') + ping.return_value = resp + + monitor = cronitor.Monitor(FAKE_KEY) + result = monitor.ping() + + self.assertIs(result, resp) + warning.assert_not_called() + + @patch('cronitor.monitor.logger.warning') + @patch('cronitor.Monitor._req.get') + def test_ping_empty_body_does_not_log_rejected_metrics(self, ping, warning): + resp = MagicMock() + resp.status_code = 200 + resp.content = b'' + ping.return_value = resp + + monitor = cronitor.Monitor(FAKE_KEY) + result = monitor.ping() + + self.assertIs(result, resp) + warning.assert_not_called() + + def test_rejected_metrics_from_response_parses_json(self): + resp = MagicMock() + resp.status_code = 200 + resp.content = b'{"rejected_metrics":[{"input":"x","reason":"too many"}]}' + resp.json.return_value = {'rejected_metrics': [{'input': 'x', 'reason': 'too many'}]} + self.assertEqual( + _rejected_metrics_from_response(resp), + [{'input': 'x', 'reason': 'too many'}], + ) + + def test_rejected_metrics_from_response_ignores_non_json_and_errors(self): + text = MagicMock() + text.status_code = 200 + text.content = b'ok' + text.json.side_effect = ValueError('not json') + self.assertIsNone(_rejected_metrics_from_response(text)) + self.assertIsNone(_rejected_metrics_from_response(None)) + + empty = MagicMock() + empty.status_code = 200 + empty.content = b'' + self.assertIsNone(_rejected_metrics_from_response(empty)) + + error = MagicMock() + error.status_code = 500 + error.content = b'{"rejected_metrics":[]}' + self.assertIsNone(_rejected_metrics_from_response(error)) + class PingDecoratorTests(unittest.TestCase): From 70b8cd35b04b26569364337df6735902fd1946c1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Sep 2026 20:05:26 +0000 Subject: [PATCH 2/4] Remove rejected_metrics handling from ping responses Metric validation is async in the ping worker, not returned on the ingestion response. Drop response-body inspection, warning logs, tests, and docs that suggested clients would see rejected_metrics on ping. Co-authored-by: August Flanagan --- README.md | 20 +-------- cronitor/monitor.py | 49 +--------------------- cronitor/tests/test_pings.py | 78 ------------------------------------ 3 files changed, 4 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 1a08729..b00a90c 100644 --- a/README.md +++ b/README.md @@ -130,28 +130,12 @@ monitor.ping( The `metrics` dict is the public API for both built-in measurements (`duration`, `count`, `error_count`) and arbitrary custom metrics. Each entry is encoded as a repeated `metric=name:value` query parameter — the same Telemetry API wire format, for example `?metric=queue_depth:42&metric=quality_score:0.87`. -Cronitor's server is the authority for validation (this client does not reject names or values locally): +Cronitor's server is the authority for these constraints (this client encodes names and values as given and does not validate locally): -- **Names** are normalized to lowercase server-side. Maximum **63** characters; letters, numbers, underscores, and hyphens only. Longer names are rejected, never truncated. +- **Names** are normalized to lowercase server-side. Maximum **63** characters; letters, numbers, underscores, and hyphens only. Longer names are not accepted (never truncated). - **Values** must be finite numbers, including `0`, negatives, and fractions. - **Limits:** at most 10 measurements per ping, and 10 distinct metric names per monitor. -Invalid metrics do **not** fail the ping. The event is still accepted, and a `200` JSON body may include `rejected_metrics`: - -```json -{"rejected_metrics":[{"input":"bad name:1","reason":"invalid name"}]} -``` - -`Monitor.ping` still returns the `requests.Response`; callers that ignore the body are unchanged. When `rejected_metrics` is present the SDK logs a warning. Typical successful pings are not JSON (for example `ok`), so only inspect the JSON body when you need rejection details: - -```python -response = monitor.ping(state='complete', metrics={'queue_depth': 42, 'quality_score': 0.87}) -try: - rejected = response.json().get('rejected_metrics') -except (ValueError, AttributeError): - rejected = None -``` - You can assert on custom metrics with the same `metric.` syntax as built-ins, e.g. `metric.queue_depth < 100`. ## Configuring Monitors diff --git a/cronitor/monitor.py b/cronitor/monitor.py index ad188be..4162a21 100644 --- a/cronitor/monitor.py +++ b/cronitor/monitor.py @@ -164,17 +164,13 @@ def ping(self, **params): ``metrics`` is an optional dict of named numeric measurements (built-ins and arbitrary custom names). Encoded as repeated ``metric=name:value`` query params. Returns a ``requests.Response``, or ``None`` if no API key - is set. HTTP 200 JSON may include ``rejected_metrics`` when some - measurements are invalid; the event is still accepted and a warning is - logged. Callers that ignore the response body are unchanged. + is set. """ if not self.api_key: logger.error('No API key detected. Set cronitor.api_key or initialize Monitor with kwarg api_key.') return - resp = self._req.get(url=self._ping_api_url(), params=self._clean_params(params), timeout=5, headers=self._headers) - self._log_rejected_metrics(resp) - return resp + return self._req.get(url=self._ping_api_url(), params=self._clean_params(params), timeout=5, headers=self._headers) def ok(self): self.ping(state=cronitor.State.OK) @@ -219,24 +215,6 @@ def _clean_params(self, params): 'env': self.env, } - def _log_rejected_metrics(self, resp): - rejected = _rejected_metrics_from_response(resp) - if not rejected: - return - - details = [] - for item in rejected: - if type(item) == dict: - details.append('{} ({})'.format(item.get('input'), item.get('reason'))) - else: - details.append(str(item)) - - logger.warning( - 'Cronitor rejected %s metric(s); the ping was still accepted. %s', - len(rejected), - '; '.join(details), - ) - def _ping_api_url(self): return "https://cronitor.link/p/{}/{}".format(self.api_key, self.key) @@ -326,29 +304,6 @@ def _monitor_api_url(cls, key=None): return "https://cronitor.io/api/monitors/{}".format(key) -def _rejected_metrics_from_response(resp): - """Return rejected_metrics from a 200 JSON ping body, else None. - - Typical pings are not JSON (e.g. ``ok``). HEAD/empty bodies are ignored. - """ - if resp is None: - return None - try: - if getattr(resp, 'status_code', None) != 200: - return None - if not getattr(resp, 'content', None): - return None - data = resp.json() - except (ValueError, TypeError, AttributeError): - return None - if type(data) != dict: - return None - rejected = data.get('rejected_metrics') - if not rejected: - return None - return rejected - - def _prepare_payload(monitors, rollback=False, request_format=JSON): ret = {} if request_format == JSON: diff --git a/cronitor/tests/test_pings.py b/cronitor/tests/test_pings.py index 1a72ca2..ab9376d 100644 --- a/cronitor/tests/test_pings.py +++ b/cronitor/tests/test_pings.py @@ -3,7 +3,6 @@ from unittest.mock import patch, ANY, call from unittest.mock import MagicMock import cronitor -from cronitor.monitor import _rejected_metrics_from_response import pytest # a reserved monitorkey for running integration tests against cronitor.link @@ -133,83 +132,6 @@ def test_ping_encodes_custom_metrics(self, ping): 'empty_batch:0', ]) - @patch('cronitor.monitor.logger.warning') - @patch('cronitor.Monitor._req.get') - def test_ping_logs_rejected_metrics(self, ping, warning): - rejected = [{'input': 'bad name:1', 'reason': 'invalid name'}] - resp = MagicMock() - resp.status_code = 200 - resp.content = b'{"rejected_metrics":[{"input":"bad name:1","reason":"invalid name"}]}' - resp.json.return_value = {'rejected_metrics': rejected} - ping.return_value = resp - - monitor = cronitor.Monitor(FAKE_KEY) - result = monitor.ping(metrics={'bad name': 1}) - - self.assertIs(result, resp) - warning.assert_called_once() - self.assertIn('rejected', warning.call_args[0][0]) - self.assertEqual(warning.call_args[0][1], 1) - self.assertIn('bad name:1', warning.call_args[0][2]) - self.assertIn('invalid name', warning.call_args[0][2]) - - @patch('cronitor.monitor.logger.warning') - @patch('cronitor.Monitor._req.get') - def test_ping_ok_text_body_does_not_log_rejected_metrics(self, ping, warning): - resp = MagicMock() - resp.status_code = 200 - resp.content = b'ok' - resp.json.side_effect = ValueError('not json') - ping.return_value = resp - - monitor = cronitor.Monitor(FAKE_KEY) - result = monitor.ping() - - self.assertIs(result, resp) - warning.assert_not_called() - - @patch('cronitor.monitor.logger.warning') - @patch('cronitor.Monitor._req.get') - def test_ping_empty_body_does_not_log_rejected_metrics(self, ping, warning): - resp = MagicMock() - resp.status_code = 200 - resp.content = b'' - ping.return_value = resp - - monitor = cronitor.Monitor(FAKE_KEY) - result = monitor.ping() - - self.assertIs(result, resp) - warning.assert_not_called() - - def test_rejected_metrics_from_response_parses_json(self): - resp = MagicMock() - resp.status_code = 200 - resp.content = b'{"rejected_metrics":[{"input":"x","reason":"too many"}]}' - resp.json.return_value = {'rejected_metrics': [{'input': 'x', 'reason': 'too many'}]} - self.assertEqual( - _rejected_metrics_from_response(resp), - [{'input': 'x', 'reason': 'too many'}], - ) - - def test_rejected_metrics_from_response_ignores_non_json_and_errors(self): - text = MagicMock() - text.status_code = 200 - text.content = b'ok' - text.json.side_effect = ValueError('not json') - self.assertIsNone(_rejected_metrics_from_response(text)) - self.assertIsNone(_rejected_metrics_from_response(None)) - - empty = MagicMock() - empty.status_code = 200 - empty.content = b'' - self.assertIsNone(_rejected_metrics_from_response(empty)) - - error = MagicMock() - error.status_code = 500 - error.content = b'{"rejected_metrics":[]}' - self.assertIsNone(_rejected_metrics_from_response(error)) - class PingDecoratorTests(unittest.TestCase): From ad8a800fc58ae4e88362e1827c093f542405b9b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Sep 2026 20:05:50 +0000 Subject: [PATCH 3/4] Drop unused MagicMock import after rejected_metrics test removal Co-authored-by: August Flanagan --- cronitor/tests/test_pings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cronitor/tests/test_pings.py b/cronitor/tests/test_pings.py index ab9376d..7b5862e 100644 --- a/cronitor/tests/test_pings.py +++ b/cronitor/tests/test_pings.py @@ -1,7 +1,6 @@ import os import unittest from unittest.mock import patch, ANY, call -from unittest.mock import MagicMock import cronitor import pytest From 7752d7682e7a33985b8c9059dc777234fe2e8a7d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 11 Sep 2026 20:23:05 +0000 Subject: [PATCH 4/4] Narrow custom-metrics PR to a README-only note Revert production code and tests to default. Document that arbitrary metrics use the existing metrics dict and metric=name:value encoding. Co-authored-by: August Flanagan --- README.md | 21 ++--------- cronitor/monitor.py | 10 ------ cronitor/tests/test_pings.py | 68 +----------------------------------- 3 files changed, 3 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index b00a90c..030d93d 100644 --- a/README.md +++ b/README.md @@ -126,17 +126,7 @@ monitor.ping( ) ``` -### Custom / arbitrary metrics - -The `metrics` dict is the public API for both built-in measurements (`duration`, `count`, `error_count`) and arbitrary custom metrics. Each entry is encoded as a repeated `metric=name:value` query parameter — the same Telemetry API wire format, for example `?metric=queue_depth:42&metric=quality_score:0.87`. - -Cronitor's server is the authority for these constraints (this client encodes names and values as given and does not validate locally): - -- **Names** are normalized to lowercase server-side. Maximum **63** characters; letters, numbers, underscores, and hyphens only. Longer names are not accepted (never truncated). -- **Values** must be finite numbers, including `0`, negatives, and fractions. -- **Limits:** at most 10 measurements per ping, and 10 distinct metric names per monitor. - -You can assert on custom metrics with the same `metric.` syntax as built-ins, e.g. `metric.queue_depth < 100`. +Custom metric names (for example `queue_depth` or `quality_score`) use the same `metrics` dict and are sent as repeated `metric=name:value` query params. Server-side, names are lowercased, max 63 characters, and may contain letters, numbers, underscores, and hyphens. ## Configuring Monitors @@ -186,12 +176,6 @@ jobs: - metric.count > 0 - metric.duration < 30 seconds - process-queue: - schedule: every 5 minutes - assertions: - - metric.queue_depth < 100 - - metric.quality_score > 0.8 - checks: cronitor-homepage: request: @@ -234,8 +218,7 @@ monitors = cronitor.Monitor.put([ 'key': 'send-customer-invoices', 'schedule': '0 0 * * *', 'assertions': [ - 'metric.duration < 5 min', - 'metric.queue_depth < 100' + 'metric.duration < 5 min' ], 'notify': ['devops-alerts-slack'] }, diff --git a/cronitor/monitor.py b/cronitor/monitor.py index 4162a21..657c79d 100644 --- a/cronitor/monitor.py +++ b/cronitor/monitor.py @@ -159,13 +159,6 @@ def delete(self): raise cronitor.APIError("An unexpected error occured when deleting '%s'" % self.key) def ping(self, **params): - """Send a telemetry ping. - - ``metrics`` is an optional dict of named numeric measurements (built-ins - and arbitrary custom names). Encoded as repeated ``metric=name:value`` - query params. Returns a ``requests.Response``, or ``None`` if no API key - is set. - """ if not self.api_key: logger.error('No API key detected. Set cronitor.api_key or initialize Monitor with kwarg api_key.') return @@ -199,8 +192,6 @@ def _fetch(self): return resp.json() def _clean_params(self, params): - # Encode metrics dict as repeated metric=name:value query params. - # Built-ins (duration, count, error_count) and custom names share this format. metrics = None if 'metrics' in params and type(params['metrics']) == dict: metrics = ['{}:{}'.format(k,v) for k,v in params['metrics'].items()] @@ -303,7 +294,6 @@ def _monitor_api_url(cls, key=None): if not key: return "https://cronitor.io/api/monitors" return "https://cronitor.io/api/monitors/{}".format(key) - def _prepare_payload(monitors, rollback=False, request_format=JSON): ret = {} if request_format == JSON: diff --git a/cronitor/tests/test_pings.py b/cronitor/tests/test_pings.py index 7b5862e..f53e996 100644 --- a/cronitor/tests/test_pings.py +++ b/cronitor/tests/test_pings.py @@ -1,6 +1,7 @@ import os import unittest from unittest.mock import patch, ANY, call +from unittest.mock import MagicMock import cronitor import pytest @@ -64,73 +65,6 @@ def test_convert_metrics_hash(self): }}) self.assertListEqual(sorted(clean['metric']), sorted(['count:500', 'duration:100', 'error_count:20' ])) - def test_clean_params_custom_metrics(self): - monitor = cronitor.Monitor(FAKE_KEY) - clean = monitor._clean_params({'metrics': { - 'queue_depth': 42, - 'quality_score': 0.87, - 'retry-count': 3, - }}) - self.assertCountEqual(clean['metric'], [ - 'queue_depth:42', - 'quality_score:0.87', - 'retry-count:3', - ]) - - def test_clean_params_metric_numeric_edges(self): - monitor = cronitor.Monitor(FAKE_KEY) - clean = monitor._clean_params({'metrics': { - 'empty_batch': 0, - 'offset': -3, - 'quality_score': 0.87, - 'duration': 1.5, - }}) - self.assertCountEqual(clean['metric'], [ - 'empty_batch:0', - 'offset:-3', - 'quality_score:0.87', - 'duration:1.5', - ]) - - def test_clean_params_does_not_truncate_or_reject_names(self): - # Server is the authority: the SDK encodes names as given (never truncates). - long_name = 'a' * 64 - monitor = cronitor.Monitor(FAKE_KEY) - clean = monitor._clean_params({'metrics': {long_name: 1, 'bad name': 2}}) - self.assertCountEqual(clean['metric'], [long_name + ':1', 'bad name:2']) - - def test_clean_params_mixed_builtin_and_custom_metrics(self): - monitor = cronitor.Monitor(FAKE_KEY) - clean = monitor._clean_params({'metrics': { - 'count': 10, - 'queue_depth': 42, - 'error_count': 0, - 'quality_score': 0.5, - }}) - self.assertCountEqual(clean['metric'], [ - 'count:10', - 'queue_depth:42', - 'error_count:0', - 'quality_score:0.5', - ]) - - @patch('cronitor.Monitor._req.get') - def test_ping_encodes_custom_metrics(self, ping): - monitor = cronitor.Monitor(FAKE_KEY) - monitor.ping(state='complete', metrics={ - 'queue_depth': 42, - 'quality_score': 0.87, - 'offset': -1, - 'empty_batch': 0, - }) - params = ping.call_args[1]['params'] - self.assertCountEqual(params['metric'], [ - 'queue_depth:42', - 'quality_score:0.87', - 'offset:-1', - 'empty_batch:0', - ]) - class PingDecoratorTests(unittest.TestCase):