|
7 | 7 |
|
8 | 8 | from datadog_checks.base import ConfigurationError |
9 | 9 | from datadog_checks.clickhouse import ClickhouseCheck, advanced_queries, queries |
10 | | -from datadog_checks.clickhouse.utils import cluster_aware_query |
| 10 | +from datadog_checks.clickhouse.utils import ( |
| 11 | + BUILTIN_SAMPLE_CLUSTERS, |
| 12 | + CLUSTER_GROUP_PREFIX, |
| 13 | + CLUSTER_MACRO_QUERY, |
| 14 | + CLUSTER_NAME_QUERY, |
| 15 | + CLUSTER_TAG, |
| 16 | + cluster_aware_query, |
| 17 | +) |
11 | 18 |
|
12 | 19 | from .utils import ensure_csv_safe, parse_described_metrics, raise_error |
13 | 20 |
|
@@ -455,3 +462,123 @@ def test_get_queries_uses_base_queries_for_direct_connection(instance, use_advan |
455 | 462 | check._server_version = '24.8' |
456 | 463 |
|
457 | 464 | assert all('clusterAllReplicas' not in q['query'] for q in check.get_queries()) |
| 465 | + |
| 466 | + |
| 467 | +def make_cluster_name_check(query_results): |
| 468 | + """Build a check whose execute_query_raw replays query_results keyed by SQL.""" |
| 469 | + check = ClickhouseCheck('clickhouse', {}, [BASE_INSTANCE]) |
| 470 | + |
| 471 | + def execute(query): |
| 472 | + result = query_results[query] |
| 473 | + if isinstance(result, Exception): |
| 474 | + raise result |
| 475 | + return result |
| 476 | + |
| 477 | + check.execute_query_raw = mock.Mock(side_effect=execute) |
| 478 | + return check |
| 479 | + |
| 480 | + |
| 481 | +def test_cluster_name_prefers_the_macro(): |
| 482 | + check = make_cluster_name_check({CLUSTER_MACRO_QUERY: [['macro_cluster']]}) |
| 483 | + |
| 484 | + assert check.cluster_name == 'macro_cluster' |
| 485 | + # system.clusters must not be consulted once the macro answers. |
| 486 | + check.execute_query_raw.assert_called_once_with(CLUSTER_MACRO_QUERY) |
| 487 | + |
| 488 | + |
| 489 | +@pytest.mark.parametrize( |
| 490 | + 'macro_result', |
| 491 | + [ |
| 492 | + pytest.param([], id='macro-not-set'), |
| 493 | + pytest.param([['']], id='empty-value'), |
| 494 | + pytest.param(Error('query failed'), id='query-error'), |
| 495 | + ], |
| 496 | +) |
| 497 | +def test_cluster_name_falls_back_to_system_clusters(macro_result): |
| 498 | + check = make_cluster_name_check({CLUSTER_MACRO_QUERY: macro_result, CLUSTER_NAME_QUERY: [['prod_cluster']]}) |
| 499 | + |
| 500 | + assert check.cluster_name == 'prod_cluster' |
| 501 | + |
| 502 | + |
| 503 | +def test_cluster_name_query_excludes_builtin_sample_clusters(): |
| 504 | + """ClickHouse <=21.x ships test_* clusters in its default config. |
| 505 | +
|
| 506 | + They are is_local and would otherwise mislabel every stock instance, so the |
| 507 | + fallback query filters them out by name rather than picking one. |
| 508 | + """ |
| 509 | + for name in BUILTIN_SAMPLE_CLUSTERS: |
| 510 | + assert f"'{name}'" in CLUSTER_NAME_QUERY |
| 511 | + |
| 512 | + assert 'test_cluster_two_shards_localhost' in BUILTIN_SAMPLE_CLUSTERS |
| 513 | + assert 'test_shard_localhost' in BUILTIN_SAMPLE_CLUSTERS |
| 514 | + |
| 515 | + |
| 516 | +def test_cluster_name_query_excludes_cloud_group_pseudo_cluster(): |
| 517 | + """ClickHouse Cloud reports both 'all_groups.default' and 'default' as is_local. |
| 518 | +
|
| 519 | + The group entry sorts first, so without the filter Cloud instances would be |
| 520 | + tagged all_groups.default while their data comes from 'default' via |
| 521 | + clusterAllReplicas. |
| 522 | + """ |
| 523 | + assert f"NOT startsWith(cluster, '{CLUSTER_GROUP_PREFIX}')" in CLUSTER_NAME_QUERY |
| 524 | + |
| 525 | + |
| 526 | +def test_cluster_name_absent_when_both_sources_fail(): |
| 527 | + check = make_cluster_name_check( |
| 528 | + { |
| 529 | + CLUSTER_MACRO_QUERY: Error('query failed'), |
| 530 | + CLUSTER_NAME_QUERY: [], |
| 531 | + } |
| 532 | + ) |
| 533 | + |
| 534 | + # No 'default' fallback: a wrong cluster name is worse than an absent one. |
| 535 | + assert check.cluster_name is None |
| 536 | + |
| 537 | + |
| 538 | +def test_cluster_name_is_cached_including_the_absent_case(): |
| 539 | + check = make_cluster_name_check({CLUSTER_MACRO_QUERY: [], CLUSTER_NAME_QUERY: []}) |
| 540 | + |
| 541 | + assert check.cluster_name is None |
| 542 | + assert check.cluster_name is None |
| 543 | + |
| 544 | + assert check.execute_query_raw.call_count == 2 # one attempt per source, not per access |
| 545 | + |
| 546 | + |
| 547 | +def test_check_tags_with_cluster(instance): |
| 548 | + check = ClickhouseCheck('clickhouse', {}, [instance]) |
| 549 | + with mock.patch.object(ClickhouseCheck, 'cluster_name', new_callable=mock.PropertyMock) as cluster_name: |
| 550 | + cluster_name.return_value = 'prod_cluster' |
| 551 | + with mock.patch('clickhouse_connect.get_client'): |
| 552 | + check.check({}) |
| 553 | + |
| 554 | + assert f'{CLUSTER_TAG}:prod_cluster' in check.tags |
| 555 | + |
| 556 | + |
| 557 | +def test_can_connect_carries_cluster_tag_from_the_second_run(aggregator, instance): |
| 558 | + """The tag needs a live client, so connect() on the first run predates it. |
| 559 | +
|
| 560 | + The tag persists on the tag manager afterwards, so every later run — and every |
| 561 | + other surface on the first run, since they all follow the resolution point — |
| 562 | + does carry it. |
| 563 | + """ |
| 564 | + check = ClickhouseCheck('clickhouse', {}, [instance]) |
| 565 | + with mock.patch.object(ClickhouseCheck, 'cluster_name', new_callable=mock.PropertyMock) as cluster_name: |
| 566 | + cluster_name.return_value = 'prod_cluster' |
| 567 | + with mock.patch('clickhouse_connect.get_client'): |
| 568 | + check.check({}) |
| 569 | + first_run_tags = list(check.tags) |
| 570 | + check.check({}) |
| 571 | + |
| 572 | + assert f'{CLUSTER_TAG}:prod_cluster' not in aggregator.service_checks('clickhouse.can_connect')[0].tags |
| 573 | + assert f'{CLUSTER_TAG}:prod_cluster' in first_run_tags |
| 574 | + aggregator.assert_service_check('clickhouse.can_connect', tags=check.tags) |
| 575 | + |
| 576 | + |
| 577 | +def test_check_omits_cluster_tag_when_unresolved(instance): |
| 578 | + check = ClickhouseCheck('clickhouse', {}, [instance]) |
| 579 | + with mock.patch.object(ClickhouseCheck, 'cluster_name', new_callable=mock.PropertyMock) as cluster_name: |
| 580 | + cluster_name.return_value = None |
| 581 | + with mock.patch('clickhouse_connect.get_client'): |
| 582 | + check.check({}) |
| 583 | + |
| 584 | + assert not any(tag.startswith(f'{CLUSTER_TAG}:') for tag in check.tags) |
0 commit comments