From 1e2acd3e9364000d3c276503db06be2c7cdb50d4 Mon Sep 17 00:00:00 2001 From: Marta Vicente Navarro Date: Thu, 16 Jul 2026 11:16:12 +0200 Subject: [PATCH 1/2] Add container-based config discovery support for haproxy Enables Autodiscovery to generate the OpenMetrics V2 instance config (port 8404, /metrics) from a discovered haproxy container's exposed ports. Co-Authored-By: Claude Sonnet 5 --- haproxy/assets/configuration/spec.yaml | 15 +++++++ .../haproxy/config_models/discovery.py | 43 +++++++++++++++++++ .../config_models/discovery_overrides.py | 12 ++++++ .../config_models/discovery_strategies.py | 18 ++++++++ .../haproxy/data/auto_conf.yaml | 19 ++++++++ haproxy/pyproject.toml | 2 +- haproxy/tests/conftest.py | 4 +- haproxy/tests/test_e2e.py | 18 ++++++++ 8 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 haproxy/datadog_checks/haproxy/config_models/discovery.py create mode 100644 haproxy/datadog_checks/haproxy/config_models/discovery_overrides.py create mode 100644 haproxy/datadog_checks/haproxy/config_models/discovery_strategies.py create mode 100644 haproxy/datadog_checks/haproxy/data/auto_conf.yaml diff --git a/haproxy/assets/configuration/spec.yaml b/haproxy/assets/configuration/spec.yaml index fa609ad42aa28..742b41dc9946d 100644 --- a/haproxy/assets/configuration/spec.yaml +++ b/haproxy/assets/configuration/spec.yaml @@ -2,6 +2,14 @@ name: HAProxy fleet_configurable: true files: - name: haproxy.yaml + discovery: + strategies: + - strategy: from_ports + port_hints: + - 8404 + candidates: + - use_openmetrics: "true" + openmetrics_endpoint: "http://{service.host}:{port.number}/metrics" options: - template: init_config options: @@ -237,3 +245,10 @@ files: port: 514 service: haproxy source: haproxy +- name: auto_conf.yaml + options: + - template: ad_identifiers + overrides: + value.example: + - haproxy + - template: auto_conf/discovery diff --git a/haproxy/datadog_checks/haproxy/config_models/discovery.py b/haproxy/datadog_checks/haproxy/config_models/discovery.py new file mode 100644 index 0000000000000..3adc59a354c2a --- /dev/null +++ b/haproxy/datadog_checks/haproxy/config_models/discovery.py @@ -0,0 +1,43 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# This file is autogenerated. +# To change this file you should edit assets/configuration/spec.yaml and then run the following commands: +# ddev -x validate config -s +# ddev -x validate models -s + +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + +from datadog_checks.base.utils.discovery import Service, candidate_ports +from datadog_checks.haproxy.config_models import discovery_overrides +from datadog_checks.haproxy.config_models.instance import InstanceConfig +from datadog_checks.haproxy.config_models.shared import SharedConfig + + +def _generated_candidates(service: Service) -> Iterator[dict[str, Any]]: + shared = SharedConfig.model_validate({}, context={'configured_fields': frozenset()}).model_dump( + by_alias=True, mode='json', exclude_none=True + ) + # discovery[0]: from_ports + for port in candidate_ports(service, [8404]): + ctx = {'port': port} + instance_data = { + 'use_openmetrics': 'true', + 'openmetrics_endpoint': 'http://{service.host}:{port.number}/metrics'.format(service=service, **ctx), + } + instance = InstanceConfig.model_validate( + instance_data, context={'configured_fields': frozenset(instance_data)} + ).model_dump(by_alias=True, mode='json', exclude_none=True) + yield {'init_config': shared, 'instances': [instance]} + + +def candidates(service: Service) -> Iterator[dict[str, Any]]: + override = getattr(discovery_overrides, 'candidates', None) + if override is None: + yield from _generated_candidates(service) + else: + yield from override(service, default=_generated_candidates) diff --git a/haproxy/datadog_checks/haproxy/config_models/discovery_overrides.py b/haproxy/datadog_checks/haproxy/config_models/discovery_overrides.py new file mode 100644 index 0000000000000..66af68809dd4c --- /dev/null +++ b/haproxy/datadog_checks/haproxy/config_models/discovery_overrides.py @@ -0,0 +1,12 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# Override the generated discovery candidates() for this integration. +# +# Define a candidates(service, default) function to wrap or replace the generated +# candidate generation. `default` is the generated generator; call it to reuse +# the spec-driven candidates, or ignore it to replace them entirely. +# +# def candidates(service, default): +# yield from default(service) diff --git a/haproxy/datadog_checks/haproxy/config_models/discovery_strategies.py b/haproxy/datadog_checks/haproxy/config_models/discovery_strategies.py new file mode 100644 index 0000000000000..5ac036ddb4684 --- /dev/null +++ b/haproxy/datadog_checks/haproxy/config_models/discovery_strategies.py @@ -0,0 +1,18 @@ +# (C) Datadog, Inc. 2026-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +# Here you can define custom (local:) discovery strategies for this integration. +# +# Decorate a generator with @discovery_strategy (imported from +# datadog_checks.base.utils.discovery) and reference it from the spec discovery +# stanza as `strategy: local:`. The function receives the +# discovered Service plus the inputs declared in the spec and yields one context +# (ctx) mapping per candidate, exposing the keys listed in `provides`. +# +# from datadog_checks.base.utils.discovery import discovery_strategy +# +# @discovery_strategy(provides=('svc',)) +# def from_some_config(service, config_path): +# ... +# yield {'svc': ...} diff --git a/haproxy/datadog_checks/haproxy/data/auto_conf.yaml b/haproxy/datadog_checks/haproxy/data/auto_conf.yaml new file mode 100644 index 0000000000000..862f97cb313f2 --- /dev/null +++ b/haproxy/datadog_checks/haproxy/data/auto_conf.yaml @@ -0,0 +1,19 @@ +## @param ad_identifiers - list of strings - required +## A list of container identifiers that are used by Autodiscovery to identify +## which container the check should be run against. For more information, see: +## https://docs.datadoghq.com/agent/guide/ad_identifiers/ +# +ad_identifiers: + - haproxy + +## Enables configuration discovery +# +discovery: {} + +## Unused init configuration +# +init_config: + +## Unused instance configuration +# +instances: [] diff --git a/haproxy/pyproject.toml b/haproxy/pyproject.toml index 0ba1a8dca9198..ce39adf3b1a47 100644 --- a/haproxy/pyproject.toml +++ b/haproxy/pyproject.toml @@ -28,7 +28,7 @@ classifiers = [ "Private :: Do Not Upload", ] dependencies = [ - "datadog-checks-base>=37.33.0", + "datadog-checks-base>=37.41.0", ] dynamic = [ "version", diff --git a/haproxy/tests/conftest.py b/haproxy/tests/conftest.py index b32c6c12f585d..67b7af75d2b60 100644 --- a/haproxy/tests/conftest.py +++ b/haproxy/tests/conftest.py @@ -14,7 +14,7 @@ import requests from packaging import version -from datadog_checks.dev import TempDir, WaitFor, docker_run +from datadog_checks.dev import TempDir, WaitFor, docker_run, get_e2e_discovery_metadata from datadog_checks.haproxy import HAProxyCheck from datadog_checks.haproxy.metrics import METRIC_MAP @@ -49,7 +49,7 @@ def dd_environment(): yield e else: with docker_run(compose_file=os.path.join(HERE, 'docker', 'haproxy.yaml'), endpoints=[ENDPOINT_PROMETHEUS]): - yield INSTANCE + yield INSTANCE, get_e2e_discovery_metadata() @pytest.fixture(scope='session') diff --git a/haproxy/tests/test_e2e.py b/haproxy/tests/test_e2e.py index 30619a4f08922..a567673bbb92e 100644 --- a/haproxy/tests/test_e2e.py +++ b/haproxy/tests/test_e2e.py @@ -4,7 +4,9 @@ import pytest from datadog_checks.base import is_affirmative +from datadog_checks.dev.docker import assert_all_discovery_candidates_stable from datadog_checks.dev.utils import get_metadata_metrics +from datadog_checks.haproxy import HAProxyCheck from .common import ENDPOINT_PROMETHEUS, HAPROXY_LEGACY, requires_new_environment @@ -40,3 +42,19 @@ def test_checkv2(dd_agent_check, instancev2, prometheus_metricsv2): aggregator.assert_all_metrics_covered() aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_e2e_discovery(dd_agent_check_discovery, prometheus_metricsv2): + aggregator = dd_agent_check_discovery(rate=True) + + # discovery resolves the container's internal network address, which differs from + # ENDPOINT_PROMETHEUS's host-mapped one, so the endpoint tag isn't asserted here. + for metric in prometheus_metricsv2: + aggregator.assert_metric('haproxy.{}'.format(metric)) + + aggregator.assert_all_metrics_covered() + aggregator.assert_metrics_using_metadata(get_metadata_metrics()) + + +def test_e2e_discovery_all_candidates(dd_agent_check): + assert_all_discovery_candidates_stable(dd_agent_check, HAProxyCheck) From b7aed483f9cef9ff70ba1c4a1fc82d7a79eb3ef9 Mon Sep 17 00:00:00 2001 From: Marta Vicente Navarro Date: Thu, 16 Jul 2026 11:16:56 +0200 Subject: [PATCH 2/2] Add changelog entry for haproxy discovery PR Co-Authored-By: Claude Sonnet 5 --- haproxy/changelog.d/24572.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 haproxy/changelog.d/24572.added diff --git a/haproxy/changelog.d/24572.added b/haproxy/changelog.d/24572.added new file mode 100644 index 0000000000000..1455667b0b73f --- /dev/null +++ b/haproxy/changelog.d/24572.added @@ -0,0 +1 @@ +Add container-based config discovery support.