Skip to content

Commit d430be5

Browse files
Merge pull request #1 from pythonicrahul/Added-Linter-And-UT-with-Some-improvements
feat: add CI workflow, linter configuration, and unit tests for ClickHouse Cloud check
2 parents 276e5ef + cc2ee7e commit d430be5

8 files changed

Lines changed: 166 additions & 58 deletions

File tree

.github/dependabot.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: 2
2+
updates:
3+
# Python (pip) dependencies
4+
- package-ecosystem: pip
5+
directory: /
6+
schedule:
7+
interval: weekly
8+
day: monday
9+
open-pull-requests-limit: 5
10+
labels:
11+
- dependencies
12+
- python
13+
14+
# GitHub Actions
15+
- package-ecosystem: github-actions
16+
directory: /
17+
schedule:
18+
interval: weekly
19+
day: monday
20+
open-pull-requests-limit: 5
21+
labels:
22+
- dependencies
23+
- github-actions

.github/workflows/ci.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
lint:
14+
name: Lint
15+
runs-on: ubuntu-24.04
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
23+
- name: Install dependencies
24+
run: pip install -r requirements-dev.txt
25+
26+
- name: Ruff check
27+
run: ruff check .
28+
29+
- name: Ruff format check
30+
run: ruff format --check .
31+
32+
test:
33+
name: Test
34+
runs-on: ubuntu-24.04
35+
strategy:
36+
matrix:
37+
python-version: ["3.10", "3.11", "3.12"]
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- uses: actions/setup-python@v5
42+
with:
43+
python-version: ${{ matrix.python-version }}
44+
45+
- name: Install dependencies
46+
run: pip install -r requirements-dev.txt
47+
48+
- name: Run tests
49+
run: pytest tests/ -v

checks/clickhouse_cloud.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99

1010
import json
1111
import time
12-
from typing import Any, Callable
12+
from collections.abc import Callable
13+
from typing import Any
1314

1415
import requests
16+
from datadog_checks.base import AgentCheck
1517
from requests.adapters import HTTPAdapter
1618
from urllib3.util.retry import Retry
1719

18-
from datadog_checks.base import AgentCheck
19-
20-
2120
# ---------------------------------------------------------------------------
2221
# SQL templates
2322
# ---------------------------------------------------------------------------
@@ -161,10 +160,12 @@ def __init__(
161160

162161
self.custom_tags: list[str] = inst.get("tags", [])
163162

163+
# Cluster name → used as the Datadog "service" field on every log.
164+
# Defaults to "clickhouse" so logs are attributed even without config.
165+
self.cluster_name: str = inst.get("cluster_name", "clickhouse")
166+
164167
# HTTP session with automatic retries on transient failures
165-
self.base_url: str = "https://queries.clickhouse.cloud/service/{}/run".format(
166-
self.service_id
167-
)
168+
self.base_url: str = f"https://queries.clickhouse.cloud/service/{self.service_id}/run"
168169

169170
retry_strategy = Retry(
170171
total=2,
@@ -198,11 +199,9 @@ def _validate_int(
198199
try:
199200
value = int(raw)
200201
except (TypeError, ValueError):
201-
raise ValueError("{} must be an integer, got {!r}".format(key, raw))
202+
raise ValueError(f"{key} must be an integer, got {raw!r}") from None
202203
if value < lo or value > hi:
203-
raise ValueError(
204-
"{} must be between {} and {}, got {}".format(key, lo, hi, value)
205-
)
204+
raise ValueError(f"{key} must be between {lo} and {hi}, got {value}")
206205
return value
207206

208207
# ------------------------------------------------------------------
@@ -388,18 +387,15 @@ def _build_query_log_payload(self, row: dict[str, Any]) -> dict[str, Any]:
388387
type_label = "exception"
389388
else:
390389
duration_ms = int(row.get("query_duration_ms", 0))
391-
if duration_ms >= self.slow_query_threshold_ms:
392-
level = "warning"
393-
else:
394-
level = "info"
390+
level = "warning" if duration_ms >= self.slow_query_threshold_ms else "info"
395391
type_label = "finish"
396392

397393
return {
398394
"timestamp": self._timestamp_seconds(row),
399395
"message": row.get("query", ""),
400-
"ddsource": "clickhouse_cloud",
396+
"ddsource": "clickhouse",
401397
"ddtags": ",".join(self.custom_tags) if self.custom_tags else "",
402-
"service": "clickhouse",
398+
"service": self.cluster_name,
403399
"status": level,
404400
"clickhouse.query_id": row.get("query_id", ""),
405401
"clickhouse.user": row.get("user", ""),
@@ -437,9 +433,9 @@ def _build_text_log_payload(self, row: dict[str, Any]) -> dict[str, Any]:
437433
return {
438434
"timestamp": self._timestamp_seconds(row),
439435
"message": row.get("message", ""),
440-
"ddsource": "clickhouse_cloud",
436+
"ddsource": "clickhouse",
441437
"ddtags": ",".join(self.custom_tags) if self.custom_tags else "",
442-
"service": "clickhouse",
438+
"service": self.cluster_name,
443439
"status": level,
444440
"clickhouse.logger": row.get("logger_name", ""),
445441
"clickhouse.thread_id": str(row.get("thread_id", "")),

conf.d/clickhouse_cloud.d/conf.yaml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ instances:
55
key_id: "<your-api-key-id>" # required – Cloud API key ID
66
key_secret: "<your-api-key-secret>" # required – Cloud API key secret
77

8+
# Cluster / service identity
9+
cluster_name: "<your-cluster-name>" # used as the "service" field in Datadog Logs
10+
# (default: "clickhouse")
11+
812
# Log collection toggles
913
collect_query_logs: true # default: true
1014
collect_text_logs: true # default: true
@@ -19,3 +23,8 @@ instances:
1923
tags:
2024
- "env:prod"
2125
- "clickhouse_cluster:<your-cluster-name>"
26+
27+
logs:
28+
- type: integration
29+
source: clickhouse
30+

requirements-dev.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Development and CI dependencies.
2+
# Runtime dependencies (requests, urllib3) are bundled with the Datadog Agent.
3+
pytest>=9.0,<10.0
4+
requests>=2.28,<3.0
5+
ruff>=0.11,<1.0

ruff.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
target-version = "py310"
2+
line-length = 100
3+
4+
[lint]
5+
select = [
6+
"E", # pycodestyle errors
7+
"W", # pycodestyle warnings
8+
"F", # pyflakes
9+
"I", # isort
10+
"UP", # pyupgrade
11+
"B", # flake8-bugbear
12+
"SIM", # flake8-simplify
13+
"RUF", # ruff-specific rules
14+
]
15+
ignore = [
16+
"E501", # line too long -- handled by formatter
17+
]
18+
19+
[lint.isort]
20+
known-first-party = ["checks"]

tests/conftest.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
import json
1010
import os
1111
import sys
12-
13-
import pytest
1412
from unittest.mock import MagicMock as _MagicMock
1513

14+
import pytest
1615

1716
# ---------------------------------------------------------------------------
1817
# Mock AgentCheck base class

tests/test_logs.py

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66

77
import pytest
88
import requests
9+
from conftest import MockAgentCheck
910

1011
from checks.clickhouse_cloud import (
11-
ClickHouseCloudCheck,
12-
SC_QUERY_LOG_CONNECT,
13-
SC_TEXT_LOG_CONNECT,
1412
GAUGE_QUERY_LOG_ROWS,
1513
GAUGE_TEXT_LOG_ROWS,
14+
SC_QUERY_LOG_CONNECT,
15+
SC_TEXT_LOG_CONNECT,
16+
ClickHouseCloudCheck,
1617
)
17-
from conftest import MockAgentCheck
18-
1918

2019
# ---------------------------------------------------------------------------
2120
# Helpers
@@ -69,9 +68,7 @@ def test_invalid_slow_query_threshold_raises(self, default_instance):
6968

7069
def test_invalid_backfill_minutes_raises(self, default_instance):
7170
default_instance["initial_backfill_minutes"] = 0
72-
with pytest.raises(
73-
ValueError, match="initial_backfill_minutes must be between"
74-
):
71+
with pytest.raises(ValueError, match="initial_backfill_minutes must be between"):
7572
_make_check(default_instance)
7673

7774
def test_query_timeout_configurable(self, default_instance):
@@ -102,7 +99,7 @@ def test_normal_query_is_info(self, default_instance, query_log_rows):
10299

103100
assert payload["status"] == "info"
104101
assert payload["clickhouse.query_type"] == "finish"
105-
assert payload["ddsource"] == "clickhouse_cloud"
102+
assert payload["ddsource"] == "clickhouse"
106103
assert payload["service"] == "clickhouse"
107104
assert payload["clickhouse.query_id"] == "abc-123-def-456"
108105
assert payload["clickhouse.user"] == "default"
@@ -143,6 +140,22 @@ def test_no_tags(self, default_instance, query_log_rows):
143140

144141
assert payload["ddtags"] == ""
145142

143+
def test_custom_cluster_name_sets_service(self, default_instance, query_log_rows):
144+
default_instance["cluster_name"] = "analytics-prod"
145+
check = _make_check(default_instance)
146+
row = query_log_rows[0]
147+
payload = check._build_query_log_payload(row)
148+
149+
assert payload["service"] == "analytics-prod"
150+
151+
def test_default_cluster_name_is_clickhouse(self, default_instance, query_log_rows):
152+
# No cluster_name in config → defaults to "clickhouse"
153+
check = _make_check(default_instance)
154+
row = query_log_rows[0]
155+
payload = check._build_query_log_payload(row)
156+
157+
assert payload["service"] == "clickhouse"
158+
146159
def test_missing_fields_use_defaults(self, default_instance):
147160
"""Rows with missing optional fields should not crash."""
148161
check = _make_check(default_instance)
@@ -168,7 +181,7 @@ def test_error_level(self, default_instance, text_log_rows):
168181
payload = check._build_text_log_payload(row)
169182

170183
assert payload["status"] == "error"
171-
assert payload["ddsource"] == "clickhouse_cloud"
184+
assert payload["ddsource"] == "clickhouse"
172185
assert payload["clickhouse.logger"] == "MergeTreeBackgroundExecutor"
173186
assert "Memory limit exceeded" in payload["message"]
174187

@@ -323,24 +336,28 @@ def test_empty_response(self, default_instance):
323336
def test_http_error_raises(self, default_instance):
324337
check = _make_check(default_instance)
325338

326-
with patch.object(
327-
check._session,
328-
"post",
329-
side_effect=requests.exceptions.HTTPError("500 Server Error"),
339+
with (
340+
patch.object(
341+
check._session,
342+
"post",
343+
side_effect=requests.exceptions.HTTPError("500 Server Error"),
344+
),
345+
pytest.raises(requests.exceptions.HTTPError),
330346
):
331-
with pytest.raises(requests.exceptions.HTTPError):
332-
check._query_clickhouse("SELECT 1")
347+
check._query_clickhouse("SELECT 1")
333348

334349
def test_connection_error_raises(self, default_instance):
335350
check = _make_check(default_instance)
336351

337-
with patch.object(
338-
check._session,
339-
"post",
340-
side_effect=requests.exceptions.ConnectionError("DNS failed"),
352+
with (
353+
patch.object(
354+
check._session,
355+
"post",
356+
side_effect=requests.exceptions.ConnectionError("DNS failed"),
357+
),
358+
pytest.raises(requests.exceptions.ConnectionError),
341359
):
342-
with pytest.raises(requests.exceptions.ConnectionError):
343-
check._query_clickhouse("SELECT 1")
360+
check._query_clickhouse("SELECT 1")
344361

345362
def test_multiline_json_each_row_parsed(self, default_instance):
346363
check = _make_check(default_instance)
@@ -372,9 +389,7 @@ def test_custom_timeout_used(self, default_instance):
372389

373390
class TestCollectQueryLogs:
374391
@patch("checks.clickhouse_cloud.ClickHouseCloudCheck._query_clickhouse")
375-
def test_sends_logs_and_updates_cursor(
376-
self, mock_query, default_instance, query_log_rows
377-
):
392+
def test_sends_logs_and_updates_cursor(self, mock_query, default_instance, query_log_rows):
378393
check = _make_check(default_instance)
379394
mock_query.return_value = query_log_rows
380395

@@ -411,9 +426,7 @@ def test_query_failure_reports_critical(self, mock_query, default_instance):
411426
assert len(check._sent_logs) == 0
412427

413428
@patch("checks.clickhouse_cloud.ClickHouseCloudCheck._query_clickhouse")
414-
def test_no_send_log_method_does_not_crash(
415-
self, mock_query, default_instance, query_log_rows
416-
):
429+
def test_no_send_log_method_does_not_crash(self, mock_query, default_instance, query_log_rows):
417430
check = _make_check(default_instance)
418431
mock_query.return_value = query_log_rows[:1]
419432

@@ -464,9 +477,7 @@ def test_missing_cursor_us_does_not_advance(self, mock_query, default_instance):
464477
assert check.log.warning.called
465478

466479
@patch("checks.clickhouse_cloud.ClickHouseCloudCheck._query_clickhouse")
467-
def test_gauge_reports_row_count(
468-
self, mock_query, default_instance, query_log_rows
469-
):
480+
def test_gauge_reports_row_count(self, mock_query, default_instance, query_log_rows):
470481
check = _make_check(default_instance)
471482
mock_query.return_value = query_log_rows
472483

@@ -477,9 +488,7 @@ def test_gauge_reports_row_count(
477488

478489
class TestCollectTextLogs:
479490
@patch("checks.clickhouse_cloud.ClickHouseCloudCheck._query_clickhouse")
480-
def test_sends_logs_and_updates_cursor(
481-
self, mock_query, default_instance, text_log_rows
482-
):
491+
def test_sends_logs_and_updates_cursor(self, mock_query, default_instance, text_log_rows):
483492
check = _make_check(default_instance)
484493
mock_query.return_value = text_log_rows
485494

@@ -539,9 +548,7 @@ def test_check_respects_disabled_collectors(self, mock_query, default_instance):
539548
mock_query.assert_not_called()
540549

541550
@patch("checks.clickhouse_cloud.ClickHouseCloudCheck._query_clickhouse")
542-
def test_cursor_persists_across_runs(
543-
self, mock_query, default_instance, query_log_rows
544-
):
551+
def test_cursor_persists_across_runs(self, mock_query, default_instance, query_log_rows):
545552
check = _make_check(default_instance)
546553
default_instance["collect_text_logs"] = False
547554
check.collect_text_logs = False

0 commit comments

Comments
 (0)