All integrations use pydantic models as the primary way to validate and interface with configuration.
As config spec data types are based on OpenAPI 3, we automatically generate the necessary code.
The models reside in a package named config_models
located at the root of a check's namespaced package. For example, a new integration named foo
:
foo
│ ...
├── datadog_checks
│ └── foo
│ └── config_models
│ ├── __init__.py
│ ├── defaults.py
│ ├── instance.py
│ ├── shared.py
│ └── validators.py
│ └── __init__.py
│ ...
...
There are 2 possible models:
InstanceConfig
(ID:instance
) that corresponds to a check's entry in theinstances
sectionSharedConfig
(ID:shared
) that corresponds to theinit_config
section that is shared by all instances
All models are defined in <ID>.py
and are available for import directly under config_models
.
The default values for optional settings are populated in defaults.py
and are derived from the
value property of config spec options. The precedence is the default
key
followed by the example
key (if it appears to represent a real value rather than an illustrative example
and the type
is a primitive). In all other cases, the default is None
, which means there is no default
getter function.
The validation of fields for every model occurs in three high-level stages, as described in this section.
def initialize_<ID>(values: dict[str, Any], **kwargs) -> dict[str, Any]:
...
If such a validator exists in validators.py
, then it is called once with the raw config that was supplied by the user.
The returned mapping is used as the input config for the subsequent stages.
The value of each field goes through the following steps.
If a field was not supplied by the user nor during the initialization stage, then its default value is
taken from defaults.py
. This stage is skipped for required fields.
The contents of validators.py
are entirely custom and contain functions to perform extra validation if necessary.
def <ID>_<OPTION_NAME>(value: Any, *, field: pydantic.fields.FieldInfo, **kwargs) -> Any:
...
Such validators are called for the appropriate field of the proper model. The returned value is used as the new value of the option for the subsequent stages.
!!! note This only occurs if the option was supplied by the user.
A validators
key under the value property of config
spec options is considered. Every entry refers to a relative import path to a field validator
under datadog_checks.base.utils.models.validation
and is executed in the defined order.
!!! note This only occurs if the option was supplied by the user.
Every list
is converted to tuple
and every dict
is converted to types.MappingProxyType.
!!! note
A field or nested field would only be a dict
when it is defined as a mapping with arbitrary keys. Otherwise, it would be a model with its own properties as usual.
def check_<ID>(model: pydantic.BaseModel) -> pydantic.BaseModel:
...
If such a validator exists in validators.py
, then it is called with the final constructed model. At this point, it cannot
be mutated, so you can only raise errors.
A check initialization occurs before a check's first run that loads the config models. Validation errors will thus prevent check execution.
The config models package contains a class ConfigMixin
from which checks inherit:
from datadog_checks.base import AgentCheck
from .config_models import ConfigMixin
class Check(AgentCheck, ConfigMixin):
...
It exposes the instantiated InstanceConfig
model as self.config
and SharedConfig
model as self.shared_config
.
In addition to each field being converted to an immutable type, all generated models are configured as immutable.
Every option marked as deprecated in the config spec will log a warning with information about when it will be removed and what to do.
A validation command validate models
runs in our CI. To locally generate the proper files, run ddev validate models [INTEGRATION] --sync
.