From cf213ba83e92850a967474c2f68794d353d51061 Mon Sep 17 00:00:00 2001 From: ddog-nasirthomas Date: Tue, 2 Jun 2026 07:54:16 -0400 Subject: [PATCH 1/4] Add New External DNS Metrics: controller.consecutive.soft.errors & controller.last_reconcile (#23671) * Add metrics external_dns_controller_consecutive_soft_errors & external_dns_controller_last_reconcile_timestamp_seconds * Updating naming of controller.last_reconcile * Add changelog * Sorting metric names --- external_dns/changelog.d/23671.added | 1 + .../datadog_checks/external_dns/metrics.py | 2 ++ external_dns/metadata.csv | 14 ++++++++------ external_dns/tests/fixtures/metrics.txt | 8 +++++++- 4 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 external_dns/changelog.d/23671.added diff --git a/external_dns/changelog.d/23671.added b/external_dns/changelog.d/23671.added new file mode 100644 index 0000000000000..3065563846176 --- /dev/null +++ b/external_dns/changelog.d/23671.added @@ -0,0 +1 @@ +Added metrics external_dns.controller.last_reconcile & external_dns.controller.consecutive.soft.errors diff --git a/external_dns/datadog_checks/external_dns/metrics.py b/external_dns/datadog_checks/external_dns/metrics.py index b1c3fefe12fea..12f08cb5e0dda 100644 --- a/external_dns/datadog_checks/external_dns/metrics.py +++ b/external_dns/datadog_checks/external_dns/metrics.py @@ -9,4 +9,6 @@ 'source_errors_total': 'source.errors.total', 'registry_errors_total': 'registry.errors.total', 'external_dns_controller_last_sync_timestamp_seconds': 'controller.last_sync', + 'external_dns_controller_consecutive_soft_errors': 'controller.consecutive.soft.errors', + 'external_dns_controller_last_reconcile_timestamp_seconds': 'controller.last_reconcile', } diff --git a/external_dns/metadata.csv b/external_dns/metadata.csv index 64bfba8032af6..b69565f5f3bc6 100644 --- a/external_dns/metadata.csv +++ b/external_dns/metadata.csv @@ -1,6 +1,8 @@ -metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric -external_dns.controller.last_sync,gauge,,second,,Timestamp of last successful sync with the DNS provider,0,external_dns,controller last sync timestamp, -external_dns.registry.endpoints.total,gauge,,resource,,Number of registry endpoints,0,external_dns,registry endpoints, -external_dns.registry.errors.total,gauge,,error,,Number of registry errors,-1,external_dns,registry errors, -external_dns.source.endpoints.total,gauge,,resource,,Number of source endpoints,0,external_dns,source endpoints, -external_dns.source.errors.total,gauge,,error,,Number of source errors,-1,external_dns,source errors, +metric_name,metric_type,interval,unit_name,per_unit_name,description,orientation,integration,short_name,curated_metric,sample_tags +external_dns.controller.consecutive.soft.errors,gauge,,error,,Number of consecutive soft errors in reconciliation loop,-1,external_dns,controller consecutive soft errors,, +external_dns.controller.last_reconcile,gauge,,second,,Timestamp of last reconcile attempt,0,external_dns,controller last reconcile timestamp,, +external_dns.controller.last_sync,gauge,,second,,Timestamp of last successful sync with the DNS provider,0,external_dns,controller last sync timestamp,, +external_dns.registry.endpoints.total,gauge,,resource,,Number of registry endpoints,0,external_dns,registry endpoints,, +external_dns.registry.errors.total,gauge,,error,,Number of registry errors,-1,external_dns,registry errors,, +external_dns.source.endpoints.total,gauge,,resource,,Number of source endpoints,0,external_dns,source endpoints,, +external_dns.source.errors.total,gauge,,error,,Number of source errors,-1,external_dns,source errors,, diff --git a/external_dns/tests/fixtures/metrics.txt b/external_dns/tests/fixtures/metrics.txt index 6fff5265b2294..944716c172162 100644 --- a/external_dns/tests/fixtures/metrics.txt +++ b/external_dns/tests/fixtures/metrics.txt @@ -12,4 +12,10 @@ registry_errors_total 0 source_errors_total 0 # HELP external_dns_controller_last_sync_timestamp_seconds Timestamp of last successful sync with the DNS provider # TYPE external_dns_controller_last_sync_timestamp_seconds gauge -external_dns_controller_last_sync_timestamp_seconds 1.6343090342347014e+09 \ No newline at end of file +external_dns_controller_last_sync_timestamp_seconds 1.6343090342347014e+09 +# HELP external_dns_controller_consecutive_soft_errors Number of consecutive soft errors in reconciliation loop +# TYPE external_dns_controller_consecutive_soft_errors gauge +external_dns_controller_consecutive_soft_errors 0 +# HELP external_dns_controller_last_reconcile_timestamp_seconds Timestamp of last reconcile attempt +# TYPE external_dns_controller_last_reconcile_timestamp_seconds gauge +external_dns_controller_last_reconcile_timestamp_seconds 1.715520123e+09 \ No newline at end of file From 8e138201ed2bf38f59d4aee13b22b82055a6c802 Mon Sep 17 00:00:00 2001 From: ddog-nasirthomas Date: Tue, 2 Jun 2026 07:55:03 -0400 Subject: [PATCH 2/4] Emit Warning Events for Consul Integration (#23779) * Add new config health_check_warning_events * Emit event for warnings * Removing whitespace to match original message * Add test for warning events * Add changelog * Fix typo * Apply lint * Change wording --- consul/assets/configuration/spec.yaml | 10 ++++ consul/changelog.d/23779.added | 1 + .../consul/config_models/defaults.py | 4 ++ .../consul/config_models/instance.py | 1 + consul/datadog_checks/consul/consul.py | 49 +++++++++++++------ .../consul/data/conf.yaml.example | 6 +++ consul/tests/consul_mocks.py | 7 +++ consul/tests/test_unit.py | 33 +++++++++++++ 8 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 consul/changelog.d/23779.added diff --git a/consul/assets/configuration/spec.yaml b/consul/assets/configuration/spec.yaml index d9a2721d562e9..b89a407a4004f 100644 --- a/consul/assets/configuration/spec.yaml +++ b/consul/assets/configuration/spec.yaml @@ -178,6 +178,16 @@ files: example: 3600 minimum: 1 + - name: health_check_warning_events + fleet_configurable: true + description: | + Whether to emit an event when a Consul health check transitions to `warning`. + Events for critical health checks are always emitted when `collect_health_checks` is enabled. + value: + type: boolean + example: true + default: false + - template: instances/http - template: instances/default - template: logs diff --git a/consul/changelog.d/23779.added b/consul/changelog.d/23779.added new file mode 100644 index 0000000000000..6480dd9925adb --- /dev/null +++ b/consul/changelog.d/23779.added @@ -0,0 +1 @@ +Add new config `health_check_warning_events` to emit an event when a Consul health check transitions to `warning`. diff --git a/consul/datadog_checks/consul/config_models/defaults.py b/consul/datadog_checks/consul/config_models/defaults.py index 8366ca94f0602..2af51f3fb0f1d 100644 --- a/consul/datadog_checks/consul/config_models/defaults.py +++ b/consul/datadog_checks/consul/config_models/defaults.py @@ -48,6 +48,10 @@ def instance_enable_legacy_tags_normalization(): return True +def instance_health_check_warning_events(): + return False + + def instance_health_checks_cache_size(): return 5000 diff --git a/consul/datadog_checks/consul/config_models/instance.py b/consul/datadog_checks/consul/config_models/instance.py index 5f65a49cb7088..d7d948440ab2b 100644 --- a/consul/datadog_checks/consul/config_models/instance.py +++ b/consul/datadog_checks/consul/config_models/instance.py @@ -76,6 +76,7 @@ class InstanceConfig(BaseModel): enable_legacy_tags_normalization: Optional[bool] = None extra_headers: Optional[MappingProxyType[str, Any]] = None headers: Optional[MappingProxyType[str, Any]] = None + health_check_warning_events: Optional[bool] = None health_checks_cache_size: Optional[int] = Field(None, ge=1) health_checks_cache_ttl: Optional[int] = Field(None, ge=1) kerberos_auth: Optional[Literal['required', 'optional', 'disabled']] = None diff --git a/consul/datadog_checks/consul/consul.py b/consul/datadog_checks/consul/consul.py index 1501e2b5a48ef..d31393a5419ac 100644 --- a/consul/datadog_checks/consul/consul.py +++ b/consul/datadog_checks/consul/consul.py @@ -116,6 +116,12 @@ def __init__(self, name, init_config, instances): self.collect_health_checks = self.instance.get( 'collect_health_checks', self.init_config.get('collect_health_checks', False) ) + self.health_check_warning_events = is_affirmative( + self.instance.get( + 'health_check_warning_events', + self.init_config.get('health_check_warning_events', False), + ) + ) if self.threads_count > 1: self.thread_pool = ThreadPool(self.threads_count) @@ -407,22 +413,33 @@ def check(self, _): self.gauge(HEALTH_CHECK_METRIC, status_value, tags=main_tags + node_tags) self.health_checks[hc_id] = status_value - if last_hc_value != status_value and status_value == 3: - check_name = check.get("Name", "Consul Health Check") - check_output = check.get("Output", "") - self.event( - { - "timestamp": timestamp(), - "event_type": "consul.check_failed", - "alert_type": "error", - "source_type_name": SOURCE_TYPE_NAME, - "msg_title": f"{check_name} Failed", - "aggregation_key": "consul.status_check", - "msg_text": f"Check {check_id} for service {service_name}, id: {service_id}" - f"failed on node {node_name}: {check_output}", - "tags": node_tags, - } - ) + if last_hc_value != status_value: + if status_value == 3 or (status_value == 2 and self.health_check_warning_events): + check_name = check.get("Name", "Consul Health Check") + check_output = check.get("Output", "") + + if status_value == 3: + event_type = "consul.check_failed" + alert_type = "error" + label = "failed" + else: + event_type = "consul.check_warning" + alert_type = "warning" + label = "warning" + + self.event( + { + "timestamp": timestamp(), + "event_type": event_type, + "alert_type": alert_type, + "source_type_name": SOURCE_TYPE_NAME, + "msg_title": f"{check_name} {label.capitalize()}", + "aggregation_key": "consul.status_check", + "msg_text": f"Check {check_id} for service {service_name}, id: {service_id}" + f"{label} on node {node_name}: {check_output}", + "tags": node_tags, + } + ) if sc_id not in service_checks: service_checks[sc_id] = {'status': status, 'tags': tags} diff --git a/consul/datadog_checks/consul/data/conf.yaml.example b/consul/datadog_checks/consul/data/conf.yaml.example index 13b918baca614..ff480ed042b22 100644 --- a/consul/datadog_checks/consul/data/conf.yaml.example +++ b/consul/datadog_checks/consul/data/conf.yaml.example @@ -160,6 +160,12 @@ instances: # # health_checks_cache_ttl: 3600 + ## @param health_check_warning_events - boolean - optional - default: false + ## Whether to emit an event when a Consul health check transitions to `warning`. + ## Events for critical health checks are always emitted when `collect_health_checks` is enabled. + # + # health_check_warning_events: true + ## @param proxy - mapping - optional ## This overrides the `proxy` setting in `init_config`. ## diff --git a/consul/tests/consul_mocks.py b/consul/tests/consul_mocks.py index fa4e164987229..24ad54eeb425e 100644 --- a/consul/tests/consul_mocks.py +++ b/consul/tests/consul_mocks.py @@ -306,6 +306,13 @@ def mock_get_coord_nodes_benchmark(num_nodes): return nodes +def mock_get_health_check_with_warning(_): + checks = mock_get_health_check(_) + checks[0]["Status"] = "warning" + checks[0]["Output"] = "disk usage high" + return checks + + def mock_get_health_check(_): return [ { diff --git a/consul/tests/test_unit.py b/consul/tests/test_unit.py index f7dd1bc797608..94009731a4253 100644 --- a/consul/tests/test_unit.py +++ b/consul/tests/test_unit.py @@ -331,6 +331,39 @@ def test_health_checks(aggregator, collect_health_checks, expected_metric_count, aggregator.assert_event(exact_match=False, count=expected_metric_count, **event) +@pytest.mark.parametrize( + 'health_check_warning_events, expected_warning_events', + [ + pytest.param(True, 1, id="warning events enabled"), + pytest.param(False, 0, id="warning events disabled"), + ], +) +def test_health_check_warning_events(aggregator, health_check_warning_events, expected_warning_events): + config = consul_mocks.MOCK_CONFIG_DISABLE_SERVICE_TAG.copy() + config['collect_health_checks'] = True + config['health_check_warning_events'] = health_check_warning_events + consul_check = ConsulCheck(common.CHECK_NAME, {}, [config]) + my_mocks = consul_mocks._get_consul_mocks() + my_mocks['consul_request'] = consul_mocks.mock_get_health_check_with_warning + consul_mocks.mock_check(consul_check, my_mocks) + consul_check.check(None) + + warning_events = [e for e in aggregator.events if e['event_type'] == 'consul.check_warning'] + assert len(warning_events) == expected_warning_events + if expected_warning_events: + assert warning_events[0]['alert_type'] == 'warning' + assert warning_events[0]['msg_title'] == "Service 'server-loadbalancer' check Warning" + assert ( + warning_events[0]['msg_text'] + == "Check server-loadbalancer for service server-loadbalancer, id: server-loadbalancerwarning " + "on node node-2: disk usage high" + ) + + consul_check.check(None) + warning_events = [e for e in aggregator.events if e['event_type'] == 'consul.check_warning'] + assert len(warning_events) == 1 + + def test_service_checks_disable_service_tag(aggregator): consul_check = ConsulCheck(common.CHECK_NAME, {}, [consul_mocks.MOCK_CONFIG_DISABLE_SERVICE_TAG]) my_mocks = consul_mocks._get_consul_mocks() From 602ebc16e091141e15a55f2ecb922d828e6e0502 Mon Sep 17 00:00:00 2001 From: ddog-nasirthomas Date: Tue, 2 Jun 2026 07:56:42 -0400 Subject: [PATCH 3/4] Add new config use_ssl for Tibco EMS (#23732) * Add option to connect via ssl * Update model for new use_ssl config * Fixing is_affirmative import error * Add unit test for new config * Add changelog * Applying lint * Displaying true as the example for config use_ssl --- tibco_ems/assets/configuration/spec.yaml | 8 ++++++++ tibco_ems/changelog.d/23732.added | 1 + .../tibco_ems/config_models/defaults.py | 4 ++++ .../tibco_ems/config_models/instance.py | 1 + .../datadog_checks/tibco_ems/data/conf.yaml.example | 6 ++++++ tibco_ems/datadog_checks/tibco_ems/tibco_ems.py | 11 ++++++++--- tibco_ems/tests/test_unit.py | 5 +++++ 7 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 tibco_ems/changelog.d/23732.added diff --git a/tibco_ems/assets/configuration/spec.yaml b/tibco_ems/assets/configuration/spec.yaml index 819af054484e2..45221f422df50 100644 --- a/tibco_ems/assets/configuration/spec.yaml +++ b/tibco_ems/assets/configuration/spec.yaml @@ -63,4 +63,12 @@ files: value: type: string require_trusted_provider: true + - name: use_ssl + description: | + Set to `true` to connect to the Tibco EMS server using SSL (`ssl://host:port`). + When `false`, the check uses `tcp://host:port`. + value: + type: boolean + default: false + example: true - template: instances/default diff --git a/tibco_ems/changelog.d/23732.added b/tibco_ems/changelog.d/23732.added new file mode 100644 index 0000000000000..b1fa5ec0b8f93 --- /dev/null +++ b/tibco_ems/changelog.d/23732.added @@ -0,0 +1 @@ +Add new config ``use_ssl`` to connect via SSL instead of TCP. \ No newline at end of file diff --git a/tibco_ems/datadog_checks/tibco_ems/config_models/defaults.py b/tibco_ems/datadog_checks/tibco_ems/config_models/defaults.py index b8a6d5a544e5e..b4dde6c2d5118 100644 --- a/tibco_ems/datadog_checks/tibco_ems/config_models/defaults.py +++ b/tibco_ems/datadog_checks/tibco_ems/config_models/defaults.py @@ -34,3 +34,7 @@ def instance_min_collection_interval(): def instance_port(): return 7222 + + +def instance_use_ssl(): + return False diff --git a/tibco_ems/datadog_checks/tibco_ems/config_models/instance.py b/tibco_ems/datadog_checks/tibco_ems/config_models/instance.py index dcbab946b66c0..84877e8c21f06 100644 --- a/tibco_ems/datadog_checks/tibco_ems/config_models/instance.py +++ b/tibco_ems/datadog_checks/tibco_ems/config_models/instance.py @@ -49,6 +49,7 @@ class InstanceConfig(BaseModel): service: Optional[str] = None tags: Optional[tuple[str, ...]] = None tibemsadmin: Optional[str] = None + use_ssl: Optional[bool] = None username: Optional[str] = None @model_validator(mode='before') diff --git a/tibco_ems/datadog_checks/tibco_ems/data/conf.yaml.example b/tibco_ems/datadog_checks/tibco_ems/data/conf.yaml.example index d8ee2a768829a..94245304c99f2 100644 --- a/tibco_ems/datadog_checks/tibco_ems/data/conf.yaml.example +++ b/tibco_ems/datadog_checks/tibco_ems/data/conf.yaml.example @@ -58,6 +58,12 @@ instances: # # tibemsadmin: + ## @param use_ssl - boolean - optional - default: false + ## Set to `true` to connect to the Tibco EMS server using SSL (`ssl://host:port`). + ## When `false`, the check uses `tcp://host:port`. + # + # use_ssl: true + ## @param tags - list of strings - optional ## A list of tags to attach to every metric and service check emitted by this instance. ## diff --git a/tibco_ems/datadog_checks/tibco_ems/tibco_ems.py b/tibco_ems/datadog_checks/tibco_ems/tibco_ems.py index bdfdfcbc2e1c0..d095460071191 100644 --- a/tibco_ems/datadog_checks/tibco_ems/tibco_ems.py +++ b/tibco_ems/datadog_checks/tibco_ems/tibco_ems.py @@ -5,14 +5,15 @@ import subprocess from typing import Any # noqa: F401 -from datadog_checks.base import AgentCheck +from datadog_checks.base import AgentCheck, is_affirmative from .constants import SHOW_METRIC_DATA, UNIT_PATTERN DEFAULT_HOST = 'localhost' DEFAULT_PORT = 7222 TO_BYTES = {'b': 1, 'kb': 1e3, 'mb': 1e6, 'gb': 1e9, 'tb': 1e12} -CONNECTION_STRING = 'tcp://{}:{}' +TCP_CONNECTION_STRING = 'tcp://{}:{}' +SSL_CONNECTION_STRING = 'ssl://{}:{}' class TibcoEMSCheck(AgentCheck): @@ -29,7 +30,11 @@ def __init__(self, name, init_config, instances): username = self.instance.get('username') password = self.instance.get('password') script_path = self.instance.get('script_path') - server_string = CONNECTION_STRING.format(host, port) + use_ssl = is_affirmative(self.instance.get('use_ssl', False)) + if not use_ssl: + server_string = TCP_CONNECTION_STRING.format(host, port) + else: + server_string = SSL_CONNECTION_STRING.format(host, port) self.tags = self.instance.get('tags', []) self.cmd = tibemsadmin_cmd + [ diff --git a/tibco_ems/tests/test_unit.py b/tibco_ems/tests/test_unit.py index 96f85d6d8cf1f..badbcde869556 100644 --- a/tibco_ems/tests/test_unit.py +++ b/tibco_ems/tests/test_unit.py @@ -163,3 +163,8 @@ def test_base_tags(dd_run_check, instance): # assert the lenght of tags does not grow indefinitely assert len(check.tags) == 3 + + +def test_use_ssl_server_string(instance): + check = TibcoEMSCheck('tibco_ems', {}, [{**instance, 'use_ssl': True}]) + assert 'ssl://localhost:7222' in check.cmd From 22faef8345611ab67fcfcd148a7fe1906887f463 Mon Sep 17 00:00:00 2001 From: Lucia Date: Tue, 2 Jun 2026 15:41:31 +0200 Subject: [PATCH 4/4] Pin aiohttp<3.14 in ddev test dependencies to fix vcrpy compatibility. (#23909) Co-authored-by: Claude Sonnet 4.6 --- ddev/hatch.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ddev/hatch.toml b/ddev/hatch.toml index 16d9b3980fa02..794ba47049d31 100644 --- a/ddev/hatch.toml +++ b/ddev/hatch.toml @@ -10,6 +10,8 @@ e2e-env = false dependencies = [ "pyyaml", "vcrpy", + # vcrpy uses aiohttp.streams.AsyncStreamReaderMixin, removed in aiohttp 3.14 + "aiohttp<3.14", ] # TODO: remove this when the old CLI is gone pre-install-commands = [