Skip to content

Commit c672979

Browse files
Modernize ddev create check template (DataDog#23705)
* Modernize ddev create check template to Python 3.13 idioms - Replace Python 2-style super() call with bare super().__init__() - Replace PEP 484 comment-form type annotations with inline annotations - Add typed signatures to check() and __init__() using InitConfigType and InstanceType from datadog_checks.base.types - Type conftest fixtures (Iterator[None], InstanceType) - Remove noqa: F401 markers that existed only to support comment annotations - Use dict[str, Any] instead of Dict[str, Any] in test_unit.py * Add changelog entry for PR DataDog#23705 * Shorter changelog * Add ConfigMixin and bump datadog-checks-base version in template * Enable mypy in check template via hatch check-types Enable check-types in hatch.toml so mypy runs as part of ddev test --lint on every new integration generated from the check template. Add --explicit-package-bases to handle the namespace package layout under datadog_checks/. Fix the dd_run_check fixture annotation in test_unit.py from Callable[[AgentCheck, bool], None] (which lied about the signature and was previously hidden inside a PEP 484 comment) to Callable[..., None], matching the actual fixture signature run_check(check, extract_message= False, cancel=True). * Add test_e2e template and teaching comments for tests Add tests/test_e2e.py to the check template so new integrations ship with an end-to-end test from day one. The stub is gated by @pytest.mark.e2e so it does not run under `ddev test`, only under `ddev env test`. Live assertions cover the metadata sanity checks (assert_metrics_using_metadata + assert_all_metrics_covered); a commented block lists the common per-metric, per-tag and service-check assertions. Expand tests/test_unit.py with the same teaching-comment pattern already used in check.py: a commented menu of assert_metric, assert_metric_has_tag, assert_metric_has_tag_prefix and assert_service_check. Add a commented docker_run + CheckEndpoints example to dd_environment in tests/conftest.py so users see the canonical setup pattern without shipping an empty docker/ folder, which would be misleading given how divergent real integration setups are. * Delete servicechecks comment * Use find_free_port in conftest docker_run example Address review feedback: replace the hardcoded port 1234 in the commented dd_environment example with find_free_port(get_docker_hostname()), so users following the pattern do not collide with whatever happens to be listening on their host. * Move self.config to check() and rename changelog * Change comment * Fix README template to align with current capitalization * Capitalize 'Checks' Co-authored-by: Ursula Chen <58821586+urseberry@users.noreply.github.com> * Capitalize 'Collected' Co-authored-by: Ursula Chen <58821586+urseberry@users.noreply.github.com> * Remove --explicit-package-bases from hatch.toml template * Capitalize Collected Co-authored-by: Ursula Chen <58821586+urseberry@users.noreply.github.com> * Capitalize Checks Co-authored-by: Ursula Chen <58821586+urseberry@users.noreply.github.com> --------- Co-authored-by: Ursula Chen <58821586+urseberry@users.noreply.github.com>
1 parent 656fac7 commit c672979

8 files changed

Lines changed: 99 additions & 29 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Modernize the `ddev create -t check` scaffold template to use Python 3.13 idioms.

datadog_checks_dev/datadog_checks/dev/tooling/templates/integration/check/{check_name}/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Overview
44

5-
This check monitors [{integration_name}][1] through the Datadog Agent.
5+
This check monitors [{integration_name}][1] through the Datadog Agent.
66

77
Include a high level overview of what this integration does:
88
- What does your product do (in 1-2 sentences)?
@@ -11,7 +11,7 @@ Include a high level overview of what this integration does:
1111

1212
## Setup
1313

14-
Follow the instructions below to install and configure this check for an Agent running on a host. For containerized environments, see the [Autodiscovery Integration Templates][3] for guidance on applying these instructions.
14+
Follow the instructions below to install and configure this check for an Agent running on a host. For containerized environments, see the [Autodiscovery integration templates][3] for guidance on applying these instructions.
1515

1616
### Installation
1717

@@ -27,7 +27,7 @@ Follow the instructions below to install and configure this check for an Agent r
2727

2828
[Run the Agent's status subcommand][6] and look for `{check_name}` under the Checks section.
2929

30-
## Data Collected
30+
## Data collected
3131

3232
### Metrics
3333

@@ -37,7 +37,7 @@ See [metadata.csv][7] for a list of metrics provided by this integration.
3737

3838
The {integration_name} integration does not include any events.
3939

40-
### Service Checks
40+
### Service checks
4141

4242
The {integration_name} integration does not include any service checks.
4343

datadog_checks_dev/datadog_checks/dev/tooling/templates/integration/check/{check_name}/datadog_checks/{check_name}/check.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
{license_header}
2-
from typing import Any # noqa: F401
32

4-
from datadog_checks.base import AgentCheck # noqa: F401
3+
from datadog_checks.base import AgentCheck
4+
from datadog_checks.base.types import InitConfigType, InstanceType
5+
6+
from .config_models import ConfigMixin
57

68
# from datadog_checks.base.utils.db import QueryManager
79
# from requests.exceptions import ConnectionError, HTTPError, InvalidURL, Timeout
810
# from json import JSONDecodeError
911

1012

11-
class {check_class}(AgentCheck):
12-
13+
class {check_class}(AgentCheck, ConfigMixin):
1314
# This will be the prefix of every metric the integration sends
1415
__NAMESPACE__ = '{check_name}'
1516

16-
def __init__(self, name, init_config, instances):
17-
super({check_class}, self).__init__(name, init_config, instances)
18-
19-
# Use self.instance to read the check configuration
20-
# self.url = self.instance.get("url")
17+
def __init__(self, name: str, init_config: InitConfigType, instances: list[InstanceType]) -> None:
18+
super().__init__(name, init_config, instances)
2119

2220
# If the check is going to perform SQL queries you should define a query manager here.
2321
# More info at
@@ -32,24 +30,27 @@ def __init__(self, name, init_config, instances):
3230
# self._query_manager = QueryManager(self, self.execute_query, queries=[sample_query])
3331
# self.check_initializations.append(self._query_manager.compile_queries)
3432

35-
def check(self, _):
36-
# type: (Any) -> None
33+
def check(self, _: InstanceType) -> None:
3734
# The following are useful bits of code to help new users get started.
3835

36+
# Read validated, typed configuration from self.config (and self.shared_config).
37+
# Fields must be declared in spec.yaml and the models regenerated with `ddev validate models`.
38+
# url = self.config.url
39+
3940
# Perform HTTP Requests with our HTTP wrapper.
4041
# More info at https://datadoghq.dev/integrations-core/base/http/
4142
# try:
42-
# response = self.http.get(self.url)
43+
# response = self.http.get(url)
4344
# response.raise_for_status()
4445
# response_json = response.json()
4546

46-
# except (HTTPError, InvalidURL, ConnectionError, Timeout) as e:
47+
# except (HTTPError, InvalidURL, ConnectionError, Timeout):
4748
# self.log.debug("Could not connect", exc_info=True)
4849

49-
# except JSONDecodeError as e:
50+
# except JSONDecodeError:
5051
# self.log.debug("Could not parse JSON", exc_info=True)
5152

52-
# except ValueError as e:
53+
# except ValueError:
5354
# self.log.debug("Unexpected value", exc_info=True)
5455

5556
# This is how you submit metrics
@@ -60,7 +61,7 @@ def check(self, _):
6061
# Perform database queries using the Query Manager
6162
# self._query_manager.execute()
6263

63-
# This is how you use the persistent cache. This cache file based and persists across agent restarts.
64+
# This is how you use the persistent cache. This cache is file based and persists across agent restarts.
6465
# If you need an in-memory cache that is persisted across runs
6566
# You can define a dictionary in the __init__ method.
6667
# self.write_persistent_cache("key", "value")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[env.collectors.datadog-checks]
2+
check-types = true
23

34
[[envs.default.matrix]]
45
python = ["3.13"]

datadog_checks_dev/datadog_checks/dev/tooling/templates/integration/check/{check_name}/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ classifiers = [
2929
"Topic :: System :: Monitoring",
3030
]
3131
dependencies = [
32-
"datadog-checks-base>=37.33.0",
32+
"datadog-checks-base>=37.36.0",
3333
]
3434
dynamic = [
3535
"version",
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
{license_header}
2+
from typing import Iterator
3+
24
import pytest
35

6+
from datadog_checks.base.types import InstanceType
7+
48

59
@pytest.fixture(scope='session')
6-
def dd_environment():
10+
def dd_environment() -> Iterator[None]:
11+
# When the integration has a real test environment, wire it here.
12+
# Typical Docker Compose setup:
13+
#
14+
# from pathlib import Path
15+
# from datadog_checks.dev import docker_run
16+
# from datadog_checks.dev.conditions import CheckEndpoints
17+
# from datadog_checks.dev.docker import get_docker_hostname
18+
# from datadog_checks.dev.utils import find_free_port
19+
#
20+
# host = get_docker_hostname()
21+
# port = find_free_port(host)
22+
# compose_file = Path(__file__).parent / "docker" / "docker-compose.yml"
23+
# with docker_run(
24+
# compose_file=str(compose_file),
25+
# env_vars={{"PORT": str(port)}},
26+
# conditions=[CheckEndpoints(f"http://{{host}}:{{port}}/health", attempts=60, wait=2)],
27+
# ):
28+
# yield {{"instances": [{{"openmetrics_endpoint": f"http://{{host}}:{{port}}/metrics"}}]}}
729
yield
830

931

1032
@pytest.fixture
11-
def instance():
33+
def instance() -> InstanceType:
1234
return {{}}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{license_header}
2+
3+
from typing import Any
4+
5+
import pytest
6+
7+
from datadog_checks.base.types import InstanceType
8+
from datadog_checks.dev.utils import get_metadata_metrics
9+
10+
11+
@pytest.mark.e2e
12+
def test_e2e(dd_agent_check: Any, instance: InstanceType) -> None:
13+
aggregator = dd_agent_check(instance, rate=True)
14+
15+
# Assert every metric emitted is declared in metadata.csv with the correct type and unit,
16+
# and that every metric in metadata.csv was emitted at least once.
17+
aggregator.assert_metrics_using_metadata(get_metadata_metrics())
18+
19+
# Assert no metric was emitted that wasn't covered by an assertion above.
20+
aggregator.assert_all_metrics_covered()
21+
22+
# Other useful assertions to consider for end-to-end coverage:
23+
# aggregator.assert_metric('{check_name}.<metric>', value=1.23, count=1, tags=['foo:bar'])
24+
# aggregator.assert_metric_has_tag('{check_name}.<metric>', 'env:prod')
25+
# aggregator.assert_metric_has_tag_prefix('{check_name}.<metric>', 'host:')
Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
11
{license_header}
22

3-
from typing import Any, Callable, Dict # noqa: F401
3+
from typing import Callable
44

5-
from datadog_checks.base import AgentCheck # noqa: F401
6-
from datadog_checks.base.stubs.aggregator import AggregatorStub # noqa: F401
5+
from datadog_checks.base.stubs.aggregator import AggregatorStub
6+
from datadog_checks.base.types import InstanceType
77
from datadog_checks.dev.utils import get_metadata_metrics
88
from datadog_checks.{check_name} import {check_class}
99

1010

11-
def test_check(dd_run_check, aggregator, instance):
12-
# type: (Callable[[AgentCheck, bool], None], AggregatorStub, Dict[str, Any]) -> None
11+
def test_check(
12+
dd_run_check: Callable[..., None],
13+
aggregator: AggregatorStub,
14+
instance: InstanceType,
15+
) -> None:
1316
check = {check_class}('{check_name}', {{}}, [instance])
1417
dd_run_check(check)
1518

16-
aggregator.assert_all_metrics_covered()
19+
# Assert every metric emitted is declared in metadata.csv with the correct type and unit,
20+
# and that every metric in metadata.csv was emitted.
1721
aggregator.assert_metrics_using_metadata(get_metadata_metrics())
22+
23+
# The following are useful assertions to help new users get started.
24+
25+
# Assert a specific metric was emitted with a specific value, count, and tag set.
26+
# aggregator.assert_metric('{check_name}.<metric>', value=1.23, count=1, tags=['foo:bar'])
27+
28+
# Assert a metric carries a specific tag (exact match) or any tag with a given prefix.
29+
# aggregator.assert_metric_has_tag('{check_name}.<metric>', 'env:prod')
30+
# aggregator.assert_metric_has_tag_prefix('{check_name}.<metric>', 'host:')
31+
32+
# Assert a service check was emitted with a specific status.
33+
# from datadog_checks.base.constants import ServiceCheck
34+
# aggregator.assert_service_check('{check_name}.can_connect', ServiceCheck.OK, count=1)
35+
36+
# Assert nothing was emitted that wasn't covered by an assertion above.
37+
aggregator.assert_all_metrics_covered()

0 commit comments

Comments
 (0)