From 2283bb842808b2ecc3be35b6c04ac2399f188d0b Mon Sep 17 00:00:00 2001 From: Marta Vicente Navarro Date: Thu, 2 Jul 2026 10:08:55 +0200 Subject: [PATCH] n8n: add container-based config discovery (#23964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * n8n: add container-based config discovery support * n8n: pass explicit config to dd_agent_check in regular e2e test Without an explicit config, dd_agent_check relies on conf.d which now includes the mounted auto_conf.yaml (from get_e2e_discovery_metadata). This causes autodiscovery to fire an extra check instance alongside the two static ones, corrupting metric assertions in test_check_n8n_e2e. Passing the instances explicitly forces --config-file, which overrides conf.d and isolates the test from autodiscovery — matching the krakend reference pattern. Co-Authored-By: Claude Sonnet 4.6 * Revert "n8n: pass explicit config to dd_agent_check in regular e2e test" This reverts commit 7f26c0f8f380893e38964fa1020b2145fab9d86e. * n8n: add discovery stub files * n8n: remove ad_identifiers from discovery spec stanza * n8n: update discovery.py to new generated format * n8n: add test_e2e_discovery_all_candidates Co-Authored-By: Claude Sonnet 4.6 * n8n: fix import sort order in test_e2e.py Co-Authored-By: Claude Sonnet 4.6 * fix: restore blank line between import groups in test_e2e.py * Add auto_conf.yaml section to spec.yaml so it is generated from spec. Co-Authored-By: Claude Sonnet 4.6 * Regenerate auto_conf.yaml with doc comments from spec template. Co-Authored-By: Claude Sonnet 4.6 * Use discovery/openmetrics_from_ports template in n8n spec, consistent with krakend Co-Authored-By: Claude Sonnet 4.6 * Assert both main and worker instances are discovered in test_e2e_discovery. Both containers share the same image and are both discovered: main on port 5678 (via the hint), worker on port 5680 (via the port fallback). Together they cover the full metric set so symmetric inclusion can be enabled. Uses discovery_min_instances=2 as suggested. Co-Authored-By: Claude Sonnet 4.6 * Skip test_e2e_discovery in lab mode as auto_conf.yaml is not mounted. Follows the same approach as krakend. Co-Authored-By: Claude Sonnet 4.6 * Extract common assertions into helper to align test_e2e_discovery with test_check_n8n_e2e. Co-Authored-By: Claude Sonnet 4.6 * Restore worker readiness comment and explain missing n8n_process tags in discovery test. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- n8n/assets/configuration/spec.yaml | 14 +++++++ n8n/changelog.d/23964.added | 1 + .../n8n/config_models/discovery.py | 42 +++++++++++++++++++ .../n8n/config_models/discovery_overrides.py | 12 ++++++ .../n8n/config_models/discovery_strategies.py | 18 ++++++++ n8n/datadog_checks/n8n/data/auto_conf.yaml | 19 +++++++++ n8n/tests/common.py | 2 - n8n/tests/conftest.py | 4 +- n8n/tests/test_e2e.py | 34 +++++++++++---- 9 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 n8n/changelog.d/23964.added create mode 100644 n8n/datadog_checks/n8n/config_models/discovery.py create mode 100644 n8n/datadog_checks/n8n/config_models/discovery_overrides.py create mode 100644 n8n/datadog_checks/n8n/config_models/discovery_strategies.py create mode 100644 n8n/datadog_checks/n8n/data/auto_conf.yaml diff --git a/n8n/assets/configuration/spec.yaml b/n8n/assets/configuration/spec.yaml index cea34bff83932..284c19a22d2bb 100644 --- a/n8n/assets/configuration/spec.yaml +++ b/n8n/assets/configuration/spec.yaml @@ -1,6 +1,12 @@ name: n8n files: - name: n8n.yaml + discovery: + strategies: + - template: discovery/openmetrics_from_ports + overrides: + port_hints: + - 5678 options: - template: init_config options: @@ -34,3 +40,11 @@ files: - type: docker source: n8n service: + +- name: auto_conf.yaml + options: + - template: ad_identifiers + overrides: + value.example: + - n8n + - template: auto_conf/discovery diff --git a/n8n/changelog.d/23964.added b/n8n/changelog.d/23964.added new file mode 100644 index 0000000000000..1455667b0b73f --- /dev/null +++ b/n8n/changelog.d/23964.added @@ -0,0 +1 @@ +Add container-based config discovery support. diff --git a/n8n/datadog_checks/n8n/config_models/discovery.py b/n8n/datadog_checks/n8n/config_models/discovery.py new file mode 100644 index 0000000000000..589600f71509e --- /dev/null +++ b/n8n/datadog_checks/n8n/config_models/discovery.py @@ -0,0 +1,42 @@ +# (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.n8n.config_models import discovery_overrides +from datadog_checks.n8n.config_models.instance import InstanceConfig +from datadog_checks.n8n.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, [5678]): + ctx = {'port': port} + instance_data = { + '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/n8n/datadog_checks/n8n/config_models/discovery_overrides.py b/n8n/datadog_checks/n8n/config_models/discovery_overrides.py new file mode 100644 index 0000000000000..66af68809dd4c --- /dev/null +++ b/n8n/datadog_checks/n8n/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/n8n/datadog_checks/n8n/config_models/discovery_strategies.py b/n8n/datadog_checks/n8n/config_models/discovery_strategies.py new file mode 100644 index 0000000000000..5ac036ddb4684 --- /dev/null +++ b/n8n/datadog_checks/n8n/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/n8n/datadog_checks/n8n/data/auto_conf.yaml b/n8n/datadog_checks/n8n/data/auto_conf.yaml new file mode 100644 index 0000000000000..89298a4ed7799 --- /dev/null +++ b/n8n/datadog_checks/n8n/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: + - n8n + +## Enables configuration discovery +# +discovery: {} + +## Unused init configuration +# +init_config: + +## Unused instance configuration +# +instances: [] diff --git a/n8n/tests/common.py b/n8n/tests/common.py index e16b77d30072c..7cf7c67fa62e5 100644 --- a/n8n/tests/common.py +++ b/n8n/tests/common.py @@ -169,8 +169,6 @@ } INSTANCE = MAIN_INSTANCE # back-compat default for unit tests -E2E_METADATA = {'docker_volumes': ['/var/run/docker.sock:/var/run/docker.sock:ro']} - def get_compose_env_vars() -> dict[str, str]: """Variables consumed by ``tests/docker/docker-compose.yaml``'s ``${...}`` placeholders. diff --git a/n8n/tests/conftest.py b/n8n/tests/conftest.py index 3c775384528a0..c93fe91c8a74b 100644 --- a/n8n/tests/conftest.py +++ b/n8n/tests/conftest.py @@ -12,7 +12,7 @@ import pytest import requests -from datadog_checks.dev import docker_run +from datadog_checks.dev import docker_run, get_e2e_discovery_metadata from datadog_checks.dev.conditions import CheckEndpoints, WaitFor from . import common @@ -195,7 +195,7 @@ def dd_environment() -> Iterator[Any]: }, ) else: - yield instances, common.E2E_METADATA + yield instances, get_e2e_discovery_metadata() @pytest.fixture diff --git a/n8n/tests/test_e2e.py b/n8n/tests/test_e2e.py index d16b74c257117..4f8e39fa54be5 100644 --- a/n8n/tests/test_e2e.py +++ b/n8n/tests/test_e2e.py @@ -5,11 +5,23 @@ import pytest +from datadog_checks.dev.docker import assert_all_discovery_candidates_stable from datadog_checks.dev.utils import assert_service_checks +from datadog_checks.n8n import N8nCheck from . import common +def _assert_metrics(aggregator): + aggregator.assert_metrics_using_metadata( + common.get_metadata_metrics_for_version(exclude_rare=True), + check_submission_type=True, + check_symmetric_inclusion=True, + exclude=list(common.RARE_EVENT_METRIC_NAMES), + ) + assert_service_checks(aggregator) + + @pytest.mark.e2e def test_check_n8n_e2e( dd_agent_check: Callable[..., Any], @@ -19,11 +31,19 @@ def test_check_n8n_e2e( aggregator.assert_metric('n8n.readiness.check', value=1, tags=['status_code:200', 'n8n_process:main'], at_least=1) # Worker also exposes /healthz/readiness via QUEUE_HEALTH_CHECK_ACTIVE on its own port. aggregator.assert_metric('n8n.readiness.check', value=1, tags=['status_code:200', 'n8n_process:worker'], at_least=1) + _assert_metrics(aggregator) - aggregator.assert_metrics_using_metadata( - common.get_metadata_metrics_for_version(exclude_rare=True), - check_submission_type=True, - check_symmetric_inclusion=True, - exclude=list(common.RARE_EVENT_METRIC_NAMES), - ) - assert_service_checks(aggregator) + +@pytest.mark.e2e +def test_e2e_discovery(dd_agent_check_discovery): + if common.IS_LAB: + pytest.skip('lab does not currently support configuration discovery') + + aggregator = dd_agent_check_discovery(check_rate=True, discovery_min_instances=2) + # n8n_process:main/worker tags come from instance config; the autodiscovery template only sets openmetrics_endpoint. + _assert_metrics(aggregator) + + +@pytest.mark.e2e +def test_e2e_discovery_all_candidates(dd_agent_check): + assert_all_discovery_candidates_stable(dd_agent_check, N8nCheck)