Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions vault/assets/configuration/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ name: Vault
fleet_configurable: true
files:
- name: vault.yaml
discovery:
strategies:
- strategy: from_ports
port_hints:
- 8200
candidates:
- use_openmetrics: "true"
api_url: "http://{service.host}:{port.number}/v1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Probe HTTPS Vault listeners as well

When a Vault listener has TLS enabled, every generated candidate uses plaintext http://, so each probe fails before it can collect the leader/health metric and discovery returns no configuration. This excludes secure production Vault deployments even though the check's api_url supports HTTPS; include HTTPS candidates or derive the listener scheme rather than only probing HTTP.

Useful? React with 👍 / 👎.

no_token: "true"
- use_openmetrics: "true"
api_url: "http://{service.host}:{port.number}/v1"
- api_url: "http://{service.host}:{port.number}/v1"
no_token: "true"
- api_url: "http://{service.host}:{port.number}/v1"
options:
- template: init_config
options:
Expand Down Expand Up @@ -86,3 +100,10 @@ files:
path: /var/log/vault.log
source: vault
service: <SERVICE_NAME>
- name: auto_conf.yaml
options:
- template: ad_identifiers
overrides:
value.example:
- vault
- template: auto_conf/discovery
1 change: 1 addition & 0 deletions vault/changelog.d/24851.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Config Discovery support.
67 changes: 67 additions & 0 deletions vault/datadog_checks/vault/config_models/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# (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 <INTEGRATION_NAME>
# ddev -x validate models -s <INTEGRATION_NAME>

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.vault.config_models import discovery_overrides
from datadog_checks.vault.config_models.instance import InstanceConfig
from datadog_checks.vault.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, [8200]):
ctx = {'port': port}
instance_data = {
'use_openmetrics': 'true',
'api_url': 'http://{service.host}:{port.number}/v1'.format(service=service, **ctx),
'no_token': 'true',
}
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]}
instance_data = {
'use_openmetrics': 'true',
'api_url': 'http://{service.host}:{port.number}/v1'.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]}
instance_data = {
'api_url': 'http://{service.host}:{port.number}/v1'.format(service=service, **ctx),
'no_token': 'true',
}
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]}
instance_data = {
'api_url': 'http://{service.host}:{port.number}/v1'.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)
12 changes: 12 additions & 0 deletions vault/datadog_checks/vault/config_models/discovery_overrides.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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:<function_name>`. 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': ...}
19 changes: 19 additions & 0 deletions vault/datadog_checks/vault/data/auto_conf.yaml
Original file line number Diff line number Diff line change
@@ -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:
- vault

## Enables configuration discovery
#
discovery: {}

## Unused init configuration
#
init_config:

## Unused instance configuration
#
instances: []
2 changes: 1 addition & 1 deletion vault/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
"Private :: Do Not Upload",
]
dependencies = [
"datadog-checks-base>=37.33.0",
"datadog-checks-base>=37.41.0",
]
dynamic = [
"version",
Expand Down
12 changes: 10 additions & 2 deletions vault/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
import requests

from datadog_checks.dev import LazyFunction, TempDir, docker_run, run_command
from datadog_checks.dev import LazyFunction, TempDir, docker_run, get_e2e_discovery_metadata, run_command
from datadog_checks.dev.ci import running_on_ci
from datadog_checks.dev.conditions import WaitFor
from datadog_checks.dev.fs import create_file
Expand Down Expand Up @@ -97,7 +97,15 @@ def dd_environment(e2e_instance, dd_save_state):
):
dd_save_state('client_token_path', token_file)

yield e2e_instance(), {'docker_volumes': ['{}:/home/vault-sink'.format(sink_dir)]}
yield (
e2e_instance(),
{
'docker_volumes': [
'{}:/home/vault-sink'.format(sink_dir),
*get_e2e_discovery_metadata()['docker_volumes'],
]
},
)


class ApplyPermissions(LazyFunction):
Expand Down
45 changes: 45 additions & 0 deletions vault/tests/test_discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# (C) Datadog, Inc. 2026-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import pytest

from datadog_checks.base.utils.discovery import Port, Service
from datadog_checks.vault import Vault

pytestmark = [pytest.mark.unit]


def generated_instances(service: Service) -> list[dict]:
return [config['instances'][0] for config in Vault.generate_configs(service)]


def test_generates_one_candidate_per_mode_and_token_strategy() -> None:
# Order matters: discovery accepts the first candidate whose real check run collects a
# metric, so the richest (full OpenMetrics scrape) candidates must be tried before the
# safe fallbacks that only ever hit the always-unauthenticated leader/health endpoints.
service = Service(id='vault', host='127.0.0.1', ports=(Port(number=8200),))

instances = generated_instances(service)

assert [(instance.get('use_openmetrics'), instance.get('no_token')) for instance in instances] == [
(True, True),
(True, False),
(False, True),
(False, False),
]


def test_all_candidates_target_the_same_api_url() -> None:
service = Service(id='vault', host='127.0.0.1', ports=(Port(number=8200),))

instances = generated_instances(service)

assert all(instance['api_url'] == 'http://127.0.0.1:8200/v1' for instance in instances)


def test_ipv6_host_is_bracketed_in_generated_api_url() -> None:
service = Service(id='vault', host='fd00::1', ports=(Port(number=8200),))

instances = generated_instances(service)

assert all(instance['api_url'] == 'http://[fd00::1]:8200/v1' for instance in instances)
59 changes: 58 additions & 1 deletion vault/tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

import pytest

from .common import auth_required
from datadog_checks.dev.docker import assert_all_discovery_candidates_stable
from datadog_checks.vault import Vault

from .common import auth_required, noauth_required
from .utils import assert_collection


Expand All @@ -18,3 +21,57 @@ def test_e2e(dd_agent_check, e2e_instance, global_tags, use_openmetrics, use_aut
aggregator = dd_agent_check(instance, rate=True)

assert_collection(aggregator, global_tags, use_openmetrics, runs=2)


@noauth_required
@pytest.mark.e2e
def test_e2e_discovery(dd_agent_check_discovery):
aggregator = dd_agent_check_discovery(rate=True)

# `assert_collection` needs at least one tag every metric carries to drive its per-metric
# coverage loop. Discovery resolves `api_url` from the container at runtime, so read back
# the tag the check itself attached instead of hardcoding a value.
api_url_tag = next(tag for tag in aggregator.metrics('vault.is_leader')[0].tags if tag.startswith('api_url:'))
assert_collection(
aggregator,
[api_url_tag],
use_openmetrics=True,
runs=2,
# These metrics only appear once a request has gone through Vault's authenticated
# logical-request pipeline (ACL/token checks, audit logging, policy lookups, lease
# issuance). The main `test_e2e` suite gets that incidentally, from auth-related tests
# that run earlier in the same long-lived container; this test's `no_token` discovery
# candidate only ever hits unauthenticated status endpoints (health/leader/metrics),
# which bypass that pipeline entirely, so these metrics can't be guaranteed present here.
exclude=(
'vault.vault.audit.log.request.count',
'vault.vault.audit.log.request.quantile',
'vault.vault.audit.log.request.sum',
'vault.vault.audit.log.request.failure.count',
'vault.vault.audit.log.response.count',
'vault.vault.audit.log.response.quantile',
'vault.vault.audit.log.response.sum',
'vault.vault.audit.log.response.failure.count',
'vault.vault.core.check.token.count',
'vault.vault.core.check.token.quantile',
'vault.vault.core.check.token.sum',
'vault.vault.core.fetch.acl_and_token.count',
'vault.vault.core.fetch.acl_and_token.quantile',
'vault.vault.core.fetch.acl_and_token.sum',
'vault.vault.core.handle.request.count',
'vault.vault.core.handle.request.quantile',
'vault.vault.core.handle.request.sum',
'vault.vault.expire.num_leases',
'vault.vault.policy.get_policy.count',
'vault.vault.policy.get_policy.quantile',
'vault.vault.policy.get_policy.sum',
'vault.vault.token.lookup.count',
'vault.vault.token.lookup.quantile',
'vault.vault.token.lookup.sum',
),
)


@pytest.mark.e2e
def test_e2e_discovery_all_candidates(dd_agent_check):
assert_all_discovery_candidates_stable(dd_agent_check, Vault, compose_service='vault-leader')
4 changes: 3 additions & 1 deletion vault/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def assert_all_metrics(aggregator):
aggregator.assert_no_duplicate_metrics()


def assert_collection(aggregator, tags, use_openmetrics, runs=1):
def assert_collection(aggregator, tags, use_openmetrics, runs=1, exclude=()):
metrics = set(METRICS)
metrics.update(METRICS_OPTIONAL)
metrics.add('is_leader')
Expand Down Expand Up @@ -73,6 +73,8 @@ def assert_collection(aggregator, tags, use_openmetrics, runs=1):
if metric.startswith(tuple(METRICS_OPTIONAL)):
at_least = 0
metric = 'vault.{}'.format(metric)
if metric in exclude:
at_least = 0

for tag in tags:
try:
Expand Down
Loading