Verification tools
None of the tools below overlaps perfectly, and none is complete alone. The AI bug catalog’s Caught by line names, for each of its 27 entries, which one (if any) would have caught it — and a good share name only agent status, or nothing offline. Every output here is real: datadog/agent:7 = Agent 7.83.3 (container dd-reading-a, fake DD_API_KEY, DD_DD_URL pointed at 127.0.0.1 or a local listener, no traffic to Datadog), dd-trace 6.17.0, ddtrace 4.15.2, datadog-api-client 2.61.0, datadog Terraform provider 4.22.0 on Terraform v1.16.4 (no plan, no apply, cloud credentials unset), all run on one Mac.
flowchart TD F["Config file / code fence"] --> S["Shape and names<br/>(configcheck, tsc, terraform validate, model constructor)"] S --> V["Values and types<br/>(agent status, config dump, ApiTypeError)"] V --> R["Runtime behaviour<br/>(captured payloads, fake trace-agent, flush receiver)"] R --> M["Meaning and semantics<br/>(docs, review, the Datadog API)"]
agent configcheck — shape, not health
Section titled “agent configcheck — shape, not health”configcheck prints every configuration the Agent resolved, and a Configuration errors block for files it could not parse. It needs a running Agent (it talks to the Agent’s command socket), so docker run --rm datadog/agent:7 agent configcheck fails with unable to read artifact: open auth_token: no such file or directory; start a container, wait about 20–35 s on this Mac, then docker exec:
docker run -d --rm --name dd-reading-a \ -e DD_API_KEY=<your-api-key> -e DD_HOSTNAME=probe -e DD_DD_URL=http://127.0.0.1:1 \ -v "$PWD/conf.d:/etc/datadog-agent/conf.d" datadog/agent:7sleep 30docker exec dd-reading-a agent configcheck=== Configuration errors ===
mysql: yaml: unmarshal errors: line 4: cannot unmarshal !!map into []integration.RawMap
=== http_check check ===Configuration provider: fileConfiguration source: file:/etc/datadog-agent/conf.d/http_check.d/conf.yamlConfig for instance ID: http_check:checkout:85dc6c39d0e92c1bname: checkouttimeout: fiveurl: http://checkout:8080/health~===Read this output carefully. The mysql file (an instances: mapping, catalog entry 1) is reported. The http_check file with timeout: "five" (entry 4) is printed as a normal configuration with no complaint, because wrong-typed values are the check’s business, not the loader’s. So configcheck proves “the loader could parse the shape” and nothing else.
Blind spot, measured: agent configcheck exits with status 0 even with a Configuration errors block (configcheck exit=0 above). A CI step that only checks the exit code will pass a broken file.
🐛 Spot the bug
# CI: validate Agent integration configdocker exec dd-reading-a agent configcheck || exit 1echo "agent config OK"Show the answer
The || exit 1 never fires: measured exit status is 0 for a file that configcheck itself labels Configuration errors. It also only shows the shape half of the truth (entries 2, 3, 4 pass configcheck and fail in agent status). Grep the output for the error block and read status:
out=$(docker exec dd-reading-a agent configcheck 2>&1)if printf '%s\n' "$out" | grep -q "Configuration errors"; then printf '%s\n' "$out" exit 1fidocker exec dd-reading-a agent status 2>&1 | grep -A20 -E "Config Errors|Loading Errors" && exit 1echo "agent config OK"agent status — does the check load, and does it run?
Section titled “agent status — does the check load, and does it run?”agent status is where the previous tool’s blind spots become visible. It lists Running Checks with per-instance [ERROR] and the underlying exception, plus Config Errors and Loading Errors sections. Everything below is from the same container, after the checks had run at least once:
Config Errors ============== mysql ----- yaml: unmarshal errors: line 4: cannot unmarshal !!map into []integration.RawMap Loading Errors ============== postgres -------- Python Check Loader: could not configure check instance for python check postgres: yaml: unmarshal errors: line 3: cannot unmarshal !!str `env:prod` into []string redis ----- Python Check Loader: unable to load check redis: ... No module named 'datadog_checks.redis' ... http_check (13.1.0) Instance ID: http_check:checkout:85dc6c39d0e92c1b [ERROR] Error: Detected 1 error while loading configuration model `InstanceConfig`:timeout Input should be a valid number, unable to parse string as a numberTwo limits. First, the exit status is 0 here too (status exit=0 measured), so it is a reading tool, not a gate; parse it. Second, a check that connects to something (a database, an nginx status URL) will show a runtime [ERROR] for an unreachable host — as in catalog entry 5 (Failed to resolve 'nginx') — that is a fine result for a config review; the configuration was loaded. agent status also prints the endpoints, which is how catalog entry 6 (DD_SITE as a URL) was proven:
$ docker exec dd-reading-a agent status | grep -n "process\." https://process.https://api.datadoghq.com. - API Key ending with:agent dogstatsd-stats and a local receiver — what custom metrics really become
Section titled “agent dogstatsd-stats and a local receiver — what custom metrics really become”To see what a custom metric turns into, you need something to receive the Agent’s flush. Point DD_DD_URL at a tiny local HTTP server (nothing reaches Datadog), send DogStatsD over UDP from inside the container, and read what arrives. Two settings make it readable: DD_USE_V2_API_SERIES=false (the JSON /api/v1/series body instead of protobuf) and DD_SERIALIZER_COMPRESSOR_KIND=zlib (the default serializer compression is not zlib, so a plain Python receiver could not parse the body).
dogstatsd_metrics_stats_enable: true$ docker exec dd-reading-a agent dogstatsd-statsMetric | Tags | Count | Last Seen-----------------------------------------|----------------------|------------|---------------------orders.bounded | env:prod plan:pro | 25 | ...orders.bounded | env:prod plan:free | 25 | ...checkout.latency_dist | env:prod | 4 | ...checkout.latency_hist | env:prod | 4 | ...orders.count | env:prod user_id:u10 | 1 | ...orders.count | env:prod user_id:u41 | 1 | ...The same run captured, on the receiver:
POST /api/v1/series checkout.latency_hist.{avg,count,median,95percentile,max} (histogram)POST /api/beta/sketches (protobuf) (distribution)orders.count 50 series orders.bounded 2 seriesWithout dogstatsd_metrics_stats_enable the command answers Dogstatsd metrics stats not enabled in the Agent configuration. This is how catalog entries 17 and 18 were proven. The limits: it needs a running Agent, and it proves what the Agent saw, not what your application intended to send.
The tracer’s startup config dump and .d.ts — option names, values, and order
Section titled “The tracer’s startup config dump and .d.ts — option names, values, and order”dd-trace prints a DATADOG TRACER CONFIGURATION line at startup (on by default) with what it actually resolved. Compare two runs that differ by one key:
tracer.init({ service: 'probe', logInjection: false }) -> "log_injection_enabled":falsetracer.init({ service: 'probe', log_injection: false }) -> "log_injection_enabled":trueDD_ENV=Prod DD_SERVICE=Checkout DD_VERSION=1.0.0 -> "env":"Prod","service":"Checkout","dd_version":"1.0.0"DD_TRACE_SAMPLE_RATE=0.1 -> "sample_rate":0.1The types answer the names question without running anything: tsc --noEmit against dd-trace/index.d.ts rejects log_injection (TS2561 ... Did you mean to write 'logInjection'?) and agentUrl (TS2353), but reports only the first unknown key per object literal and does nothing for .js files. The course harness --options mode extracts the TracerOptions keys from the same .d.ts and checks every key in every tracer.init({...}) fence.
Neither tells you about order. Catalog entry 10 (init after require('express')) was proven by running the app against a fake trace-agent — a 10-line http.createServer on 127.0.0.1:28126 that records the span names found in each PUT /v0.4/traces:
init first => express.request, http.requestinit second => http.request, web.requestddtrace introspection — attributes and keyword arguments, for real
Section titled “ddtrace introspection — attributes and keyword arguments, for real”ddtrace.config is a dynamic object, so a static key list is never authoritative; the only reliable check is a real getattr:
>>> config.logs_injectionAttributeError: ... object has no attribute logs_injection, logs_injection is not a valid configuration>>> config._logs_injection # DD_LOGS_INJECTION, default TrueTrue>>> sorted(inspect.signature(tracer.configure).parameters)['apm_tracing_disabled', 'appsec_enabled', 'appsec_enabled_origin', 'compute_stats_enabled', 'context_provider', 'iast_enabled', 'trace_processors']Blind spot: a write to an unknown attribute succeeds (config.logs_injection = False creates a plain attribute), so introspection is a read-side check — the harness resolves the name with getattr, which is why it flags writes too. Environment variables come from a dump of SUPPORTED_CONFIGURATIONS (DD_LOGS_INJECTION present; DD_LOG_INJECTION and DD_TRACE_ANALYTICS_ENABLED absent from ddtrace 4.15.2).
terraform validate — names, blocks, and types against the pinned provider
Section titled “terraform validate — names, blocks, and types against the pinned provider”export TF_PLUGIN_CACHE_DIR=~/.terraform.d/plugin-cacheterraform init -backend=false -input=false && terraform validateThe provider block used in every proof here — a fake key, validate = false so the provider never calls the API, and no plan or apply ever:
# provider block used for every proof (the harness generates the same one)terraform { required_providers { datadog = { source = "DataDog/datadog" version = "4.22.0" } }}
provider "datadog" { api_key = "<your-api-key>" app_key = "<your-app-key>" validate = false}What it says, from the catalog: Unsupported argument, Unsupported block type ... Did you mean "template_variable"?, Incorrect attribute value type ... set of string required, but have string, Missing required argument. What it cannot say: whether a query string is a valid Datadog query, whether a permission.id is an ID, whether an exclusion filter is too broad. Two extra commands make it a better reading tool:
$ terraform providers schema -json | jq '.provider_schemas[].resource_schemas.datadog_monitor.block.attributes.notify_no_data.description'"A boolean indicating whether this monitor will notify when data stops reporting. Defaults to `false`."That single line corrected a claim in the planning review that the default was true. Treat the provider schema as documentation you can grep. Be careful with credentials: a developer shell often holds real cloud credentials, so run these proofs with fake Datadog keys, the cloud variables unset, and never a plan or apply.
datadog-api-client models — types and required fields, not enums
Section titled “datadog-api-client models — types and required fields, not enums”from datadog_api_client.v1.model.monitor import MonitorMonitor(**data, _check_type=True)Measured results (2.61.0), the ones that raise:
options.thresholds.critical="2" -> ApiTypeError: Invalid type for variable 'critical'. Required value type is float and passed type was str at ['options']['thresholds']['critical']tags="env:prod" -> ApiTypeError: Invalid type for variable 'tags'. Required value type is list and passed type was str at ['tags']missing query -> TypeError: Monitor.__init__() missing 1 required positional argument: 'query'and the ones that do not (each OK (constructs)): type="metrics alert", an SLO timeframe="30days", a pipeline processor type attribute_remapper, a dashboard layout_type="grid", a widget with no type, a burn_rate query with no .short_window(), a composite "12345 && not-an-id". The stricter client-side signal is the deserializer:
from datadog_api_client import ApiClient, Configurationobj = ApiClient(Configuration()).deserialize(json_text, (Monitor,), True)obj._unparsed # True when an enum value or a oneOf did not match_unparsed was True for every enum/oneOf case above. The Service Catalog check is a different mechanism: it validates against the JSON Schema published at github.com/DataDog/schema (apiVersion: v2 → 'v2' is not one of ['v3']), and it accepts unknown spec keys.
The course harness — npm run verify and its modes
Section titled “The course harness — npm run verify and its modes”tools/verify-snippets.mjs collects only fences whose first line is a path comment (# datadog.yaml, # conf.d/<check>.d/conf.yaml, // monitors/<name>.json, # terraform/<name>.tf, // src/<name>.ts, …); a first line containing @expect-error is a deliberate failure and is skipped. Any other fence is a fragment and never touched by the strict modes.
$ npm run verify # = --options + --tf + --json--options: [email protected] init/configure calls: 10 checked, 0 with unknown key(s) [email protected] config.<x> refs: 0 checked, 0 unknown; tracer.configure(kw=) refs: 0 checked, 0 unknown DD_* env var refs: 8 checked, 0 unknown, 0 deprecated--tf: provider [email protected] -- 14 fence(s) validated, 0 failed--json: [email protected] -- 8 fence(s) classified, 0 failedWhat each mode does with a bad fence — a scratch lesson with a log_injection init, a tags = "env:prod" monitor, a string critical threshold, and an instances: mapping in a conf.d file:
--options: [email protected] init/configure calls: 11 checked, 1 with unknown key(s) ERROR ...:fence #1: tracer.init({...}) unknown key(s) [log_injection] ([email protected] TracerOptions)--tf: provider [email protected] -- 15 fence(s) validated, 1 failed ERROR ...:fence #2: line 7: Incorrect attribute value type -- Inappropriate value for attribute "tags": set of string required, but have string.--json: [email protected] -- 9 fence(s) classified, 1 failed ERROR ...:fence #3 (monitor): ApiTypeError: Invalid type for variable 'critical'. Required value type is float and passed type was str at ['options']['thresholds']['critical']--agent: datadog/agent:7 configcheck -- 3 lesson group(s) checked, 0 failedThe last line is the important one. --agent reported 0 failed for the instances: mapping, although agent configcheck itself printed Configuration errors. The harness fails a group only if the output matches \bERROR\b or is not valid, and the real message is unmarshal errors under Configuration errors with exit status 0 — neither pattern matches. This is a harness limitation found while writing this module (reported to the course coordinator, not changed here). Until it is widened, treat a green --agent as “the Agent started and parsed something” and confirm with your own grep "Configuration errors" (see the Spot-the-bug above).
Other modes:
| Mode | Proves | Costs |
|---|---|---|
--options | every tracer.init({...}) key exists in the pinned .d.ts; every ddtrace.config.<x> resolves; every DD_* name is in the supported list (the harness does not flag DD_TRACE_SAMPLE_RATE; being in the supported list does not mean ddtrace 4.15.2 acts on it — catalog entry 13 shows it is a silent no-op in Python) | about a second |
--tf | terraform init + validate per fence with provider 4.22.0 | seconds per fence, needs the provider cache |
--json | Monitor / Dashboard / SLO / LogsPipeline constructor + Service Catalog JSON Schema | about a second, misses enums |
--agent | a real datadog/agent:7 starts with your YAML mounted | ~35 s per lesson, needs Docker, see the blind spot above |
--k8s | kubectl --dry-run=server on kind-verify-k8s | optional, skipped when the cluster does not exist |
--self-test | the harness’s own good/bad fixtures (jsUnknownFound, tfBadFound, jsonBadFound, agentOk, … all true) | starts a container, about a minute |
What to run, in what order
Section titled “What to run, in what order”- Lint the file’s own format (YAML/JSON/HCL parse) — nothing here is Datadog-specific.
- Names and shape:
tsc --noEmit/ harness--options,terraform validate, the model constructor,agent configcheck. - Values and loading:
agent status(read it), the tracer config dump,_unparsed. - Behaviour: a fake trace-agent, a local flush receiver.
- Everything else — meaning, ordering of exclusion filters, burn-rate grammar, Autodiscovery placement — is a docs URL and a reviewer. Say so in the review.
Mental Model
Section titled “Mental Model”- Each tool answers one question.
configcheck: can the loader read the shape.status: does it load and run. Dump: what value won.validate: does the schema know the name. Model: are the types right. Harness: did a human wire those into CI. - Exit codes are not the signal. Two of the Agent’s commands returned
0while printing errors. Read output. - A green tool has a scope. Write the scope next to the result.
Trade-offs
Section titled “Trade-offs”- Speed vs. truth.
--options/--json/--tfare seconds;--agentandagent statusafter a run are minutes but see what the others cannot. - Fake keys vs. realism. A fake
DD_API_KEYand a local receiver make every proof safe and offline; they cannot show a real intake response, a real monitor evaluation, or a real UI. - Pinned vs. current. The harness pins dd-trace, ddtrace, the client, and the provider; pinned answers are reproducible but go stale on upgrade. Re-run the catalog’s probes when a pin moves.
Common Misconceptions
Section titled “Common Misconceptions”- “If
agent configcheckreturns 0, the config is fine.” It returned 0 with aConfiguration errorsblock. - “
npm run verifycovers the Agent.” It covers--options,--tf, and--json; the Agent is--agent, run separately, with the limitation above. - “The model check is a validator.” It is a constructor that checks scalar types and required arguments.
- “
terraform validatetalked to Datadog.” It read the provider’s schema; the provider block setsvalidate = falseand no request is made.