Skip to content
Gauge

ax-check.com

Datadog

datadog.com

ax-check.com

Checked 6 October 2026

B

84/100

Agents need your login

Datadog setup and pricing are clear and ready to use.

21 of 23 checklist items passed: install steps, code samples, API key prereqs, and pricing all shown plainly with no login wall.

2 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a text/markdown request; no Markdown representation offered.

Recorded coding sessions

Same task, 3 models, no datadog.com account. Recorded 6 October 2026 on Pi.

3 recorded · 9m 33s

9m 33s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Datadog. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user wants a simple example of using Datadog, pricing information, and an assessment of feasibility. First let's check the environment — are there any credentials? Is there a DD API key? Let's take a look at the working directory.

The instructions say: "If this product doesn't have a developer workflow that can be executed, state that clearly and stop." Datadog has an API/SDK. But if there are no credentials, we'll be blocked. Let's check the environment.

I'll check the environment first to see what's available.

ls -la /sandbox/repo 2>/dev/null; echo "---"; env | grep -iE 'datadog|DD_|datad' | sed 's/=.*/=<set>/' ; echo "---"; which python3 node npm curl 2>/dev/null
total 16
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Oct  6 19:54 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
---
---
/usr/local/bin/python3
/usr/local/bin/node
/usr/local/bin/npm
/usr/bin/curl
pip list 2>/dev/null | grep -i datadog; npm ls -g --depth=0 2>/dev/null | grep -i datadog; echo "---"; ls /usr/local/lib/node_modules 2>/dev/null
---
---
@anthropic-ai
@earendil-works
@openai
corepack
npm
Thinking…

Empty repository, Datadog SDK not installed, no DD_API_KEY / DD_APP_KEY environment variables. Let me check if there's network access and whether I can install the datadog-api-client SDK.

Important question: does Datadog have a developer workflow that can be executed? Yes — there's a REST API and official SDKs (datadog-api-client for Python, @datadog/datadog-api-client for Node). But credentials are required. Without API/App keys, I can't make live calls.

Options:

  1. Build a sample using a mock/fake transport so I can verify the code works without credentials (no local service stack needed — just mocking the HTTP layer).
  2. Clearly report that I'm blocked on credentials.

First let me check network access and try installing the SDK. This is quick and lightweight.

Also, I should verify the pricing info — rather than hallucinating numbers, I should fetch the actual pricing page to get accurate information. Let me try curl on datadoghq.com/pricing.

Run these in parallel.

Empty repo, no Datadog credentials in env, no SDK installed. Let me check network access and try to get real pricing info.

cd /tmp && timeout 25 curl -s -o /dev/null -w "pypi:%{http_code}\n" https://pypi.org/simple/datadog-api-client/; timeout 25 curl -s -o /dev/null -w "npm:%{http_code}\n" https://registry.npmjs.org/@datadog%2Fdatadog-api-client; timeout 25 curl -s -o /dev/null -w "ddq:%{http_code}\n" https://www.datadoghq.com/pricing/
pypi:200
pypi:200
npm:200
ddq:200
env | grep -icE 'api_key|token|secret'; echo "--- git log:"; cd /sandbox/repo && git log --oneline -5 2>&1 | head; echo "--- netrc/config files:"; ls -la ~/.datadog* ~/.config/datadog* 2>&1 | head -5
1
1
--- git log:
9ebf5fb Initial workspace
--- netrc/config files:
ls: cannot access '/sandbox/.datadog*': No such file or directory
ls: cannot access '/sandbox/.config/datadog*': No such file or directory
Thinking…

Network access is working normally, and PyPI/npm are reachable. However, Datadog credentials don't exist anywhere. Let's check what the only thing that matched api_key|token|secret was (it might be unrelated, like an OPENAI key or something).

Let's verify that.

Also, to get accurate pricing information, I'll fetch the pricing page too.

Network works, PyPI/npm reachable. But there are no Datadog credentials anywhere. Let me confirm and fetch real pricing data.

env | grep -iE 'api_key|token|secret' | sed -E 's/=.*/=<redacted>/'
PI_GATEWAY_API_KEY=<redacted>
PI_GATEWAY_API_KEY=<redacted>
cd /tmp && timeout 30 curl -s -A "Mozilla/5.0" https://www.datadoghq.com/pricing/ -o ddq.html; ls -la ddq.html; python3 - <<'EOF'
import re,html
t=open('/tmp/ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S)
t=re.sub(r'<style.*?</style>','',t,flags=re.S)
txt=html.unescape(re.sub(r'<[^>]+>',' ',t))
txt=re.sub(r'\s+',' ',txt)
for m in re.finditer(r'\$\d[\d,.]*',txt):
    s=max(0,m.start()-140); print('...'+txt[s:m.end()+110]+'...'); print('-'*100)
EOF
-rw------- 1 root root 1638768 Oct 6 19:54 ddq.html
-rw------- 1 root root 1638768 Oct  6 19:54 ddq.html
...orecast spend to stay ahead of changes Set budgets and stay on track Limited to Datadog costs only Start Free Trial Pro Starting At $ 5 Per $1,000 in cloud/SaaS spend, per month* Understand and optimize your cloud & SaaS costs Everything included in Datado...
----------------------------------------------------------------------------------------------------
...st spikes with a FinOps agent for root cause analysis *Billed annually or $ 7.20 on-demand Start Free Trial Enterprise Starting At $ 10 Per $1,000 in cloud/SaaS spend, per month* Enterprise-grade FinOps with automation and governance Everything included in...
----------------------------------------------------------------------------------------------------
Thinking…

The pricing page I fetched was redirected/rendered toward Cloud Cost Management. The main pricing page is a large SPA. Let me try fetching the pricing page for a specific product — for example, the infrastructure monitoring pricing page. That's the core product for the metrics example I'm going to create.

The only environment variable that matched was PI_GATEWAY_API_KEY (pi's own gateway key), not Datadog's.

Let me fetch the pricing pages for several specific products:

Let me extract the prices.

Actually, the pricing page is a big single page with all the products listed. In my regex I only found 2 dollar amounts, which means the content is mostly JS-rendered. Let me look for JSON embedded in the page, or try a different approach.

Let me search for pricing data within the HTML — maybe it's inside a NEXT_DATA script or similar.

Also, there's an official Datadog pricing API! There's a "Pricing API" in beta: GET /api/v2/usage/... no, that's not it. Actually, it's https://docs.datadoghq.com/api/latest/pricing/ — GET /api/v2/pricing. But this requires authentication.

Let me grep the HTML to look for pricing JSON.

Only PI_GATEWAY_API_KEY (pi's own key) — no Datadog credentials. Let me dig real pricing numbers out of the page data.

cd /tmp && python3 - <<'EOF'
import re,json,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
# look for embedded JSON blobs with price fields
hits=re.findall(r'"(?:price|amount|usdPrice|monthlyPrice)"\s*:\s*"?[\d.]+"?',t)
print("json price fields:",len(hits), hits[:20])
# any $ in scripts
scr=''.join(re.findall(r'<script[^>]*>(.*?)</script>',t,flags=re.S))
print("script len:",len(scr))
for m in list(re.finditer(r'\$\s?\d[\d,.]*',scr))[:25]:
    print('  ',repr(scr[max(0,m.start()-90):m.end()+60]))
EOF
json price fields: 0 []
json price fields: 0 []
script len: 25399
cd /tmp && for p in infrastructure log-management apm; do timeout 25 curl -s -A "Mozilla/5.0" "https://www.datadoghq.com/pricing/?product=$p" -o "p_$p.html"; echo "$p: $(wc -c < p_$p.html) bytes"; done; grep -o 'Per [A-Za-z0-9 ,/]*' p_infrastructure.html 2>/dev/null | sort -u | head
infrastructure: 1638768 bytes
infrastructure: 1638768 bytes
log-management: 1638768 bytes
apm: 1638768 bytes
Per 
Per 1,000 sessions,
Per 1,000 tests,
Per 100 Workflow Executions,
Per 10K test runs, per month
Per 1M objects monitored,
Per 500 credits,
Per GB analyzed
Per GB of uncompressed data forwarded per destination
Per GB of uncompressed data ingested for processing, or compressed data scanned for rehydrating
Thinking…

The pricing page is rendered with JS, and the content seems to be inside a data structure. The fact that the byte size is identical suggests it's the same SPA page. Let's look for where the actual pricing data lives — maybe it's in the JSON files being fetched, or embedded as escaped JSON.

Let me grep for the "Per host" string, or find a JSON endpoint.

Actually, this page is probably loading data from an API. Let me search for URLs within the script, or look for text like "Infrastructure" near prices in the raw HTML (possibly with escaped unicode).

Alternative: Datadog has a public pricing API that requires authentication. There are also documentation pages where prices are listed in static HTML: https://docs.datadoghq.com/account_management/billing/ — these are static markdown-rendered pages. Let's try fetching these. They're more reliable and authoritative about how billing works.

Let me try:

Actually, the real list of products is on the pricing SPA. But the docs billing pages are static. Let's fetch several of them.

First, let's confirm what the pricing SPA loads — grep for fetch/JSON paths.

Pricing page is a JS SPA (identical bytes for all products). Let me find its data source and also check the static billing docs.

cd /tmp && python3 - <<'EOF'
import re
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
scr=''.join(re.findall(r'<script[^>]*>(.*?)</script>',t,flags=re.S))
for m in set(re.findall(r'https?://[^"\'\\ ]{10,120}',scr))|set(re.findall(r'"/[^"]*\.json[^"]*"',t)):
    print(m)
EOF
echo "=== docs billing ==="
timeout 25 curl -s -A "Mozilla/5.0" -o bill.html -w "%{http_code} %{url_effective}\n" -L https://docs.datadoghq.com/account_management/billing/; wc -c bill.html
https://www.datadoghq.com/it/dg/webinars/uxdem-italy-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-em
https://www.datadoghq.com/it/dg/webinars/uxdem-italy-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-em
https://www.redditstatic.com/ads/pixel.js
https://www.datadoghq.com/pt/dg/webinars/uxdem-pt-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-latam
https://www.datadoghq.com/legal/privacy/
https://px.ads.linkedin.com/collect/?pid=74631&conversionId=1516249&fmt=gif
https://www.datadoghq.com/fr/dg/webinars/uxdem-france-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-e
https://www.datadoghq.com/locate
https://www.datadoghq.com/es/dg/webinars/uxdem-spain-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-em
https://lp.datadoghq.com/js/forms2/js/forms2.min.js
https://corpsite-staging.datadoghq.com/locate
https://corp.dd-static.net/
https://www.datadoghq.com/legal/cookies/
https://www.datadoghq.com/
https://www.datadoghq.com/es/dg/webinars/uxdem-es-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-latam
https://www.datadoghq.com/ko/dg/webinars/infra-korea-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-infra-
https://www.datadoghq.com/ja/dg/webinars/uxdem-japan-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-ja
https://www.datadoghq.com/event/datadog-detect-q425/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-security-ww-
https://www.datadoghq.com/de/dg/webinars/uxdem-dach-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-eme
https://www.datadoghq.com/dg/webinars/uxdem-anz-2025/?utm_source=inbound&utm_medium=corpsite-display&utm_campaign=dg-dem-apac-li
=== docs billing ===
200 https://docs.datadoghq.com/account_management/billing/
97348 bill.html
Thinking…

The billing docs are static and accessible. Let's extract the actual billing mechanisms from there, and additionally fetch the pricing details pages for the main products. I'll parse bill.html and also try the per-product pricing detail pages under docs (e.g., https://docs.datadoghq.com/account_management/billing/pricing/?product=...). Actually, docs has an integrated "Pricing" page: https://docs.datadoghq.com/account_management/billing/pricing/

Let's extract the text from bill.html and check the billing plan table.

Also, regarding infrastructure host pricing, there's https://docs.datadoghq.com/infrastructure/ and billing/usage_method. The authoritative numbers are in the pricing SPA, but is that inaccessible without JS execution? Hmm. But maybe the SPA content actually exists within the HTML, just heavily obfuscated/escaped. Identical byte counts across ?product= suggest the same document, and the product content might exist inline. Let's search the raw HTML for strings like "Per Host" or "Infrastructure".

Let's do both: parse bill.html into text, and grep ddq.html for "Infrastructure".

Billing docs are static. Let me extract the billing mechanics and check whether product prices are inline in the SPA.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('bill.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style|nav|header|footer).*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
i=txt.lower().find('billing')
print(txt[i:i+2600])
EOF
No output

cd /tmp && python3 -c "
import re
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
for k in ['Infrastructure','Per Host','per host','Log Management','APM']:
    print(k, t.count(k))
"
Infrastructure 102
Infrastructure 102
Per Host 0
per host 34
Log Management 19
APM 409
Thinking…

The billing text extraction didn't output anything (maybe 'billing' wasn't found in the lowercased text, or it was empty). I'll retry with a different approach.

Also, in the pricing SPA's HTML, "per host" appears 34 times — meaning the pricing data is embedded inline. Let me try extracting it.

Pricing data IS inline ("per host" ×34). Let me extract it properly.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t2=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>','|',t2)))
txt=re.sub(r'\|+','|',txt)
for kw in ['per host','Per Host']:
    for m in list(re.finditer(kw,txt))[:14]:
        print(repr(txt[max(0,m.start()-260):m.end()+90]));print('-'*90)
EOF
'tion capabilities|Includes all features in Infrastructure Enterprise and Cloud Security Management (CSM) Enterprise|File integrity monitoring|Workload Protecti
'tion capabilities|Includes all features in Infrastructure Enterprise and Cloud Security Management (CSM) Enterprise|File integrity monitoring|Workload Protection for Linux, Windows Kubernetes, and Docker|Increased container allotment (10 containers |allotted| per host license**)|*Billed annually or $|41| on-demand|Start Free Trial|Multi-Year/Volume discoun'
------------------------------------------------------------------------------------------
' version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Full-Resolution Data Retention|1 day|15 months|15 months Customizable|Alerts|Unlimited|Unlimited|Container Monitoring|5 per host allotted|10 per host allotted|Custom Metrics|100 per host allotted|200 per host allotted|'
------------------------------------------------------------------------------------------
'"http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Full-Resolution Data Retention|1 day|15 months|15 months Customizable|Alerts|Unlimited|Unlimited|Container Monitoring|5 per host allotted|10 per host allotted|Custom Metrics|100 per host allotted|200 per host allotted|Custom Events|500 per'
------------------------------------------------------------------------------------------
'ink="http://www.w3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Full-Resolution Data Retention|1 day|15 months|15 months Customizable|Alerts|Unlimited|Unlimited|Container Monitoring|5 per host allotted|10 per host allotted|Custom Metrics|100 per host allotted|200 per host allotted|Custom Events|500 per host allotted|1000 per host allotted'
------------------------------------------------------------------------------------------
'/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Full-Resolution Data Retention|1 day|15 months|15 months Customizable|Alerts|Unlimited|Unlimited|Container Monitoring|5 per host allotted|10 per host allotted|Custom Metrics|100 per host allotted|200 per host allotted|Custom Events|500 per host allotted|1000 per host allotted|Single Sign-On with S'
------------------------------------------------------------------------------------------
'ed with Sketch.| | | | |Full-Resolution Data Retention|1 day|15 months|15 months Customizable|Alerts|Unlimited|Unlimited|Container Monitoring|5 per host allotted|10 per host allotted|Custom Metrics|100 per host allotted|200 per host allotted|Custom Events|500 per host allotted|1000 per host allotted|Single Sign-On with SAML|\' width="25px" height="25px" vie'
------------------------------------------------------------------------------------------
'|Full-Resolution Data Retention|1 day|15 months|15 months Customizable|Alerts|Unlimited|Unlimited|Container Monitoring|5 per host allotted|10 per host allotted|Custom Metrics|100 per host allotted|200 per host allotted|Custom Events|500 per host allotted|1000 per host allotted|Single Sign-On with SAML|\' width="25px" height="25px" viewBox="0 0 48 48" versio'
------------------------------------------------------------------------------------------
'w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Workflows|Free when triggered from security findings and signals|Free when triggered from security findings and signals|Containers|5 containers allotted per host per month|10 containers allotted per host per month|Support Plans & Post Sales Services|F'
------------------------------------------------------------------------------------------
'3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Workflows|Free when triggered from security findings and signals|Free when triggered from security findings and signals|Containers|5 containers allotted per host per month|10 containers allotted per host per month|Support Plans & Post Sales Services|From basic plans to bespoke offerings, Data'
------------------------------------------------------------------------------------------
'xample uses: monitoring ad revenue, the rate at which shopping carts are abandoned, or the health of an in-game economy. Many customers find these metrics invaluable, especially when combined with infrastructure metrics.|How many custom metrics are |allotted| per host?|Datadog offers 100 custom metrics for every host monitored with Infrastructure Pro, or 2'
------------------------------------------------------------------------------------------
'eduled maintenance jobs. This way, you can correlate any of these events with performance metrics, create monitors for alerting and enrich events at intake with processing pipelines to be queried alongside other standard events.|How many events are |allotted| per host?|Datadog offers 500 custom events for every host monitored with infrastructure monitoring'
------------------------------------------------------------------------------------------
'|\' width="25px" height="25px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Data Streams Monitoring is also available standalone starting at $|15| per host|and |Continuous Profiler is available standalone starting at $|19| per host.|APM is also '
------------------------------------------------------------------------------------------
'//www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | |Icon/check | |Created with Sketch.| | | | |Data Streams Monitoring is also available standalone starting at $|15| per host|and |Continuous Profiler is available standalone starting at $|19| per host.|APM is also available for Edge Devices, at a reduced price per device with lower ingest '
------------------------------------------------------------------------------------------
' Monitoring to use APM, APM Pro, or APM Enterprise?|No. APM|, APM Pro,| and APM Enterprise can be purchased standalone and do not require Infrastructure Monitoring. Standalone pricing is $|36| for APM|, $|41| for APM Pro,| and $|47| for APM Enterprise, priced per host, per month, billed annually and available on-demand.|Is Pro or Enterprise Infrastructure '
------------------------------------------------------------------------------------------
Thinking…

Great, the pricing info is inline. Let's extract the main product prices in a structured way. I'll try searching for the "Starting At $ X" pattern near the product names.

Let me write a targeted extraction for the core products (Infrastructure, APM, Log Management, Dashboards, Monitors, API).

Let's find the "Starting At" blocks.

Now let me extract structured prices for the core products.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\|+','|',re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>','|',t))))
seen=set()
for m in re.finditer(r'Starting At\|?\s*\$?\|?\s*([\d,.]+)\|([^|]{0,60})',txt):
    key=(m.group(1),m.group(2)[:40])
    ctx=txt[max(0,m.start()-320):m.start()]
    # find nearest preceding product/plan heading
    heads=re.findall(r'\|([A-Z][A-Za-z0-9 &/\+\-\.]{2,40})\|',ctx)
    prod=heads[-1] if heads else '?'
    if key in seen: continue
    seen.add(key)
    print(f"${m.group(1):<8} per:{m.group(2)[:44]:<46} plan~{prod}")
EOF
$2 per:% of monthly spend* plan~?
$2        per:% of monthly spend*                            plan~?
Thinking…

The reason the regex isn't matching well is because the pipe positions aren't consistent. Let me take a different approach and search for the context of "$" amounts and product names. Let me search for known product names and grab the text nearby.

Let me find the "Infrastructure" section and try extracting around it.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
prods=['Infrastructure Pro','Infrastructure Enterprise','Log Management','APM','APM Pro','Synthetic','Dashboards','Custom Metrics','Database Monitoring']
for p in prods:
    idx=txt.find(p)
    if idx<0: print(f"[{p}] not found"); continue
    seg=txt[idx:idx+420]
    pr=re.findall(r'\$\s?([\d,.]+)',seg)
    per=re.findall(r'(per [a-z0-9,/\s]{2,28})',seg)
    print(f"[{p}] prices={pr[:4]} units={per[:3]}")
    print('   ',seg[:190].strip())
    print('-'*95)
EOF
[Infrastructure Pro] prices=['27'] units=[]
[Infrastructure Pro] prices=['27'] units=[]
    Infrastructure Pro and Cloud Security Management (CSM) Pro Cloud Security Posture Management (CSPM) Kubernetes Security Posture Management (KSPM) Built-in mapping of your resources to compli
-----------------------------------------------------------------------------------------------
[Infrastructure Enterprise] prices=['41'] units=['per host license']
    Infrastructure Enterprise and Cloud Security Management (CSM) Enterprise File integrity monitoring Workload Protection for Linux, Windows Kubernetes, and Docker Increased container allotment
-----------------------------------------------------------------------------------------------
[Log Management] prices=[] units=[]
    Log Management Audit Trail Observability Pipelines Error Tracking Bring Your Own Cloud Log Management Sensitive Data Scanner ) 0%, %!s( ) 100%)"> Infrastructure ) 0%, %!s( ) 100%)"> Applicat
-----------------------------------------------------------------------------------------------
[APM] prices=[] units=[]
    APM Ingest Serverless Monitoring Universal Service Monitoring This product is currently unavailable on the US-FED site. Data Streams Monitoring This product is currently unavailable on the U
-----------------------------------------------------------------------------------------------
[APM Pro] prices=['35', '54'] units=['per month']
    APM Pro Starting At $ 35 Per host, per month* Track and improve the performance of your streaming data pipelines end to end Everything included in APM, plus Data Streams Monitoring: Automati
-----------------------------------------------------------------------------------------------
[Synthetic] prices=[] units=[]
    Synthetic Monitoring Mobile App Testing Continuous Testing Error Tracking Dashboards Software Delivery Build, test, secure and ship quality code faster Software Delivery Continuous Testing I
-----------------------------------------------------------------------------------------------
[Dashboards] prices=[] units=[]
    Dashboards Software Delivery Build, test, secure and ship quality code faster Software Delivery Continuous Testing Internal Developer Portal (IDP) CI Visibility Test Optimization Feature Fla
-----------------------------------------------------------------------------------------------
[Custom Metrics] prices=[] units=[]
    Custom Metrics Network Monitoring Container Monitoring Kubernetes Autoscaling Serverless Monitoring Cloud Cost Management Storage Management GPU Monitoring Applications Application Performan
-----------------------------------------------------------------------------------------------
[Database Monitoring] prices=[] units=[]
    Database Monitoring Data Observability Data Streams Monitoring Logs Log Management Audit Trail Observability Pipelines Error Tracking Bring Your Own Cloud Log Management Sensitive Data Scann
-----------------------------------------------------------------------------------------------
Thinking…

Confirmed the actual prices:

  • Infrastructure Pro: $27 (found near the Infrastructure Pro section, "Billed annually or $X on-demand")
  • Infrastructure Enterprise: $41 (per host license, billed annually)
  • APM Pro: $35 per host/month (that was actually in the Data Streams context) — hmm, the APM Pro snippet says "Track and improve the performance of your streaming data pipelines", which is the Data Streams Monitoring context. Let's proceed carefully.

From the earlier extraction results:

  • "Data Streams Monitoring is also available standalone starting at $15 per host, and Continuous Profiler is available standalone starting at $19 per host"
  • "Standalone pricing is $36 for APM, $41 for APM Pro, and $47 for APM Enterprise, priced per host, per month, billed annually and available on-demand"
  • Cloud Cost Management: Pro $5 per $1,000 of cloud/SaaS spend (billed annually, $7.20 on-demand), Enterprise $10

Let's obtain the Infrastructure Pro/Enterprise annual-billing numbers and the log ingestion prices more accurately, and also get the custom metrics pricing. Let's target the "Billed annually or $" pattern and the Infrastructure section.

Let's search for "Per host, per month" near Infrastructure.

Honestly, for the response we have enough info:

  • Infrastructure: Pro ~$15/host/month (billed annually), Enterprise ~$23/host/month… wait, historically it was Pro $15, Enterprise $23. But the page shows $27 and $41? Hmm — "$27" appeared after "Infrastructure Pro and CSM Pro" — this might be the CSM bundle price. And "$41" after Infrastructure Enterprise…

Historically, Datadog's Infrastructure pricing was Pro = $15/host/month (billed annually), $18 on-demand, Enterprise = $23/host/month (billed annually), $27 on-demand. So $27 = Enterprise on-demand. Which means $41 might be something else. Hmm.

Prices have changed over time. Let's extract more carefully in the context of "on-demand".

To find the pricing cards, let's look for all occurrences of "$" preceded by a product plan name within 200 characters.

Let's do this systematically: find all "Billed annually or $ X on-demand" and grab the surrounding ~500 characters preceding them.

Getting closer. Let me nail down the Infrastructure/APM/Logs numbers precisely via the "on-demand" pricing markers.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
for m in re.finditer(r'Billed annually or \$\s?([\d,.]+)\s?on-demand',txt):
    pre=txt[max(0,m.start()-700):m.start()]
    names=re.findall(r'(Infrastructure (?:Pro|Enterprise)|APM (?:Pro|Enterprise)?|Cloud Cost Management|CSM (?:Pro|Enterprise)|Cloud Security Management (?:Pro|Enterprise)?|Continuous Profiler|Data Streams Monitoring|Network Performance Monitoring|Database Monitoring|Serverless Monitoring|Container Monitoring|Log Management|Cloud SIEM|Sensitive Data Scanner)',pre)
    # nearest "Starting At $ N Per <unit>"
    cards=re.findall(r'Starting At \$\s?([\d,.]+)\s?(Per [^A-Z]{0,60})',pre)
    print(f"on-demand ${m.group(1):<7} | names={names[-2:] if names else '?'} | card={cards[-1] if cards else '?'}")
EOF
on-demand $12 | names=? | card=('8', 'Per committer, per month* ')
on-demand $12      | names=? | card=('8', 'Per committer, per month* ')
on-demand $0.10    | names=? | card=('0.10', 'Per ingested or scanned ')
on-demand $2.55    | names=? | card=?
on-demand $0.10    | names=? | card=('0.10', 'Per ingested or scanned ')
on-demand $0.075   | names=? | card=('0.05', 'Per million events stored, per month* ** ')
on-demand $0.90    | names=? | card=('0.60', 'Per million events stored, per month* ** ')
on-demand $100     | names=? | card=?
on-demand $18      | names=? | card=('15', 'Per host, per month* ')
on-demand $27      | names=? | card=('23', 'Per host, per month* ')
on-demand $27      | names=['Infrastructure Pro', 'Cloud Security Management '] | card=('22', 'Per host, per month* ')
on-demand $41      | names=['Infrastructure Enterprise', 'Cloud Security Management '] | card=('34', 'Per host, per month* ')
on-demand $18      | names=? | card=('15', 'Per host, per month* ')
on-demand $12      | names=? | card=('8', 'Per committer, per month* ')
on-demand $7.20    | names=? | card=('5', 'Per 10')
on-demand $18      | names=? | card=('12', 'Per thousand test runs, per month* ')
on-demand $72      | names=? | card=('50', 'Per hundred test runs, per month* ')
on-demand $114     | names=? | card=('79', 'Per additional parallelization, per month* ')
on-demand $29      | names=? | card=('20', 'Per committer, per month* ')
on-demand $51      | names=? | card=?
on-demand $29      | names=? | card=('20', 'Per seat, per month* ')
on-demand $43.20   | names=? | card=('30', 'Per seat, per month* ')
on-demand $58      | names=? | card=('40', 'Per seat, per month* ')
on-demand $0.14    | names=? | card=('0.10', 'Per event evaluated by a correlation pattern (')
on-demand $14      | names=? | card=('10', 'Per 100 ')
on-demand $36      | names=['APM '] | card=?
on-demand $1.20    | names=? | card=('0.80', 'Per 1,000 sessions, per month on full traffic* ')
on-demand $3.60    | names=? | card=('2.50', 'Per 1,000 sessions, per month* ')
on-demand $0.50    | names=? | card=('0.35', 'Per 1')
on-demand $1.20    | names=? | card=?
on-demand $7.20    | names=? | card=('5', 'Per host, per month* ')
on-demand $4.32    | names=? | card=('3', 'Per autoscaled v')
on-demand $575     | names=? | card=?
on-demand $4.32    | names=? | card=('3', 'Per active application instance* ')
on-demand $9       | names=['APM '] | card=('6', 'Per actively traced application instance* per month** ')
on-demand $7.20    | names=? | card=('5', 'Per active* step function, per month** ')
on-demand $4.32    | names=? | card=('3', 'Per active application instance* per month** ')
on-demand $9       | names=['APM '] | card=('6', 'Per active* application instance, per month** ')
on-demand $4.32    | names=? | card=('3', 'Per active application instance* per month** ')
on-demand $9       | names=['APM '] | card=('6', 'Per actively traced application instance* per month** ')
on-demand $4.32    | names=? | card=('3', 'Per active application instance* per month** ')
on-demand $9       | names=['APM '] | card=('6', 'Per actively traced application instance* per month** ')
on-demand $4.32    | names=? | card=('3', 'Per active application instance* per month** ')
on-demand $9       | names=['APM '] | card=('6', 'Per actively traced application instance* per month** ')
on-demand $4.32    | names=? | card=('3', 'Per active application instance* per month** ')
on-demand $9       | names=['APM '] | card=('6', 'Per actively traced application instance* per month** ')
on-demand $15      | names=['APM '] | card=('10', 'Per million state transitions*, per month** ')
on-demand $7.20    | names=? | card=('5', 'Per active* function, per month** ')
on-demand $15      | names=['APM '] | card=('10', 'Per million traced invocations*, per month** ')
on-demand $15      | names=? | card=('10', 'Per million traced invocations*, per month** ')
on-demand $0.22    | names=? | card=('0.15', 'Per 1,000 sessions, per month* on full traffic ')
on-demand $4.50    | names=? | card=('3', 'Per 1,000 sessions, per month* on filtered sessions ')
on-demand $3.60    | names=? | card=('2.50', 'Per 1,000 sessions, per month* ')
on-demand $18      | names=? | card=('12', 'Per thousand test runs, per month* ')
on-demand $72      | names=? | card=('50', 'Per hundred test runs, per month* ')
on-demand $7.20    | names=? | card=('5', 'Per 10')
on-demand $7.20    | names=? | card=('5', 'Per 1,000 tests, per month* ')
on-demand $114     | names=? | card=('79', 'Per additional parallelization, per month* ')
on-demand $7.20    | names=? | card=('5', 'Per host, per month* ')
on-demand $10.20   | names=? | card=('7', 'Per device, per month* ')
on-demand $5.76    | names=? | card=('4', 'Per device, per month* ')
on-demand $0.94    | names=? | card=?
on-demand $7.20    | names=? | card=('5', 'Per 1,000 tests, per month* ')
on-demand $0.045   | names=['Sensitive Data Scanner'] | card=?
on-demand $0.45    | names=['Sensitive Data Scanner', 'Sensitive Data Scanner'] | card=('0.30', 'Per scanned ')
on-demand $13      | names=? | card=('9', 'Per ')
on-demand $36      | names=? | card=('31', 'Per host, per month* ')
on-demand $12      | names=['CSM Pro'] | card=('10', 'Per host, per month* ')
on-demand $30      | names=['CSM Enterprise', 'CSM Pro'] | card=('25', 'Per host, per month* ')
on-demand $18      | names=['APM Pro', 'APM Enterprise'] | card=('15', 'Per client host, per month* ')
on-demand $120     | names=['APM Pro', 'APM Enterprise'] | card=('100', 'Per broker host, per month* ')
on-demand $7.20    | names=? | card=('5', 'Per $1,000 in cloud/')
on-demand $15      | names=? | card=('10', 'Per $1,000 in cloud/')
on-demand $84      | names=? | card=('70', 'Per database host, per month* ')
on-demand $0.12    | names=? | card=('0.095', 'Per ingested ')
on-demand $36      | names=? | card=('31', 'Per host, per month* ')
on-demand $0.072   | names=? | card=('0.05', 'Per host, per hour* ')
on-demand $0.144   | names=? | card=('0.10', 'Per job, per hour* ')
on-demand $24      | names=? | card=('16', 'Per ')
on-demand $36      | names=? | card=('25', 'Per committer, per month* ')
on-demand $36      | names=? | card=('25', 'Per committer, per month* ')
on-demand $22      | names=? | card=('15', 'Per committer, per month* ')
on-demand $22      | names=? | card=('15', 'Per committer, per month* ')
on-demand $29      | names=? | card=('20', 'Per host, per month* ')
on-demand $15      | names=? | card=('10', 'Per host, per month* ')
on-demand $57.60   | names=? | card=('40', 'Per committer, per month* ')
Thinking…

Now I've got solid, real pricing data lined up. Confirmed key data points:

Infrastructure:

  • Pro: $15 per host/month billed annually, $18 on-demand
  • Enterprise: $23 per host/month billed annually, $27 on-demand
  • (Bundle with CSM: Pro $22/$27, Enterprise $34/$41)
  • Free tier available (Infrastructure free is $0, 1-day retention)

APM:

  • APM: $31/host/month (annual), $36 on-demand
  • APM Pro: $35 (from earlier snippet — actually the APM Pro card near Data Streams showed $35… and there was a $36 one too — let's not overstate. Earlier text: "Standalone pricing is $36 for APM, $41 for APM Pro, and $47 for APM Enterprise, priced per host, per month, billed annually and available on-demand" — hmm, this contradicts $31. This citation might be about standalone purchase without Infrastructure. Let's verify this specific sentence precisely.)

Log Management: ingestion $0.10 per GB ingested ($0.10 on-demand)… "$0.10 Per ingested or scanned" — this might be Log Management's $0.10/GB ingestion, $1.70 per million indexed… let's check the Log Management section separately.

Events: $0.05–0.60 per million events stored (Audit Trail?)

Custom metrics: need to check.

Cloud Cost Management: $5 per $1,000 spend (annual), $7.20 on-demand (Pro); $10/$15 Enterprise.

Synthetic: $5 per 1,000 tests/month (annual), $7.20 on-demand.

RUM: $1.50 per 1,000 sessions (annual), $2.50 on-demand? Actually the card=('2.50','Per 1,000 sessions') with on-demand $3.60 → annual $2.50, on-demand $3.60.

Database Monitoring: $70 per DB host/month (annual), $84 on-demand.

Cloud SIEM: $9 per GB? Earlier: card=('9','Per ') on-demand $13 → $9/GB ingested.

OK. I have enough info to give an accurate pricing overview. Rather than relying on my heuristic "names" matching, let's verify a few key ones (Log Management, Custom Metrics, Monitors/Events free) with targeted extractions.

Let's extract the Log Management and Custom Metrics sections.

Also need to verify the APM standalone sentence.

After that, build an example.

OK, plan for the example:

  • Datadog has a clear developer workflow: REST API + official SDKs (Python's datadog-api-client, Node's @datadog/datadog-api-client), plus dd-trace, dogstatsd.
  • But: making actual calls requires DD_API_KEY and DD_APP_KEY. I don't have these. That's the blocker.
  • Can I do something without credentials? Yes: install the SDK, write a working sample, and verify that it runs end-to-end against a mocked HTTP transport (no local service stack needed — just intercept the SDK's HTTP layer in-process). This lets me prove the code is correct without touching Datadog or spinning up services.

This is the right balance: stay light, use the hosted product via the SDK, don't spin up a stack, and honestly report the credentials blocker.

What to build:

  1. Install datadog-api-client (Python) — quick pip install.
  2. submit_metrics.py — a small DogStatsD-style / API v2 metrics submission sample + a query sample.
  3. Make it runnable in two modes: real mode (requires DD_API_KEY/DD_APP_KEY) and --dry-run/mock mode that verifies against an in-process fake so we can prove it works here.

First let's verify the remaining pricing details, then install the SDK in parallel.

Actually, let's do the pricing verification and pip install simultaneously (they're independent).

Solid real data. Let me verify Logs/Custom Metrics specifics and install the SDK in parallel.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
for kw in ['Standalone pricing is','per GB ingested','Custom Metrics Starting','Per million custom metrics','ingestion','indexed']:
    i=txt.find(kw)
    if i<0: print(f"--[{kw}] not found"); continue
    print(f"--[{kw}]"); print('   ',txt[max(0,i-200):i+330].strip()); print()
EOF
--[Standalone pricing is]
--[Standalone pricing is]
    ed on them. Do I need Infrastructure Monitoring to use APM, APM Pro, or APM Enterprise? No. APM , APM Pro, and APM Enterprise can be purchased standalone and do not require Infrastructure Monitoring. Standalone pricing is $ 36 for APM , $ 41 for APM Pro, and $ 47 for APM Enterprise, priced per host, per month, billed annually and available on-demand. Is Pro or Enterprise Infrastructure Monitoring required on every APM Host? Only if you purchase APM using the Infrastructure Monitoring pricing model. In this case, every host r

--[per GB ingested]
    ehavior: 'smooth'}); }, 200); } }"> Common Questions What indexed span and ingested span allotments apply under the new pricing? There are no allotments. Pricing is purely consumption-based — you pay per GB ingested and per million indexed spans, with no included quantity. When do I need a managed platform specific endpoint? We use managed platform-specific endpoints to identify when managed platform billing should apply, so it is important to use the endpoint that matches your actual architecture. If you are on a managed pl

--[Custom Metrics Starting] not found
--[Per million custom metrics] not found
--[ingestion]
    APM with containers? Yes, you can run APM in a containerized environment. The number of containers running on each host (node) does not impact the APM host-based price. What is the difference between ingestion and retention? Ingestion means sending your traces to Datadog and having all of them available for Live Search and Analytics for 15 minutes. Retention means storing your most important traces (e.g. error, high latency, or business-critical ones) and making them available for search and analysis during a retention perio

--[indexed]
    archive for the time period you requested, and then indexes only log events that match your rehydration query. Datadog charges $ 0.10 per compressed GB of log data that is scanned. For any log events indexed from a rehydration, the cost is equal to your contracted indexing rates for the retention period selected for the rehydration. How are log-based metrics billed? Log-based metrics count towards your Custom Metrics allocation as outlined in your infrastructure plan . Are there other billing options? Annual and monthly plan

cd /tmp && timeout 240 pip install --quiet datadog-api-client 2>&1 | tail -5; python3 -c "import datadog_api_client as d; print('datadog-api-client', d.__version__ if hasattr(d,'__version__') else 'ok')" 2>&1 | tail -3
datadog-api-client ok
datadog-api-client ok
Thinking…

SDK installation succeeded. Next, let's specifically grab the Log Management pricing (ingest per GB + per-million index pricing). I'll search for the "Log Management" price card.

Let me try extracting the Log Management section.

SDK installed. Let me pin down Log Management pricing, then build the example.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
# find log-related price cards
for m in re.finditer(r'Starting At \$\s?([\d,.]+)\s?(Per (?:million |billion )?(?:log|events|GB)[^.]{0,70})',txt):
    print('$'+m.group(1),'|',m.group(2)[:80])
print('---- log ingest mentions ----')
for m in list(re.finditer(r'\$\s?0\.10[^.]{0,90}',txt))[:6]:
    print('  ',m.group(0)[:110])
print('---- custom metrics ----')
for m in list(re.finditer(r'[Cc]ustom [Mm]etrics[^.]{0,150}',txt))[:8]:
    print('  *',m.group(0)[:170])
EOF
$0.05 | Per million events stored, per month* ** For historical investigations or securi
$0.05 | Per million events stored, per month* ** For historical investigations or securi
$0.60 | Per million events stored, per month* ** For historical investigations and long-
$0.25 | Per GB outbound, per destination, per month* Centralize your log processing,
$5 | Per GB analyzed per month* Start Free Trial More than 800, and growing, out-
$0.05 | Per GB scanned * ** Start Free Trial *No minimum scan size
---- log ingest mentions ----
   $ 0.10 Per ingested or scanned GB, per month* ** Ingest, process, enrich, live tail, and archive
   $ 0.10 on-demand Start Free Trial Standard Indexing 15-day retention 3-day retention 7-day reten
   $ 0.10 Per ingested or scanned GB, per month* ** Ingest, process, enrich, live tail, and archive
   $ 0.10 on-demand Start Free Trial Flex Storage Starting At $ 0
   $ 0.10 per ingested GB
   $ 0.10 per compressed GB of log data that is scanned
---- custom metrics ----
  * Custom Metrics Network Monitoring Container Monitoring Kubernetes Autoscaling Serverless Monitoring Cloud Cost Management Storage Management GPU Monitoring Applicat
  * Custom Metrics Dashboards Notebooks Mobile App Fleet Automation Access Control Governance Console DORA Metrics + AI Impact Incident Management Work Management Workf
  * Custom Metrics Network Monitoring Container Monitoring Kubernetes Autoscaling Serverless Monitoring Cloud Cost Management Storage Management GPU Monitoring Applicat
  * Custom Metrics Dashboards Notebooks Mobile App Fleet Automation Access Control Governance Console DORA Metrics + AI Impact Incident Management Work Management Workf
  * custom metrics Archive to AWS S3, Azure Blob Storage, and Google Cloud Storage Log Rehydration™ from AWS S3, Azure Blob Storage, and Google Cloud Storage When Combi
  * Custom Metrics allocation as outlined in your infrastructure plan 
  * Custom Metrics 100 per host allotted 200 per host allotted Custom Events 500 per host allotted 1000 per host allotted Single Sign-On with SAML ' width="25px" height
  * custom metrics are allotted per host? Datadog offers 100 custom metrics for every host monitored with Infrastructure Pro, or 200 custom metrics for every host monit
Thinking…

Now I have solid pricing data lined up. Let me record the main facts:

Log Management:

  • Ingest: $0.10/GB ingested or scanned per month
  • Indexing: varies by retention period (15-day, 3-day, 7-day...). Let me grab the per-million indexing rates. I'll search for "Per million log events".

Custom Metrics: 100 per host granted (Infra Pro), 200 per host (Enterprise); beyond that, billed per million.

Let me quickly grab the indexing rates, then move on to building. Actually, at this level of detail I have enough — I don't need every single number. That said, indexing is the biggest cost driver for logs, so let me grab those numbers.

Let me do one quick extraction, then build out an example.

Let me grab log indexing rates (the main cost driver), then build.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
for m in re.finditer(r'Starting At \$\s?([\d,.]+)\s?(Per (?:million|billion)[^.]{0,80})',txt):
    print('$'+m.group(1),'|',m.group(2)[:85])
i=txt.find('Standard Indexing')
print('\n[Standard Indexing ctx]\n',txt[i:i+600])
EOF
$0.05 | Per million events stored, per month* ** For historical investigations or security, a
$0.05 | Per million events stored, per month* ** For historical investigations or security, a
$0.60 | Per million events stored, per month* ** For historical investigations and long-term 
$10 | Per million state transitions*, per month** Tracing for your workflows Visualize the 
$10 | Per million traced invocations*, per month** Add deeper visibility into your active f
$10 | Per million traced invocations*, per month** Comprehensive threat detection Detect at

[Standard Indexing ctx]
 Standard Indexing 15-day retention 3-day retention 7-day retention 15-day retention 30-day retention Greater than 30 days $ 1.70 Per million log events, per month* Contact Us for Pricing For real-time exploration, alerting, and dashboards with mission-critical logs Prioritize logs for critical investigations Build log alerts with Log Monitors Detect log anomalies proactively with Watchdog Insights Best suited for frequently accessed logs i.e. infrastructure, application, operational logs *Billed annually or $ 2.55 on-demand Start Free Trial Let's Talk Ingest Starting At $ 0.10 Per ingested or 
Thinking…

Now I have all the data I need:

Log Management:

  • Ingest: $0.10/GB per month (annual contract) — on-demand also $0.10
  • Standard indexing 15-day retention: $1.70 per 1M log events per month (annual), $2.55 on-demand
  • Flex storage / rehydration scan: $0.05/GB scanned, $0.10/GB compressed scanned
  • Observability pipelines: $0.25/GB outbound per destination

Infrastructure: Pro $15/host/month (annual) / $18 on-demand; Enterprise $23 / $27 on-demand. Free tier is $0 (1-day retention, 5 containers, 100 custom metrics).

Custom metrics: 100 per host (Pro), 200 per host (Enterprise); additional beyond that billed per 1M (let me quickly confirm the rate — commonly $5/million). I saw "$5 Per GB analyzed" for Network Performance Monitoring. I'll leave the custom metrics rate as "billed additionally per 1M" — no, let me verify it properly. Not critical; I'll list the allocation which is the key mechanism.

APM: When purchased alongside Infra, $31/host/month (annual) / $36 on-demand; standalone $36 (APM), $41 (Pro), $47 (Enterprise). Additionally pure consumption-based: $ per GB ingest + $ per 1M indexed spans.

Cloud Cost Management: Pro $5 per $1,000 spend (annual), $7.20 on-demand; Enterprise $10/$15.

Synthetic: $5 per 1,000 tests/month (annual), $7.20 on-demand.

RUM: $2.50 per 1,000 sessions (annual), $3.60 on-demand (mobile is $3/$4.32? Actually $1.50/$2.20). I'll skip the details.

Database Monitoring: $70/DB host/month (annual), $84 on-demand.

Cloud SIEM: $9/GB ingest (annual), $13 on-demand.

Sensitive Data Scanner: $0.30/scanned GB (annual), $0.45 on-demand.

Now let's build an example. Blocker: no DD_API_KEY / DD_APP_KEY.

Approach: create a small Python project:

  • requirements.txt
  • datadog_example.py — send custom metrics via API v2 and query them back, plus list monitors. Real mode when credentials exist.
  • In-process mock mode for validation (--mock). This uses the SDK's own configuration to point at a local fake… no, "don't spin up a local service stack" — a mock inside the process is fine (no server process). We can monkey-patch the SDK's REST client.

Better approach: use the SDK with Configuration and inject a fake transport. datadog-api-client uses urllib3 via RESTClientObject. We can subclass/patch datadog_api_client.rest.RESTClientObject.request to return canned responses. This is fully in-process, no stack, fast.

This lets me prove the sample code paths (serialization, payload shape, response parsing) work without credentials or network.

What to write:

  1. README.md — explain pricing + how to run.
  2. requirements.txt
  3. dd_example.py — main sample, with --mock and real mode.
  4. mock_transport.py — in-process fake API (or inline it).

Keep it compact. Let me check the SDK's API surface for v2 metrics submission and v1 query.

What to check: datadog_api_client.v2.api.metrics_api.MetricsApi.submit_metrics, and the MetricPayload model. Also v1's DashboardsApi. Let me introspect the installed package so I write correct code (avoid hallucinations).

SDK installed and I have real pricing data. Let me introspect the SDK so the example is accurate, not hallucinated.

cd /tmp && python3 - <<'EOF'
import datadog_api_client as d, pkgutil, importlib
print("ver:", getattr(d,'__version__','?'))
from datadog_api_client import Configuration, ApiClient
from datadog_api_client.v2.api import metrics_api
print("MetricsApi methods:", [m for m in dir(metrics_api.MetricsApi) if not m.startswith('_') and m.endswith(('metrics','series','tags'))][:12])
from datadog_api_client.v2 import model as m2
print("has MetricPayload:", hasattr(m2,'metric_payload'))
from datadog_api_client.v2.model.metric_payload import MetricPayload
from datadog_api_client.v2.model.metric_series import MetricSeries
import inspect
print("MetricSeries attrs:", [a for a in MetricSeries.openapi_types.keys()])
print("MetricPayload attrs:", list(MetricPayload.openapi_types.keys()))
EOF
ver: ?
ver: ?
MetricsApi methods: ['estimate_metrics_output_series', 'submit_metrics']
has MetricPayload: True
MetricSeries attrs: ['interval', 'metadata', 'metric', 'points', 'resources', 'source_type_name', 'tags', 'type', 'unit']
MetricPayload attrs: ['series']
cd /tmp && python3 - <<'EOF'
from datadog_api_client.v2.api import dashboards_api
from datadog_api_client.v1.api import monitors_api
from datadog_api_client.v2.api import usage_metering_api
print("Dashboards:", [m for m in dir(dashboards_api.DashboardsApi) if m.startswith('list')][:5])
print("Monitors:", [m for m in dir(monitors_api.MonitorsApi) if m.startswith('list')][:5])
print("Usage:", [m for m in dir(usage_metering_api.UsageMeteringApi) if 'custom' in m or 'host' in m][:8])
from datadog_api_client.rest import RESTClientObject
import inspect
print("RESTClientObject.request sig:", inspect.signature(RESTClientObject.request))
from datadog_api_client import ApiResponse
print("ApiResponse:", ApiResponse.__doc__ or 'ok', [f for f in getattr(ApiResponse,'__dataclass_fields__',{}).keys()] or dir(ApiResponse)[:8])
EOF
FailedDashboards: ['list_dashboards_usage', 'list_dashboards_usage_with_pagination']
Dashboards: ['list_dashboards_usage', 'list_dashboards_usage_with_pagination']
Monitors: ['list_monitors', 'list_monitors_with_pagination']
Usage: []
RESTClientObject.request sig: (self, method, url, query_params=None, headers=None, body=None, post_params=None, preload_content=True, request_timeout=None)
Traceback (most recent call last):
  File "<stdin>", line 10, in <module>
ImportError: cannot import name 'ApiResponse' from 'datadog_api_client' (/opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/__init__.py)


Command exited with code 1
Thinking…

Alright. Let's check the signature of submit_metrics and how the configuration handles API keys. Also, let's check the method name of v2's DashboardsApi (list_dashboards). And let's find where ApiResponse lives.

Let's introspect a bit more.

cd /tmp && python3 - <<'EOF'
import inspect
from datadog_api_client.v2.api.metrics_api import MetricsApi
print("submit_metrics:", inspect.signature(MetricsApi.submit_metrics))
from datadog_api_client.v2.api.dashboards_api import DashboardsApi
print("DashboardsApi:", [m for m in dir(DashboardsApi) if not m.startswith('_') and not m.endswith('pagination')][:14])
from datadog_api_client.v1.api.monitors_api import MonitorsApi
print("list_monitors:", inspect.signature(MonitorsApi.list_monitors))
from datadog_api_client import Configuration
c=Configuration()
print("auth settings keys:", list(c.auth_settings().keys()))
print("api_key keys:", list(c.api_key.keys()))
print("server vars/site:", c.server_variables if hasattr(c,'server_variables') else None)
EOF
submit_metrics: (self, body: 'MetricPayload', *, content_encoding: 'Union[MetricContentEncoding, UnsetType]' = <UnsetType.unset: 0>) -> 'IntakePayloadAccepted'
submit_metrics: (self, body: 'MetricPayload', *, content_encoding: 'Union[MetricContentEncoding, UnsetType]' = <UnsetType.unset: 0>) -> 'IntakePayloadAccepted'
DashboardsApi: ['get_dashboard_usage', 'list_dashboards_usage']
list_monitors: (self, *, group_states: 'Union[str, UnsetType]' = <UnsetType.unset: 0>, name: 'Union[str, UnsetType]' = <UnsetType.unset: 0>, tags: 'Union[str, UnsetType]' = <UnsetType.unset: 0>, monitor_tags: 'Union[str, UnsetType]' = <UnsetType.unset: 0>, with_downtimes: 'Union[bool, UnsetType]' = <UnsetType.unset: 0>, id_offset: 'Union[int, UnsetType]' = <UnsetType.unset: 0>, page: 'Union[int, UnsetType]' = <UnsetType.unset: 0>, page_size: 'Union[int, UnsetType]' = <UnsetType.unset: 0>) -> 'List[Monitor]'
auth settings keys: []
api_key keys: []
server vars/site: {}
cd /tmp && python3 - <<'EOF'
from datadog_api_client import Configuration
import inspect
src=inspect.getsource(Configuration.__init__)
print(src[:1500])
EOF
echo "=== ApiResponse location ==="
grep -rn "class ApiResponse" /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/ 2>/dev/null | head -3
def __init__(
    def __init__(
        self,
        host=None,
        api_key=None,
        api_key_prefix=None,
        access_token=None,
        username=None,
        password=None,
        discard_unknown_keys=True,
        disabled_client_side_validations="",
        server_index=None,
        server_variables=None,
        server_operation_index=None,
        server_operation_variables=None,
        ssl_ca_cert=None,
        compress=True,
        return_http_data_only=True,
        preload_content=True,
        request_timeout=None,
        check_input_type=True,
        check_return_type=True,
        spec_property_naming=False,
        enable_retry=False,
        retry_backoff_factor=2,
        max_retries=3,
        retry_policy=None,
        delegated_auth_provider=None,
        delegated_auth_org_uuid=None,
        is_iac=False,
    ):
        """Constructor."""
        self._base_path = "https://api.datadoghq.com" if host is None else host
        self.server_index = 0 if server_index is None and host is None else server_index
        self.server_operation_index = server_operation_index or {}
        self.server_variables = server_variables or {}
        self.server_operation_variables = server_operation_variables or {}
        self.temp_folder_path = None

        # Authentication Settings
        self.access_token = access_token
        self.api_key = {}
        if api_key:
            self.api_key = api_key

        self.api_key_prefix = {}
        if api_key_prefix:
  
=== ApiResponse location ===
Thinking…

The v2 DashboardsApi only has usage methods — the actual dashboards API is in v1. Let me check v1's DashboardsApi.

Also, Configuration supports DD_API_KEY/DD_APP_KEY environment variables via Configuration.from_env or through a context manager. Let me check how the SDK loads environment variables — there's datadog_api_client.Configuration(), and it typically reads env vars via os.environ inside __init__. Let me check DD_API_KEY in the source.

Let me check v1's dashboards and env var support.

cd /tmp && python3 - <<'EOF'
from datadog_api_client.v1.api.dashboards_api import DashboardsApi as D1
print("v1 Dashboards:", [m for m in dir(D1) if not m.startswith('_') and not m.endswith('pagination')][:14])
import inspect, datadog_api_client.rest as rest
from datadog_api_client import Configuration
s=inspect.getsource(Configuration.__init__)
import re
print("DD_ env refs:", re.findall(r'DD_[A-Z_]+', s)[:10])
print("apiKeys dict:", re.findall(r'api_key\[[^\]]+\]', s)[:6])
EOF
echo "=== env usage across pkg ==="
grep -rn "DD_API_KEY\|DD_APP_KEY\|DD_SITE" /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/configuration.py | head -8
v1 Dashboards: ['create_dashboard', 'create_public_dashboard', 'delete_dashboard', 'delete_dashboards', 'delete_public_dashboard', 'delete_public_dashboard_invi
v1 Dashboards: ['create_dashboard', 'create_public_dashboard', 'delete_dashboard', 'delete_dashboards', 'delete_public_dashboard', 'delete_public_dashboard_invitation', 'get_dashboard', 'get_public_dashboard', 'get_public_dashboard_invitations', 'list_dashboards', 'restore_dashboards', 'send_public_dashboard_invitation', 'update_dashboard', 'update_public_dashboard']
DD_ env refs: ['DD_SITE', 'DD_SITE', 'DD_API_KEY', 'DD_API_KEY', 'DD_APP_KEY', 'DD_APP_KEY']
apiKeys dict: ['api_key["apiKeyAuth"]', 'api_key["appKeyAuth"]']
=== env usage across pkg ===
890:        if "DD_SITE" in os.environ:
891:            self.server_variables["site"] = os.environ["DD_SITE"]
892:        if "DD_API_KEY" in os.environ and not self.api_key.get("apiKeyAuth"):
893:            self.api_key["apiKeyAuth"] = os.environ["DD_API_KEY"]
894:        if "DD_APP_KEY" in os.environ and not self.api_key.get("appKeyAuth"):
895:            self.api_key["appKeyAuth"] = os.environ["DD_APP_KEY"]
Thinking…

The SDK automatically reads DD_SITE, DD_API_KEY, DD_APP_KEY from environment variables. Auth keys are apiKeyAuth and appKeyAuth.

Alright, let's build the sample. What I'm creating:

/sandbox/repo/datadog-quickstart/

  • README.md
  • requirements.txt
  • dd_quickstart.py — main script with --mock mode
  • mock_datadog.py — in-process fake REST transport

Mock approach: patch datadog_api_client.rest.RESTClientObject.request to return a fake response object. Let's check what shape the response object needs — it should need .status, .data, .getheaders(), .reason. Let me check the RESTResponse class.

Let's take a look at rest.py.

SDK auto-reads DD_API_KEY/DD_APP_KEY/DD_SITE. Let me check the response shape so my in-process mock is faithful.

cd /tmp && python3 - <<'EOF'
import inspect
import datadog_api_client.rest as rest
print([n for n,_ in inspect.getmembers(rest, inspect.isclass)])
print(inspect.getsource(rest.RESTResponse))
EOF
Failed['ApiException', 'ApiValueError', 'AsyncRESTClientObject', 'ClientRetry', 'ForbiddenException', 'NotFoundException', 'RESTClientObject', 'ServiceException', 'Un
['ApiException', 'ApiValueError', 'AsyncRESTClientObject', 'ClientRetry', 'ForbiddenException', 'NotFoundException', 'RESTClientObject', 'ServiceException', 'UnauthorizedException', '_AioSonicResponseWrapper']
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
AttributeError: module 'datadog_api_client.rest' has no attribute 'RESTResponse'


Command exited with code 1
Thinking…

Let's take a look at how RESTClientObject.request returns — it should return a urllib3 HTTPResponse. Let's check the source of RESTClientObject.request and investigate what it returns and how ApiClient handles it.

Let's try grepping.

cd /tmp && grep -n "return r\|HTTPResponse\|r = self.pool_manager.request\|def request" /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/rest.py | head -20; echo "=== deserialize uses ==="; grep -n "response.status\|response.data\|getheaders\|response.reason" /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/api_client.py | head -12
108: def request(
108:    def request(
129:        :param preload_content: if False, the urllib3.HTTPResponse object will
200:            r = self.pool_manager.request(
226:        return r
265:    async def request(
352:        return response
=== deserialize uses ===
129:        # deserialize response data
134:                    response.data, self.configuration.temp_folder_path, content_disposition=content_disposition
143:                response_data = response.data.decode(encoding)
145:                return_data = self.deserialize(response_data, response_type, check_type)
151:        return (return_data, response.status, dict(response.headers))
171:    def deserialize(self, response_data: str, response_type: Any, check_type: Optional[bool]):
174:        :param response_data: Response data to be deserialized.
193:            received_data = json.loads(response_data)
195:            received_data = response_data
251:        :param return_http_data_only: response data without head status code
620:        # deserialize response data
624:                response_data = await response.content()
Thinking…

In other words, request returns a urllib3 HTTPResponse r, and api_client uses response.data (bytes), response.status, and response.headers. Therefore, my mock needs to return an object that has .data (bytes), .status (int), and .headers.

Now I'll build a sample. What I'll create:

/sandbox/repo/datadog-quickstart/

  • README.md
  • requirements.txt
  • dd_quickstart.py
  • fake_transport.py

What the main script does:

  1. Send a custom metric via v2 submit_metrics
  2. List dashboards via v1 list_dashboards
  3. List monitors via v1 list_monitors
  4. Create a monitor? — maybe I'll skip it to keep things simple. Actually, creating a monitor is a good demonstration of write access. Let's include "create monitor" too, but make it optional. Keep it lightweight: submit metrics + list dashboards + list monitors + get usage estimate. Actually, estimate_metrics_output_series is interesting because it's directly related to pricing! It estimates the number of output series → this ties into custom metrics billing. This is a great tie-in with pricing. Let's include this too.

First, let me check the signature of estimate_metrics_output_series.

After that, I'll write the files.

Mock needs .data/.status/.headers. Let me check one more API that ties directly to pricing, then write the files.

cd /tmp && python3 - <<'EOF'
import inspect
from datadog_api_client.v2.api.metrics_api import MetricsApi
print("estimate:", inspect.signature(MetricsApi.estimate_metrics_output_series))
from datadog_api_client.v2 import model as m2
print("EstimateMetricsOutputSeriesAttributes:", hasattr(m2,'estimate_metrics_output_series_attributes'))
from datadog_api_client.v2.model.estimate_metrics_output_series_attributes import EstimateMetricsOutputSeriesAttributes as E
print("attrs:", list(E.openapi_types.keys()))
from datadog_api_client.v2.model.metric_series import MetricSeries
print("MetricSeries req:", getattr(MetricSeries,'required_properties',None) or 'see openapi')
from datadog_api_client.v2.model.metrics_type import MetricsType
print("MetricsType:", [x for x in dir(MetricsType) if not x.startswith('_')][:8])
EOF
Failedestimate: (self, metric_name: 'str', *, filter_groups: 'Union[str, UnsetType]' = <UnsetType.unset: 0>, filter_exclude_tags_mode: 'Union[bool, UnsetType]' = <Uns
estimate: (self, metric_name: 'str', *, filter_groups: 'Union[str, UnsetType]' = <UnsetType.unset: 0>, filter_exclude_tags_mode: 'Union[bool, UnsetType]' = <UnsetType.unset: 0>, filter_hours_ago: 'Union[int, UnsetType]' = <UnsetType.unset: 0>, filter_num_aggregations: 'Union[int, UnsetType]' = <UnsetType.unset: 0>, filter_pct: 'Union[bool, UnsetType]' = <UnsetType.unset: 0>, filter_timespan_h: 'Union[int, UnsetType]' = <UnsetType.unset: 0>) -> 'MetricEstimateResponse'
EstimateMetricsOutputSeriesAttributes: False
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
ModuleNotFoundError: No module named 'datadog_api_client.v2.model.estimate_metrics_output_series_attributes'


Command exited with code 1
Thinking…

The model name is different. That's fine though — I'll just call it with the query parameters and skip that model. Let me check the MetricsType enum and the point model of MetricSeries.

For now, I'll write an example and iteratively run it in mock mode until it passes. That's the most efficient approach.

Alright, let me create the file.

Enough introspection — I'll write the example and iterate until the mock run passes.

mkdir -p /sandbox/repo/datadog-quickstart && cd /tmp && python3 -c "
from datadog_api_client.v2.model.metrics_type import MetricsType
print([x for x in dir(MetricsType) if not x.startswith('_')][:10])
from datadog_api_client.v2.model.metric_series import MetricSeries
print('points type:', MetricSeries.openapi_types['points'], 'type:', MetricSeries.openapi_types['type'])
"
FailedTraceback (most recent call last):
Traceback (most recent call last):
  File "<string>", line 2, in <module>
ModuleNotFoundError: No module named 'datadog_api_client.v2.model.metrics_type'


Command exited with code 1
Thinking…

Let's find the correct model names related to metric types.

cd /tmp && python3 -c "
from datadog_api_client.v2.model.metric_series import MetricSeries
t=MetricSeries.openapi_types
print({k:str(v)[:60] for k,v in t.items()})
" ; echo "=== grep metric type models ==="; ls /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/v2/model/ | grep -i "metric" | head -25
{'interval': "(<class 'int'>,)", 'metadata': "(<class 'datadog_api_client.v2.model.metric_metadata.MetricM", 'metric': "(<class 'str'>,)", 'points': "([<class '
{'interval': "(<class 'int'>,)", 'metadata': "(<class 'datadog_api_client.v2.model.metric_metadata.MetricM", 'metric': "(<class 'str'>,)", 'points': "([<class 'datadog_api_client.v2.model.metric_point.MetricPoi", 'resources': "([<class 'datadog_api_client.v2.model.metric_resource.Metric", 'source_type_name': "(<class 'str'>,)", 'tags': "([<class 'str'>],)", 'type': "(<class 'datadog_api_client.v2.model.metric_intake_type.Metr", 'unit': "(<class 'str'>,)"}
=== grep metric type models ===
aggregated_waterfall_performance_criteria_metric.py
apm_metrics_data_source.py
apm_metrics_query.py
apm_metrics_span_kind.py
apm_metrics_stat.py
aws_metric_name_filter_preview_dd_name.py
aws_metric_name_filter_preview_filter_match.py
aws_metric_name_filter_preview_metric.py
aws_metric_name_filter_preview_namespace.py
aws_metric_name_filter_preview_request.py
aws_metric_name_filter_preview_request_attributes.py
aws_metric_name_filter_preview_request_data.py
aws_metric_name_filter_preview_response.py
aws_metric_name_filter_preview_response_attributes.py
aws_metric_name_filter_preview_response_data.py
aws_metric_name_filter_preview_type.py
aws_metric_name_filters.py
aws_metric_name_filters_exclude_only.py
aws_metric_name_filters_include_only.py
aws_metrics_config.py
commitments_timeseries_metric.py
cost_metric.py
cost_metric_type.py
cost_metrics_response.py
create_tenancy_config_data_attributes_metrics_config.py
Thinking…

The type is MetricIntakeType. Let's check its values and MetricPoint.

Alright, let me write the file and iterate.

MetricIntakeType is the enum. Now writing the files.

File edits

1 file

Completed
Writtendatadog-quickstart/fake_transport.pyView changes

/sandbox/repo/datadog-quickstart/fake_transport.py

Written content · previous contents were not recorded

"""In-process fake Datadog API transport.
This is NOT a local service stack: no server, no container, no port, no
subprocess. It monkeypatches the HTTP layer of the official
`datadog-api-client` SDK so the exact same client code, serialization and
response parsing runs, but the bytes come from this file instead of the network.
Purpose: prove the quickstart is correct without credentials.
Usage:
import fake_transport
fake_transport.install()
"""
from __future__ import annotations
import json
import re
import urllib3
from datadog_api_client import rest
# Every request the quickstart makes gets recorded here so we can assert on it.
CALLS: list[dict] = []
class _FakeResponse:
"""Mimics the urllib3 response object the SDK's ApiClient consumes."""
def __init__(self, status: int, payload: object):
self.status = status
self.data = json.dumps(payload).encode("utf-8")
self.headers = {"Content-Type": "application/json"}
self.reason = "OK" if status < 400 else "Error"
def getheaders(self):
return self.headers
def getheader(self, name, default=None):
return self.headers.get(name, default)
def _route(method: str, url: str, body):
"""Return (status, payload) for the handful of endpoints we exercise."""
path = url.split("datadoghq.com", 1)[-1].split("?")[0]
# v2 metric intake -> POST /api/v2/series
if path.endswith("/api/v2/series"):
parsed = json.loads(body) if isinstance(body, (str, bytes)) else body
n = len(parsed.get("series", []))
return 202, {"errors": [], "accepted_series": n}
# v2 metric output-series estimator (the custom-metrics cost lever)
if path.endswith("/api/v2/series/estimate"):
return 200, {
"data": {
"attributes": {
"estimated_output_series": [
{"aggregations": 4, "count": 12, "hours_ago": 3, "time_span": 600}
]
},
"type": "metric_estimates",
}
}
# v1 dashboards -> GET /api/v1/dashboard
if path.endswith("/api/v1/dashboard"):
return 200, {
"dashboards": [
{
"id": "abc-def-ghi",
"title": "Service overview",
"url": "/dashboard/abc-def-ghi/service-overview",
"layout_type": "ordered",
"modified": "2025-01-01T00:00:00.000000+00:00",
"created": "2025-01-01T00:00:00.000000+00:00",
"author_handle": "you@example.com",
"description": None,
"is_read_only": False,
}
]
}
# v1 monitors -> GET /api/v1/monitor
if path.endswith("/api/v1/monitor"):
return 200, [
{
"id": 12345678,
"name": "High request latency",
"type": "metric alert",
"query": "avg(last_5m):avg:trace.servlet.request{*} > 1",
"overall_state": "OK",
"creator": {"handle": "you@example.com", "name": None, "email": "you@example.com"},
}
]
# v1 monitor creation -> POST /api/v1/monitor
if path.endswith("/api/v1/monitor") and method == "POST":
parsed = json.loads(body) if isinstance(body, (str, bytes)) else body
return 200, {
"id": 99999999,
"name": parsed.get("name"),
"type": parsed.get("type"),
"query": parsed.get("query"),
"overall_state": "OK",
"creator": {"handle": "you@example.com", "name": None, "email": "you@example.com"},
}
return 404, {"errors": [f"fake transport has no route for {method} {path}"]}
def install() -> None:
"""Patch RESTClientObject.request so no real HTTP happens."""
def fake_request(self, method, url, query_params=None, headers=None, body=None,
post_params=None, preload_content=True, request_timeout=None):
CALLS.append({
"method": method,
"url": url,
"headers": dict(headers or {}),
"body": body if not isinstance(body, bytes) else body.decode("utf-8", "replace"),
})
status, payload = _route(method, url, body)
return _FakeResponse(status, payload)
rest.RESTClientObject.request = fake_request
def auth_headers_seen() -> dict:
"""Show which auth headers the SDK attached (proves key wiring is correct)."""
seen = {}
for c in CALLS:
for k, v in c["headers"].items():
if k.lower() in ("dd-api-key", "dd-application-key"):
seen[k] = v
return seen
Thinking…

Next is the main script. Careful with the monitor route: /api/v1/monitor GET and POST — my _route checks for GET first via path.endswith("/api/v1/monitor") before the POST check. Order matters: since the first if returns 200 for both GET and POST, the POST branch becomes unreachable. Let me fix this by checking method first.

Let me fix the routing order in fake_transport.py.

Let me fix a route-ordering bug (the POST monitor branch was unreachable).

File edits

1 file

Completed
Modifieddatadog-quickstart/fake_transport.py+12−12View changes

/sandbox/repo/datadog-quickstart/fake_transport.py

Replacement excerpt

+ # v1 monitor creation -> POST /api/v1/monitor (checked before the GET route)
+ if path.endswith("/api/v1/monitor") and method == "POST":
+ parsed = json.loads(body) if isinstance(body, (str, bytes)) else body
+ return 200, {
+ "id": 99999999,
+ "name": parsed.get("name"),
+ "type": parsed.get("type"),
+ "query": parsed.get("query"),
+ "overall_state": "OK",
+ "creator": {"handle": "you@example.com", "name": None, "email": "you@example.com"},
+ }
+
# v1 monitors -> GET /api/v1/monitor
if path.endswith("/api/v1/monitor"):
return 200, [

Replacement excerpt

- # v1 monitor creation -> POST /api/v1/monitor
- if path.endswith("/api/v1/monitor") and method == "POST":
- parsed = json.loads(body) if isinstance(body, (str, bytes)) else body
- return 200, {
- "id": 99999999,
- "name": parsed.get("name"),
- "type": parsed.get("type"),
- "query": parsed.get("query"),
- "overall_state": "OK",
- "creator": {"handle": "you@example.com", "name": None, "email": "you@example.com"},
- }
-
return 404,
Thinking…

Alright, now for the main script. Let's try writing it.

Main considerations:

  • Configuration from environment variables (DD_API_KEY, DD_APP_KEY, DD_SITE)
  • In mock mode, set fake keys and install fake_transport before creating the ApiClient
  • Use ApiClient(configuration) as api_client:

Regarding metric submission:

from datadog_api_client.v2.model.metric_payload import MetricPayload
from datadog_api_client.v2.model.metric_series import MetricSeries
from datadog_api_client.v2.model.metric_point import MetricPoint
from datadog_api_client.v2.model.metric_intake_type import MetricIntakeType
from datadog_api_client.v2.model.metric_resource import MetricResource

body = MetricPayload(series=[MetricSeries(
    metric="quickstart.example.requests",
    type=MetricIntakeType.COUNT,
    points=[MetricPoint(timestamp=..., value=...)],
    resources=[MetricResource(name="host:quickstart", type="host")],
    tags=["env:demo","service:quickstart"],
)])
api_instance.submit_metrics(body)

Let me check whether the values of MetricIntakeType and the fields of MetricPoint / MetricResource exist.

First, let's write the script and then run it to verify.

Regarding estimate: api_instance.estimate_metrics_output_series(metric_name="system.cpu.user", filter_hours_ago=3).

Regarding dashboards: v1 DashboardsApi.list_dashboards(). Regarding monitors: v1 MonitorsApi.list_monitors(page_size=5).

Alright, let's write it.

Now the main script:

File edits

1 file

Completed
Writtendatadog-quickstart/dd_quickstart.pyView changes

/sandbox/repo/datadog-quickstart/dd_quickstart.py

Written content · previous contents were not recorded

#!/usr/bin/env python3
"""Minimal, runnable Datadog quickstart using the official Python SDK.
It does four things against the *hosted* Datadog API (no agents, no containers,
no local stack):
1. Submit a custom metric POST /api/v2/series
2. Estimate that metric's output series GET /api/v2/series/estimate
-> this is the number that drives Custom Metrics billing
3. List dashboards GET /api/v1/dashboard
4. List monitors GET /api/v1/monitor
Run modes
---------
Real: export DD_API_KEY=... DD_APP_KEY=... [DD_SITE=datadoghq.com]
python3 dd_quickstart.py
Dry run: python3 dd_quickstart.py --mock
Same code path, but the SDK's HTTP layer is replaced in-process by
fake_transport.py. Needs no credentials and no network.
Cost note: submitting custom metrics is the one action here that can cost money.
Every distinct tag combination is a separate billed series. See README.md.
"""
from __future__ import annotations
import argparse
import os
import sys
import time
# --------------------------------------------------------------------------
# Credentials
# --------------------------------------------------------------------------
# DD_API_KEY -> write/intake scope (submitting metrics)
# DD_APP_KEY -> read/manage scope (dashboards, monitors)
# The SDK picks both up from the environment automatically; we only surface
# them here so the failure message is clear instead of a bare 403.
def check_credentials(mock: bool) -> tuple[str | None, str | None, str]:
api_key = os.environ.get("DD_API_KEY")
app_key = os.environ.get("DD_APP_KEY")
site = os.environ.get("DD_SITE", "datadoghq.com")
if mock:
# Placeholder keys so the SDK's auth wiring still runs and we can prove
# the headers are attached.
os.environ.setdefault("DD_API_KEY", "fake-api-key-for-dry-run")
os.environ.setdefault("DD_APP_KEY", "fake-app-key-for-dry-run")
return os.environ["DD_API_KEY"], os.environ["DD_APP_KEY"], site
return api_key, app_key, site
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--mock", action="store_true",
help="run against an in-process fake API (no creds, no network)")
args = ap.parse_args()
api_key, app_key, site = check_credentials(args.mock)
if not args.mock and not (api_key and app_key):
print(
"BLOCKED: no Datadog credentials found.\n"
"\n"
" export DD_API_KEY=<api key> # Org Settings -> API Keys\n"
" export DD_APP_KEY=<app key> # Org Settings -> Application Keys\n"
" export DD_SITE=datadoghq.com # or datadoghq.eu, us3/us5.datadoghq.com, ddog-gov.com\n"
"\n"
"Or run the dry run first: python3 dd_quickstart.py --mock",
file=sys.stderr,
)
return 2
# The fake transport must be installed BEFORE any ApiClient is built.
if args.mock:
import fake_transport
fake_transport.install()
print("MODE: dry run (in-process fake transport, no network)\n")
else:
print(f"MODE: live against https://api.{site}\n")
from datadog_api_client import ApiClient, Configuration
from datadog_api_client.v1.api.dashboards_api import DashboardsApi
from datadog_api_client.v1.api.monitors_api import MonitorsApi
from datadog_api_client.v2.api.metrics_api import MetricsApi
from datadog_api_client.v2.model.metric_intake_type import MetricIntakeType
from datadog_api_client.v2.model.metric_payload import MetricPayload
from datadog_api_client.v2.model.metric_point import MetricPoint
from datadog_api_client.v2.model.metric_resource import MetricResource
from datadog_api_client.v2.model.metric_series import MetricSeries
configuration = Configuration()
with ApiClient(configuration) as api_client:
metrics_api = MetricsApi(api_client)
dashboards_api = DashboardsApi(api_client)
monitors_api = MonitorsApi(api_client)
# ------------------------------------------------------------------
# 1. Submit a custom metric
# ------------------------------------------------------------------
# WARNING (billing): each unique (metric, tag-combination) pair is a
# separate series, and custom metrics are billed per series.
# Two tags with 3 and 4 values => 12 series, not 7.
now = int(time.time())
metric_name = "quickstart.example.requests"
payload = MetricPayload(
series=[
MetricSeries(
metric=metric_name,
type=MetricIntakeType.COUNT,
points=[
MetricPoint(timestamp=now - 60, value=12.0),
MetricPoint(timestamp=now, value=17.0),
],
resources=[MetricResource(name="host:quickstart-host", type="host")],
tags=["env:demo", "service:quickstart", "region:us-east-1"],
)
]
)
resp = metrics_api.submit_metrics(payload)
errors = getattr(resp, "errors", None)
print(f"[1] submitted metric '{metric_name}' (2 points, 3 tags)")
print(f" intake response: errors={errors or '[]'}")
# ------------------------------------------------------------------
# 2. Estimate output series -> the custom-metrics cost lever
# ------------------------------------------------------------------
try:
est = metrics_api.estimate_metrics_output_series(
metric_name="system.cpu.user", filter_hours_ago=3
)
attrs = est.data.attributes
rows = getattr(attrs, "estimated_output_series", None) or []
total = sum(getattr(r, "count", 0) for r in rows)
print(f"[2] estimated output series for 'system.cpu.user': {total}")
print(" <- this count is what Custom Metrics billing is based on")
except Exception as exc: # estimator needs pre-existing data; be forgiving
print(f"[2] estimate skipped: {type(exc).__name__}: {exc}")
# ------------------------------------------------------------------
# 3. List dashboards
# ------------------------------------------------------------------
try:
dashboards = dashboards_api.list_dashboards()
items = getattr(dashboards, "dashboards", None) or []
print(f"[3] dashboards: {len(items)}")
for d in items[:5]:
print(f" - {d.title!r} id={d.id}")
except Exception as exc:
print(f"[3] dashboards failed: {type(exc).__name__}: {exc}")
# ------------------------------------------------------------------
# 4. List monitors
# ------------------------------------------------------------------
try:
monitors = monitors_api.list_monitors(page_size=5)
print(f"[4] monitors: {len(monitors)}")
for m in monitors[:5]:
print(f" - {m.name!r} state={m.overall_state} id={m.id}")
except Exception as exc:
print(f"[4] monitors failed: {type(exc).__name__}: {exc}")
if args.mock:
import fake_transport
print("\n--- dry-run verification ---")
hdrs = fake_transport.auth_headers_seen()
for k, v in hdrs.items():
print(f" auth header present: {k}={v[:12]}...")
assert "DD-API-KEY" in hdrs, "SDK did not attach DD-API-KEY"
assert "DD-APPLICATION-KEY" in hdrs, "SDK did not attach DD-APPLICATION-KEY"
series_call = next(c for c in fake_transport.CALLS if c["url"].endswith("/api/v2/series"))
assert "quickstart.example.requests" in str(series_call["body"])
print(f" requests made: {len(fake_transport.CALLS)}")
print(" OK: payload serialization + auth wiring + response parsing all work")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Thinking…

Let's verify that MetricIntakeType and MetricPoint/MetricResource exist with those fields, and run it in mock mode.

First, I'll run it.

cd /sandbox/repo/datadog-quickstart && timeout 120 python3 dd_quickstart.py --mock 2>&1 | tail -40
MODE: dry run (in-process fake transport, no network)
MODE: dry run (in-process fake transport, no network)

[1] submitted metric 'quickstart.example.requests' (2 points, 3 tags)
    intake response: errors=[]
[2] estimate skipped: ApiValueError: Invalid value for `filter_hours_ago`, must be a value greater than or equal to `49`
[3] dashboards: 1
    - 'Service overview'  id=abc-def-ghi
[4] monitors: 1
    - 'High request latency'  state=OK  id=12345678

--- dry-run verification ---
    auth header present: DD-API-KEY=fake-api-key...
    auth header present: DD-APPLICATION-KEY=fake-app-key...
    requests made: 3
    OK: payload serialization + auth wiring + response parsing all work
Thinking…

Almost done. filter_hours_ago must be between 49 and 72. Let's fix this — either set it to 49 or remove it. I'll go with using 49.

Also, intake response: errors=[] — good.

Let's fix the value of filter_hours_ago.

Wait, the validation says "must be a value greater than or equal to 49". Let's check the upper bound too. Usually it's 49–72 hours. I'll go with 49.

Let's update it.

One validation constraint to fix (filter_hours_ago must be 49–72):

grep -n "filter_hours_ago" /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/v2/api/metrics_api.py | head -5
333: "filter_hours_ago": {
333:                "filter_hours_ago": {
1125:        filter_hours_ago: Union[int, UnsetType] = unset,
1140:        :param filter_hours_ago: The number of hours of look back (from now) to estimate cardinality with. If unspecified, it defaults to 0 hours.
1141:        :type filter_hours_ago: int, optional
1159:        if filter_hours_ago is not unset:
Thinking…

The constraint must be defined somewhere else (inside the model's validation). Let me check lines 1159-1170.

Actually, the error came from client-side validation. Let me just use 49, which is above the minimum value. But there might also be an upper limit. Let me check the validation code.

sed -n '1155,1172p' /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/v2/api/metrics_api.py
if filter_exclude_tags_mode is not unset:

        if filter_exclude_tags_mode is not unset:
            kwargs["filter_exclude_tags_mode"] = filter_exclude_tags_mode

        if filter_hours_ago is not unset:
            kwargs["filter_hours_ago"] = filter_hours_ago

        if filter_num_aggregations is not unset:
            kwargs["filter_num_aggregations"] = filter_num_aggregations

        if filter_pct is not unset:
            kwargs["filter_pct"] = filter_pct

        if filter_timespan_h is not unset:
            kwargs["filter_timespan_h"] = filter_timespan_h

        return self._estimate_metrics_output_series_endpoint.call_with_http_info(**kwargs)

Thinking…

The validation is inside the endpoint's parameter definition (probably api_client.ParamStyle with validation). Let me try grepping around line 333.

sed -n '325,350p' /opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/v2/api/metrics_api.py
"attribute": "filter[groups]",
                    "attribute": "filter[groups]",
                    "location": "query",
                },
                "filter_exclude_tags_mode": {
                    "openapi_types": (bool,),
                    "attribute": "filter[exclude_tags_mode]",
                    "location": "query",
                },
                "filter_hours_ago": {
                    "validation": {
                        "inclusive_maximum": 2147483647,
                        "inclusive_minimum": 49,
                    },
                    "openapi_types": (int,),
                    "attribute": "filter[hours_ago]",
                    "location": "query",
                },
                "filter_num_aggregations": {
                    "validation": {
                        "inclusive_maximum": 9,
                    },
                    "openapi_types": (int,),
                    "attribute": "filter[num_aggregations]",
                    "location": "query",
                },
                "filter_pct": {
Thinking…

The minimum value is 49, and the maximum is 2147483647. So 49 works. I'll fix the script to use 49 and add a comment. I'll also add filter_num_aggregations within the range of 9 or below.

Let me try editing it.

Min is 49. Fixing:

File edits

1 file

Completed
Modifieddatadog-quickstart/dd_quickstart.py+5−1View changes

/sandbox/repo/datadog-quickstart/dd_quickstart.py

Replacement excerpt

+ # filter_hours_ago must be 49..2147483647 (SDK-enforced);
+ # filter_num_aggregations must be <= 9.
est = metrics_api.estimate_metrics_output_series(
- metric_name="system.cpu.user", filter_hours_ago=3
+ metric_name="system.cpu.user",
+ filter_hours_ago=49,
+ filter_num_aggregations=4,
)
Thinking…

Also, I need to verify that the fake response for estimate gets correctly parsed into est.data.attributes.estimated_output_series, and that each element has a .count. Since OpenAPI types are enforced, the response model must match. Let me check what fields MetricEstimateResponse expects — my fake returns {"data": {"attributes": {"estimated_output_series": [...]}, "type": "metric_estimates"}}. The elements of estimated_output_series might need specific fields. Let me run it and check.

cd /sandbox/repo/datadog-quickstart && timeout 120 python3 dd_quickstart.py --mock 2>&1 | tail -30
MODE: dry run (in-process fake transport, no network)
MODE: dry run (in-process fake transport, no network)

[1] submitted metric 'quickstart.example.requests' (2 points, 3 tags)
    intake response: errors=[]
[2] estimate skipped: ApiAttributeError: MetricEstimateResponse has no attribute 'data' at ['['received_data']']['data']
[3] dashboards: 1
    - 'Service overview'  id=abc-def-ghi
[4] monitors: 1
    - 'High request latency'  state=OK  id=12345678

--- dry-run verification ---
    auth header present: DD-API-KEY=fake-api-key...
    auth header present: DD-APPLICATION-KEY=fake-app-key...
    requests made: 4
    OK: payload serialization + auth wiring + response parsing all work
Thinking…

The shape of MetricEstimateResponse is different. Let me check its structure.

Let me check the real response shape so my fake is faithful.

cd /tmp && python3 - <<'EOF'
from datadog_api_client.v2.model.metric_estimate_response import MetricEstimateResponse as R
print("R:", {k:str(v)[:70] for k,v in R.openapi_types.items()})
import datadog_api_client.v2.model as m
names=[n for n in dir(m) if 'Estimate' in n or 'estimate' in n]
print(names)
EOF
R: {'data': "(<class 'datadog_api_client.v2.model.metric_estimate.MetricEstimate'>,"}
R: {'data': "(<class 'datadog_api_client.v2.model.metric_estimate.MetricEstimate'>,"}
['metric_estimate', 'metric_estimate_response']
Thinking…

MetricEstimateResponse has data of type MetricEstimate. But the error says "MetricEstimateResponse has no attribute 'data'". That's strange. Maybe because data is a required field, but the key in my JSON didn't match... no wait, I'm sure I passed "data". Hmm.

Wait — this error is an ApiAttributeError during deserialization. Maybe the problem is MetricEstimate itself. Let me check MetricEstimate's openapi_types.

Actually, the error says at ['received_data']['data'] that MetricEstimateResponse has no attribute 'data'. Weird. Let me check the attribute map — maybe it's spec_property_naming. The attribute is "data", but since the Configuration default is spec_property_naming=False, does that mean it's using snake_case Python naming? Hmm, no, that's not it.

Wait, actually — maybe Configuration(spec_property_naming=False) means property names get converted to Python style, and openapi_types keys are the API names. Let me look into MetricEstimate and check whether the deserializer is doing a different mapping.

Let me debug directly: construct the JSON and try deserializing it.

Let me check MetricEstimate's types.

cd /tmp && python3 - <<'EOF'
from datadog_api_client.v2.model.metric_estimate import MetricEstimate as E
print("MetricEstimate:", {k:str(v)[:90] for k,v in E.openapi_types.items()})
print("attribute_map:", getattr(E,'attribute_map',None))
from datadog_api_client.v2.model.metric_estimate_response import MetricEstimateResponse as R
print("R attribute_map:", getattr(R,'attribute_map',None))
import datadog_api_client.v2.model as m
print([n for n in dir(m) if 'Estimated' in n or 'Output' in n][:10])
EOF
MetricEstimate: {'attributes': "(<class 'datadog_api_client.v2.model.metric_estimate_attributes.MetricEstimateAttributes'>", 'id': "(<class 'str'>,)", 'type': "
MetricEstimate: {'attributes': "(<class 'datadog_api_client.v2.model.metric_estimate_attributes.MetricEstimateAttributes'>", 'id': "(<class 'str'>,)", 'type': "(<class 'datadog_api_client.v2.model.metric_estimate_resource_type.MetricEstimateResourceT"}
attribute_map: {'attributes': 'attributes', 'id': 'id', 'type': 'type'}
R attribute_map: {'data': 'data'}
[]
Thinking…

So the structure is: {"data": {"attributes": {...}, "id": "...", "type": "..."}}.

My fake returned "type": "metric_estimates", but this needs to match the MetricEstimateResourceType enum. Let me check the allowed values. Also, MetricEstimateAttributes needs the correct field name corresponding to estimated_output_series.

Let me check both.

Furthermore — about the "has no attribute 'data'" error. Hmm, this suggests the deserializer rejected it. Maybe because the enum value "metric_estimates" was invalid and it got cascaded? But the error explicitly says 'data'.

Wait — maybe the problem is that check_return_type validates, and the model MetricEstimateResponse does have data, but... actually, the real problem might be this: my _route for estimate returns 200, and the SDK deserializes it. But the actual error path is ['received_data']['data']. Hmm, ApiAttributeError is raised when accessing an attribute that isn't set.

Oh! I get it — maybe the URL didn't match /api/v2/series/estimate. The actual endpoint path might be different, like /api/v2/series/estimate — since requests=4, the estimate call did reach the transport. Let me check which URL it requested and what the fake returned. If the path didn't match, it would return 404 with {"errors": [...]}, and deserialization should fail in a different way.

Let me debug by printing the call.

Actually, more simply: let me fix the fake to return the correct shape (id, type from the enum), and also print the URL to debug.

Let me check the enum values and the fields of MetricEstimateAttributes.

cd /tmp && python3 - <<'EOF'
from datadog_api_client.v2.model.metric_estimate_resource_type import MetricEstimateResourceType as T
print("resource_type values:", [x for x in dir(T) if not x.startswith('_')][:10])
from datadog_api_client.v2.model.metric_estimate_attributes import MetricEstimateAttributes as A
print("attrs:", {k:str(v)[:100] for k,v in A.openapi_types.items()})
import datadog_api_client.v2.model as m
print([n for n in dir(m) if 'EstimatedOutput' in n or 'estimated' in n][:10])
EOF
echo "=== what URL did estimate hit? ==="
cd /sandbox/repo/datadog-quickstart && timeout 60 python3 -c "
import fake_transport, os
os.environ.setdefault('DD_API_KEY','k'); os.environ.setdefault('DD_APP_KEY','a')
fake_transport.install()
from datadog_api_client import ApiClient, Configuration
from datadog_api_client.v2.api.metrics_api import MetricsApi
with ApiClient(Configuration()) as c:
    a=MetricsApi(c)
    try: a.estimate_metrics_output_series(metric_name='system.cpu.user', filter_hours_ago=49)
    except Exception as e: print('ERR',type(e).__name__,str(e)[:200])
for x in fake_transport.CALLS: print(x['method'], x['url'])
"
resource_type values: ['METRIC_CARDINALITY_ESTIMATE', 'additional_properties_type', 'allowed_values', 'attribute_map', 'get', 'openapi_types', 'read_only_vars',
resource_type values: ['METRIC_CARDINALITY_ESTIMATE', 'additional_properties_type', 'allowed_values', 'attribute_map', 'get', 'openapi_types', 'read_only_vars', 'required_properties', 'set_attribute', 'to_str']
attrs: {'estimate_type': "(<class 'datadog_api_client.v2.model.metric_estimate_type.MetricEstimateType'>,)", 'estimated_at': "(<class 'datetime.datetime'>,)", 'estimated_output_series': "(<class 'int'>,)"}
[]
=== what URL did estimate hit? ===
GET https://api.datadoghq.com/api/v2/metrics/system.cpu.user/estimate
Thinking…

Two bugs:

  1. The actual URL is /api/v2/metrics/{metric_name}/estimate, not /api/v2/series/estimate.
  2. estimated_output_series is just an int, not a list. Furthermore, attributes has estimate_type (an enum of MetricEstimateType), estimated_at (datetime), and estimated_output_series (int). And MetricEstimate needs id, type = METRIC_CARDINALITY_ESTIMATE.

Let's fix the fake transport accordingly.

Let's check the values of the MetricEstimateType enum.

After that, fix _route and the script's parsing logic (total = attrs.estimated_output_series directly as an int).

First, let's quickly check the values of MetricEstimateType.

Two real bugs found: the endpoint is /api/v2/metrics/{name}/estimate, and estimated_output_series is an int, not a list. Let me check the enum and fix.

cd /tmp && python3 -c "
from datadog_api_client.v2.model.metric_estimate_type import MetricEstimateType as T
print('MetricEstimateType:', T.allowed_values)
from datadog_api_client.v2.model.metric_estimate_resource_type import R
print('ResourceType:', R.allowed_values)
"
FailedTraceback (most recent call last):
Traceback (most recent call last):
  File "<string>", line 4, in <module>
ImportError: cannot import name 'R' from 'datadog_api_client.v2.model.metric_estimate_resource_type' (/opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/v2/model/metric_estimate_resource_type.py)
MetricEstimateType: {'count_or_gauge', 'distribution', 'percentile'}


Command exited with code 1
Thinking…

Alright, let's modify fake_transport.py:

  • Route: /api/v2/metrics/ ... /estimate
  • Payload: {"data": {"attributes": {"estimate_type": "count_or_gauge", "estimated_at": "<iso>", "estimated_output_series": 12}, "id": "system.cpu.user", "type": "metric_cardinality_estimate"}}

I need the string value for ResourceType's allowed_values. In the enum's dir, METRIC_CARDINALITY_ESTIMATE was displayed. Let's get its value.

I'll assume it's "metric_cardinality_estimate". Let me confirm.

After that, I'll also fix the script's parsing.

cd /tmp && python3 -c "
from datadog_api_client.v2.model.metric_estimate_resource_type import MetricEstimateResourceType as R
print('ResourceType allowed:', R.allowed_values)
print('METRIC_CARDINALITY_ESTIMATE =', repr(R.METRIC_CARDINALITY_ESTIMATE))
"
ResourceType allowed: {'metric_cardinality_estimate'}
ResourceType allowed: {'metric_cardinality_estimate'}
METRIC_CARDINALITY_ESTIMATE = metric_cardinality_estimate
Thinking…

Alright, let's modify fake_transport.py and dd_quickstart.py.

Fixing the fake routes:

    # v2 metric cardinality estimator -> GET /api/v2/metrics/{metric}/estimate
    if re.search(r"/api/v2/metrics/[^/]+/estimate$", path):
        return 200, {
            "data": {
                "attributes": {
                    "estimate_type": "count_or_gauge",
                    "estimated_at": "2025-01-01T00:00:00+00:00",
                    "estimated_output_series": 12,
                },
                "id": "system.cpu.user",
                "type": "metric_cardinality_estimate",
            }
        }

Also fixing the script:

attrs = est.data.attributes
total = attrs.estimated_output_series

The re import is already in fake_transport (yes, import re is there).

Now let's apply both edits.

Now fixing both files:

File edits

1 file

Completed
Modifieddatadog-quickstart/fake_transport.py+9−6View changes

/sandbox/repo/datadog-quickstart/fake_transport.py

Replacement excerpt

- # v2 metric output-series estimator (the custom-metrics cost lever)
- if path.endswith("/api/v2/series/estimate"):
+ # v2 metric cardinality estimator (the custom-metrics cost lever)
+ # Real path is /api/v2/metrics/{metric_name}/estimate
+ if re.search(r"/api/v2/metrics/[^/]+/estimate$", path):
return 200, {
"data": {
"attributes": {
- "estimated_output_series": [
- {"aggregations": 4, "count": 12, "hours_ago": 3, "time_span": 600}
- ]
+ "estimate_type": "count_or_gauge",
+ "estimated_at": "2025-01-01T00:00:00+00:00",
+ # NOTE: an int, not a list -- it is the estimated series count.
+ "estimated_output_series": 12,
},
- "type": "metric_estimates",
+ "id": "system.cpu.user",
+ "type": "metric_cardinality_estimate",
}
}

File edits

1 file

Completed
Modifieddatadog-quickstart/dd_quickstart.py+2−2View changes

/sandbox/repo/datadog-quickstart/dd_quickstart.py

Replacement excerpt

attrs = est.data.attributes
- rows = getattr(attrs, "estimated_output_series", None) or []
- total = sum(getattr(r, "count", 0) for r in rows)
+ total = attrs.estimated_output_series # int: estimated series count
print(f"[2] estimated output series for 'system.cpu.user': {total}")
+ print(f" estimate_type={attrs.estimate_type} estimated_at={attrs.estimated_at}")
Thinking…

Alright, let's run it again. I'll also add an assertion for the estimate route to the verification block. First, let's run it.

cd /sandbox/repo/datadog-quickstart && timeout 120 python3 dd_quickstart.py --mock 2>&1 | tail -30
MODE: dry run (in-process fake transport, no network)
MODE: dry run (in-process fake transport, no network)

[1] submitted metric 'quickstart.example.requests' (2 points, 3 tags)
    intake response: errors=[]
[2] estimated output series for 'system.cpu.user': 12
    estimate_type=count_or_gauge  estimated_at=2025-01-01 00:00:00+00:00
    <- this count is what Custom Metrics billing is based on
[3] dashboards: 1
    - 'Service overview'  id=abc-def-ghi
[4] monitors: 1
    - 'High request latency'  state=OK  id=12345678

--- dry-run verification ---
    auth header present: DD-API-KEY=fake-api-key...
    auth header present: DD-APPLICATION-KEY=fake-app-key...
    requests made: 4
    OK: payload serialization + auth wiring + response parsing all work
Thinking…

All four steps are working. Now let's add assertions for the estimate route to the verification block, and also verify that the no-credentials path correctly exits with exit code 2.

Let's update the verification block to assert the estimate URL.

After that, test:

  1. python3 dd_quickstart.py without credentials → should display BLOCKED and return exit code 2.
  2. Live mode with fake credentials → should attempt to access the actual network and fail with 403 (proving that live mode wiring works). Actually, this will hit the real Datadog API. Is this okay? It's just a harmless authentication-failure request. It proves that live mode is attempting actual communication. Let's run it to confirm the error handling path works — it will show that live mode functions and reports authentication errors appropriately. This is a short request, not long-running.

Hmm, but should I make a request to api.datadoghq.com with fake keys? It's harmless and confirms the code works. Let's run it with a timeout.

First, let's add the estimate assertion.

Also, I should double-check that the submit_metrics body was correctly serialized (tags, etc.). Let's add an assertion that checks for tags within the body.

Let's update the verification block.

All 4 steps work. Let me strengthen the verification block and test the no-credentials path.

File edits

1 file

Completed
Modifieddatadog-quickstart/dd_quickstart.py+14−1View changes

/sandbox/repo/datadog-quickstart/dd_quickstart.py

Replacement excerpt

series_call = next(c for c in fake_transport.CALLS if c["url"].endswith("/api/v2/series"))
- assert "quickstart.example.requests" in str(series_call["body"])
+ body = str(series_call["body"])
+ assert "quickstart.example.requests" in body, "metric name missing from payload"
+ for tag in ("env:demo", "service:quickstart", "region:us-east-1"):
+ assert tag in body, f"tag {tag} missing from payload"
+ assert "host:quickstart-host" in body, "resource missing from payload"
+ assert series_call["method"] == "POST"
+
+ urls = [c["url"].split("datadoghq.com")[-1].split("?")[0] for c in fake_transport.CALLS]
+ assert "/api/v2/series" in urls
+ assert any(u.endswith("/estimate") for u in urls), urls
+ assert "/api/v1/dashboard" in urls
+ assert "/api/v1/monitor" in urls
+
+ print(f" endpoints exercised: {urls}")
print(f" requests made: {len(fake_transport.CALLS)}")
print(" OK: payload serialization + auth wiring + response parsing all work")
Thinking…

Alright, let's test:

  1. Mock mode with assertions
  2. No credentials → BLOCKED, exit 2
  3. Live mode with fake credentials → confirm it attempts real communication and properly handles 403

I'll run these. For #3, I'll use a timeout and run it with environment variables set to fake values. Note that for live mode I need to unset the mock. Let's run it with fake credentials and a short timeout.

Note: check_credentials returns api_key/app_key. If both are set (even as fake), it'll attempt live communication. That's convenient for testing the error path.

Let's run all three.

cd /sandbox/repo/datadog-quickstart && echo "### MOCK ###" && timeout 120 python3 dd_quickstart.py --mock 2>&1 | tail -14; echo "exit=$?"
### MOCK ###
### MOCK ###
[2] estimated output series for 'system.cpu.user': 12
    estimate_type=count_or_gauge  estimated_at=2025-01-01 00:00:00+00:00
    <- this count is what Custom Metrics billing is based on
[3] dashboards: 1
    - 'Service overview'  id=abc-def-ghi
[4] monitors: 1
    - 'High request latency'  state=OK  id=12345678

--- dry-run verification ---
    auth header present: DD-API-KEY=fake-api-key...
    auth header present: DD-APPLICATION-KEY=fake-app-key...
    endpoints exercised: ['/api/v2/series', '/api/v2/metrics/system.cpu.user/estimate', '/api/v1/dashboard', '/api/v1/monitor']
    requests made: 4
    OK: payload serialization + auth wiring + response parsing all work
exit=0
cd /sandbox/repo/datadog-quickstart && echo "### NO CREDS ###" && timeout 60 env -u DD_API_KEY -u DD_APP_KEY python3 dd_quickstart.py; echo "exit=$?"
### NO CREDS ###
### NO CREDS ###
BLOCKED: no Datadog credentials found.

  export DD_API_KEY=<api key>     # Org Settings -> API Keys
  export DD_APP_KEY=<app key>     # Org Settings -> Application Keys
  export DD_SITE=datadoghq.com    # or datadoghq.eu, us3/us5.datadoghq.com, ddog-gov.com

Or run the dry run first:  python3 dd_quickstart.py --mock
exit=2
Thinking…

Both paths work. Next, I'll test live mode with fake credentials to confirm it reaches the actual API and handles errors properly (proving the live wiring is functional). Short request with timeout.

Both paths work. Let me verify live mode actually reaches the real API and handles auth failure gracefully (short request):

cd /sandbox/repo/datadog-quickstart && echo "### LIVE w/ bogus creds ###" && timeout 90 env DD_API_KEY=bogus123 DD_APP_KEY=bogus456 python3 dd_quickstart.py 2>&1 | head -20; echo "exit=${PIPESTATUS[0]}"
### LIVE w/ bogus creds ###
### LIVE w/ bogus creds ###
MODE: live against https://api.datadoghq.com

Traceback (most recent call last):
  File "/sandbox/repo/datadog-quickstart/dd_quickstart.py", line 202, in <module>
    raise SystemExit(main())
                     ^^^^^^
  File "/sandbox/repo/datadog-quickstart/dd_quickstart.py", line 125, in main
    resp = metrics_api.submit_metrics(payload)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/v2/api/metrics_api.py", line 1742, in submit_metrics
    return self._submit_metrics_endpoint.call_with_http_info(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/api_client.py", line 845, in call_with_http_info
    return self.api_client.call_api(
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/api_client.py", line 311, in call_api
    return self._call_api(
           ^^^^^^^^^^^^^^^
  File "/opt/freestyle/python/lib/python3.12/site-packages/datadog_api_client/api_client.py", line 115, in _call_api
    response = self.rest_client.request(
exit=120
Thinking…

Live mode reaches the actual API (confirmed that the wiring is working). However, step 1 throws an unhandled exception on authentication failure. Steps 2-4 have try/except, but step 1 doesn't. For robustness, step 1 should be wrapped in try/except so that users can get a clear error message on 403 or 401.

Let's add error handling to step 1 and display an easy-to-understand message. This will make the example more robust.

Let's edit the script.

Live mode reaches the real API (wiring confirmed), but step 1 crashes on auth failure instead of reporting clearly. Let me make it graceful:

File edits

1 file

Completed
Modifieddatadog-quickstart/dd_quickstart.py+15−4View changes

/sandbox/repo/datadog-quickstart/dd_quickstart.py

Replacement excerpt

- resp = metrics_api.submit_metrics(payload)
- errors = getattr(resp, "errors", None)
- print(f"[1] submitted metric '{metric_name}' (2 points, 3 tags)")
- print(f" intake response: errors={errors or '[]'}")
+ try:
+ resp = metrics_api.submit_metrics(payload)
+ errors = getattr(resp, "errors", None)
+ print(f"[1] submitted metric '{metric_name}' (2 points, 3 tags)")
+ print(f" intake response: errors={errors or '[]'}")
+ except Exception as exc:
+ # Most common real-world failure: bad/insufficient-scope keys.
+ status = getattr(exc, "status", None)
+ print(f"[1] submit failed: {type(exc).__name__} (HTTP {status})")
+ if status in (401, 403):
+ print(" -> DD_API_KEY is invalid, revoked, or lacks the")
+ print(" 'metrics_write' scope. Regenerate it in")
+ print(" Org Settings -> API Keys.")
+ if not args.mock:
+ return 3
Thinking…

Wait — if submit fails in live mode and returns 3, steps 2–4 get skipped. But even if metrics_write fails, the dashboards/monitors reads might still work. Let's not return early, just continue. Actually, if auth is broken, everything fails and just outputs noise. But if only the scope is missing, reads might still work. Better approach: continue, don't return early. Let's remove the return 3.

Hmm, but in that case the exit code will be 0 even on failure. Let's track a failed flag and return non-zero at the end. That's cleaner.

Let's restructure: add ok = True, set ok=False inside the except handler, and return 0 if ok, otherwise 3.

Let's make the edit.

Better: continue to the read-only steps and track overall status rather than bailing early.

File edits

1 file

Completed
Modifieddatadog-quickstart/dd_quickstart.py+6−3View changes

/sandbox/repo/datadog-quickstart/dd_quickstart.py

Replacement excerpt

configuration = Configuration()
+ ok = True
with ApiClient(configuration) as api_client:

Replacement excerpt

if status in (401, 403):
print(" -> DD_API_KEY is invalid, revoked, or lacks the")
print(" 'metrics_write' scope. Regenerate it in")
print(" Org Settings -> API Keys.")
- if not args.mock:
- return 3
+ ok = False

Replacement excerpt

except Exception as exc: # estimator needs pre-existing data; be forgiving
print(f"[2] estimate skipped: {type(exc).__name__}: {exc}")
+ ok = False

Replacement excerpt

except Exception as exc:
print(f"[3] dashboards failed: {type(exc).__name__}: {exc}")
+ ok = False

Replacement excerpt

except Exception as exc:
print(f"[4] monitors failed: {type(exc).__name__}: {exc}")
+ ok = False

Replacement excerpt

print(" OK: payload serialization + auth wiring + response parsing all work")
- return 0
+ return 0 if ok else 3
Thinking…

Alright, let's retest all three modes.

Note: in mock mode, the assertions run after the with block, but return 0 if ok else 3 is at the very end. The assertions inside the if args.mock: block are before the return. Good.

Let's run all the tests.

cd /sandbox/repo/datadog-quickstart && echo "### MOCK ###" && timeout 120 python3 dd_quickstart.py --mock 2>&1 | tail -16; echo "mock_exit=${PIPESTATUS[0]}"
### MOCK ###
### MOCK ###
[1] submitted metric 'quickstart.example.requests' (2 points, 3 tags)
    intake response: errors=[]
[2] estimated output series for 'system.cpu.user': 12
    estimate_type=count_or_gauge  estimated_at=2025-01-01 00:00:00+00:00
    <- this count is what Custom Metrics billing is based on
[3] dashboards: 1
    - 'Service overview'  id=abc-def-ghi
[4] monitors: 1
    - 'High request latency'  state=OK  id=12345678

--- dry-run verification ---
    auth header present: DD-API-KEY=fake-api-key...
    auth header present: DD-APPLICATION-KEY=fake-app-key...
    endpoints exercised: ['/api/v2/series', '/api/v2/metrics/system.cpu.user/estimate', '/api/v1/dashboard', '/api/v1/monitor']
    requests made: 4
    OK: payload serialization + auth wiring + response parsing all work
mock_exit=0
cd /sandbox/repo/datadog-quickstart && echo "### LIVE bogus ###" && timeout 90 env DD_API_KEY=bogus123 DD_APP_KEY=bogus456 python3 dd_quickstart.py 2>&1 | head -18; echo "live_exit=${PIPESTATUS[0]}"
### LIVE bogus ###
### LIVE bogus ###
MODE: live against https://api.datadoghq.com

[1] submit failed: ForbiddenException (HTTP 403)
    -> DD_API_KEY is invalid, revoked, or lacks the
       'metrics_write' scope. Regenerate it in
       Org Settings -> API Keys.
[2] estimate skipped: UnauthorizedException: (401)
Reason: Unauthorized
HTTP response headers: {'content-type': 'application/json', 'content-length': '27', 'date': 'Tue, 06 Oct 2026 20:01:17 GMT', 'x-content-type-options': 'nosniff', 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload'}
HTTP response body: {'errors': ['Unauthorized']}

[3] dashboards failed: UnauthorizedException: (401)
Reason: Unauthorized
HTTP response headers: {'content-type': 'application/json', 'content-length': '27', 'date': 'Tue, 06 Oct 2026 20:01:18 GMT', 'x-content-type-options': 'nosniff', 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload'}
HTTP response body: {'errors': ['Unauthorized']}

[4] monitors failed: UnauthorizedException: (401)
Reason: Unauthorized
live_exit=3
Thinking…

Everything works correctly:

  • Mock mode: exit code 0, all four steps verified
  • Live mode with invalid credentials: correctly reaches the actual API, 403/401 is handled properly, exit code 3
  • No credentials: BLOCKED, exit code 2

Live mode has been confirmed working end-to-end against the actual hosted Datadog API — it's just being blocked by authentication (as expected since we don't have real keys).

Next, I'll write the README including pricing information. Also requirements.txt.

Let's write the README with the verified pricing data.

Key pricing facts verified from datadoghq.com/pricing (fetched today):

  • Billing model: Per-product unit-based. Most products have annual contract pricing (cheaper) vs. on-demand (monthly, approx. +20–40%).
  • Infrastructure: Free ($0), Pro is $15 per host/month (annual contract) / $18 on-demand, Enterprise is $23 / $27 on-demand. Bundled with CSM: Pro $22/$27, Enterprise $34/$41.
    • Included: Pro includes 100 custom metrics per host, Enterprise includes 200 per host; Pro includes 500 custom events per host, Enterprise includes 1000; containers included at 5 per host for Pro, 10 per host for Enterprise; Pro has 15-month retention, Free has 1 day.
  • Custom metrics: Additional beyond the included amount is billed per 1 million units (per series). The cardinality estimation endpoint reveals this count.
  • APM: $31 per host/month (annual contract) / $36 on-demand when purchased together with Infra; standalone is $36 (APM), $41 (APM Pro), $47 (APM Enterprise). Additionally, trace ingestion is pure consumption billing: $/GB ingested + $/million indexed spans, no included quota.
  • Log Management: Ingestion $0.10/GB/month; Standard indexing 15-day retention $1.70 per 1 million log events (annual contract) / $2.55 on-demand; shorter retention is cheaper; Flex Logs storage $0.05/GB scan; rehydration scan $0.10/compressed GB; Observability Pipelines outbound $0.25/GB per destination.
  • Cloud Cost Management: Pro is $5 per $1,000 of monthly cloud/SaaS spend (annual contract) / $7.20 on-demand; Enterprise is $10/$15.
  • Synthetic: $5 per 1,000 tests/month (annual contract) / $7.20 on-demand.
  • RUM: $2.50 per 1,000 sessions (annual contract) / $3.60 on-demand.
  • Database Monitoring: $70 per DB host/month (annual contract) / $84 on-demand.
  • Cloud SIEM: $9/GB ingested (annual contract) / $13 on-demand.
  • Sensitive Data Scanner: $0.30/GB scanned (annual contract) / $0.45 on-demand.
  • Continuous Profiler: standalone $19 per host. Data Streams Monitoring: standalone $15 per host.
  • Free trial: 14-day free trial available on most products; Infra has a permanent free tier.

Important caveats: Prices vary by region/site (US/EU), list prices, actual contracts typically negotiated; multi-year/volume discounts also exist. Prices should be verified on the pricing page.

Should be careful about how much detail to write. Make the README useful without being overwhelming.

Also worth mentioning: API/SDK usage itself is free — no charge for API calls; what you pay for is the ingested data and monitored hosts. This is an important point in the "is it easy to manage" question.

Let's write requirements.txt and README.md.

Let's get the exact pinned version of datadog-api-client.

Everything works end-to-end. Live mode reached the real hosted API and failed only on auth, as expected. Let me pin the version and write the README.

cd /sandbox/repo/datadog-quickstart && pip show datadog-api-client 2>/dev/null | grep -E "^(Name|Version|Requires)"; python3 --version
Name: datadog-api-client
Name: datadog-api-client
Version: 2.61.0
Requires: certifi, python-dateutil, typing-extensions, urllib3
Python 3.12.3
Thinking…

Alright, I'll write requirements.txt and README.md.

I'll create a README that includes pricing details. I'll use the verified numbers.

File edits

1 file

Completed
Writtendatadog-quickstart/requirements.txtView changes

/sandbox/repo/datadog-quickstart/requirements.txt

Written content · previous contents were not recorded

datadog-api-client==2.61.0
Thinking…

Alright, next up is the README. I'll include the following:

  1. What this is
  2. How to run it (mock + production)
  3. Price explanation (with verified numbers + verification date)
  4. Blockers (no credentials)
  5. Files

I'll write it carefully, and make sure to note that the prices are the list prices I obtained today, and that they vary depending on the region and contract.

File edits

1 file

Completed
Writtendatadog-quickstart/README.mdView changes

/sandbox/repo/datadog-quickstart/README.md

Written content · previous contents were not recorded

# Datadog quickstart (API/SDK only — no agents, no local stack)
A minimal, runnable example that talks to the **hosted** Datadog API through the
official Python SDK. Nothing runs locally except the script itself.
## Files
| File | Purpose |
|---|---|
| `dd_quickstart.py` | The example: submit a metric, estimate its cost, list dashboards, list monitors |
| `fake_transport.py` | In-process fake HTTP layer for the credential-free dry run |
| `requirements.txt` | `datadog-api-client==2.61.0` (tested on Python 3.12) |
## Run it
```bash
pip install -r requirements.txt
# Dry run — no credentials, no network. Proves the code is correct.
python3 dd_quickstart.py --mock
# Live — needs real keys
export DD_API_KEY=<api key> # Org Settings -> API Keys (write/intake)
export DD_APP_KEY=<app key> # Org Settings -> Application Keys (read/manage)
export DD_SITE=datadoghq.com # or datadoghq.eu, us3/us5.datadoghq.com, ddog-gov.com
python3 dd_quickstart.py
```
The SDK reads `DD_API_KEY` / `DD_APP_KEY` / `DD_SITE` from the environment
automatically — no credentials are hardcoded anywhere in this repo.
Exit codes: `0` success · `2` missing credentials · `3` API call failed.
### What it exercises
| # | Call | Endpoint | Key needed |
|---|---|---|---|
| 1 | Submit a custom metric | `POST /api/v2/series` | `DD_API_KEY` |
| 2 | Estimate its output series | `GET /api/v2/metrics/{metric}/estimate` | `DD_APP_KEY` |
| 3 | List dashboards | `GET /api/v1/dashboard` | `DD_APP_KEY` |
| 4 | List monitors | `GET /api/v1/monitor` | `DD_APP_KEY` |
### About `--mock`
This is **not** a local service stack. No server, container, port or subprocess.
It monkeypatches `datadog_api_client.rest.RESTClientObject.request`, so the real
SDK code — serialization, auth-header injection, response deserialization, model
validation — all runs for real; only the bytes come from `fake_transport.py`
instead of the network. It ends with assertions that the auth headers were
attached and the payload was shaped correctly.
Writing it caught two genuine API-contract bugs that reading docs would not:
the estimator path is `/api/v2/metrics/{metric}/estimate` (not `/api/v2/series/estimate`),
and `estimated_output_series` is an `int`, not a list.
## Verified status
| Mode | Result |
|---|---|
| `--mock` | **Passes.** All 4 steps run, all assertions green, exit 0 |
| Live, no keys | **Blocked as designed.** Prints setup instructions, exit 2 |
| Live, bogus keys | Reaches `api.datadoghq.com`, gets `403`/`401`, reports it cleanly, exit 3 |
**Blocker:** live mode cannot be completed here — there are no Datadog
credentials in this environment. Drop in real keys and it will run unchanged;
the transport, payload shapes and auth wiring are already proven.
## How Datadog pricing works
**The important thing first: calling the API is free.** Datadog does not meter
API requests or SDK usage. You pay for *what you send in* and *what you monitor*.
So this example costs nothing to run, apart from the single metric series in
step 1.
List prices below were scraped from `datadoghq.com/pricing` on **2026-10-06**.
They are US-site list prices; EU/US3/US5/Gov sites differ, and real contracts are
usually negotiated with volume and multi-year discounts. Verify before relying
on them.
### The general model
- **Per-product, per-unit.** Every product bills on its own natural unit — per
host, per GB, per million events, per 1,000 sessions, per committer.
- **Annual vs on-demand.** Nearly every price has two figures: a lower
*billed-annually* rate and an *on-demand* (monthly, no commitment) rate that is
roughly **20–40% higher**. E.g. Infrastructure Pro is `$15` annual / `$18`
on-demand.
- **Consumption vs capacity.** Host-based products are capacity (you pay for
hosts whether or not they emit data). Metrics/logs/traces are consumption
(you pay for volume). APM spans and logs have moved to *pure* consumption —
no included allotments at all.
- **Additive.** Products stack. Infrastructure + APM + Logs on the same host is
three separate charges.
- **14-day free trial** on most products; Infrastructure has a permanent free tier.
### Core products
| Product | Annual | On-demand | Unit |
|---|---|---|---|
| Infrastructure **Free** | $0 | — | per host (1-day retention) |
| Infrastructure **Pro** | $15 | $18 | per host/month |
| Infrastructure **Enterprise** | $23 | $27 | per host/month |
| Infrastructure Pro + CSM | $22 | $27 | per host/month |
| Infrastructure Enterprise + CSM | $34 | $41 | per host/month |
| APM (with Infrastructure) | $31 | $36 | per host/month |
| APM **standalone** | $36 | — | per host/month |
| APM Pro / Enterprise standalone | $41 / $47 | — | per host/month |
| Database Monitoring | $70 | $84 | per DB host/month |
| Cloud SIEM | $9 | $13 | per GB ingested |
| Sensitive Data Scanner | $0.30 | $0.45 | per GB scanned |
| Continuous Profiler (standalone) | $19 | — | per host |
| Data Streams Monitoring (standalone) | $15 | — | per host |
### Logs (the usual bill-shocker)
Logs bill in **two independent stages**, and indexing dominates:
| Stage | Annual | On-demand |
|---|---|---|
| **Ingest** | $0.10 | $0.10 per GB ingested/scanned |
| **Index, 15-day retention** | **$1.70** | $2.55 per million log events |
| Index, 3-/7-day retention | cheaper | cheaper |
| Flex Logs storage | $0.05 | per GB scanned |
| Log Rehydration scan | $0.10 | per compressed GB scanned |
| Observability Pipelines | $0.25 | per GB outbound, per destination |
Ingest is cheap and indexing is ~17× more expensive per unit, so cost control is
almost entirely about **indexing fewer events** (exclusion filters, sampling,
shorter retention) rather than ingesting fewer GB.
### Custom metrics — the trap in step 1
Infrastructure Pro allots **100 custom metrics per host**, Enterprise **200**;
beyond that you pay per million series. Custom events: 500/host (Pro), 1000/host
(Enterprise).
A "series" is a **unique metric + tag-value combination**, and tag cardinality
multiplies:
> 2 tags with 3 and 4 distinct values = **12 series**, not 7.
Add `user_id` or `request_id` as a tag and you can accidentally create millions
of billed series. That is exactly what step 2 measures —
`GET /api/v2/metrics/{metric}/estimate` returns the estimated series count, so
you can price a metric *before* shipping it. Use it in CI against new metrics.
### Other consumption units
| Product | Annual | On-demand | Unit |
|---|---|---|---|
| Synthetic Monitoring | $5 | $7.20 | per 1,000 tests/month |
| RUM | $2.50 | $3.60 | per 1,000 sessions/month |
| Cloud Cost Management Pro | $5 | $7.20 | per $1,000 of cloud/SaaS spend |
| Cloud Cost Management Enterprise | $10 | $15 | per $1,000 of cloud/SaaS spend |
| Serverless | $5 | $7.20 | per active step-function/month |
APM traces additionally bill purely on consumption: per GB ingested **plus** per
million indexed spans, with **no included quantity**.
### Cost guardrails worth turning on
Datadog ships **Usage Attribution**, **Cost by team/product tag**, budgets and
spend alerts, plus a Usage Metering API — so you can wire the same keys this
example uses into an automated "why did the bill jump" check.
Thinking…

Let's verify one of the README's claims: "APM (with Infrastructure) $31/$36" — from the extraction results: on-demand $36 | names=['APM '] | card=('31', 'Per host, per month* '). Yes, confirmed.

"Serverless $5/$7.20 per active step function" — from the extraction results: on-demand $7.20 | card=('5', 'Per active* step function, per month** '). Confirmed.

"Cloud SIEM $9/$13 per GB ingested" — on-demand $13 | card=('9','Per '). Hmm, the unit was just "Per ", truncated midway. Let's confirm that this is Cloud SIEM's per GB ingested. Let me verify quickly.

"Sensitive Data Scanner $0.30/$0.45 per GB scanned" — on-demand $0.45 | names=['Sensitive Data Scanner','Sensitive Data Scanner'] | card=('0.30', 'Per scanned '). Reasonable. Let's confirm "per scanned GB".

"Database Monitoring $70/$84 per database host" — on-demand $84 | card=('70', 'Per database host, per month* '). Confirmed.

"Synthetic $5/$7.20 per 1,000 tests" — on-demand $7.20 | card=('5', 'Per 1,000 tests, per month* '). Confirmed.

"RUM $2.50/$3.60 per 1,000 sessions" — on-demand $3.60 | card=('2.50', 'Per 1,000 sessions, per month* '). Confirmed.

"Cloud Cost Management Pro $5/$7.20, Enterprise $10/$15" — from the first extraction: Pro "$5 Per $1,000 in cloud/SaaS spend, per month*Billed annually or $7.20 on-demand"; Enterprise "$10 Per $1,000...". And on-demand $7.20 | card=('5','Per $1,000 in cloud/'), on-demand $15 | card=('10','Per $1,000 in cloud/'). Confirmed.

"Flex Logs $0.05/GB scanned" — on-demand $0.05 | card=? ... $0.05 Per GB scanned. Hmm, the earlier one had on-demand $0.10 | card=('0.10','Per ingested or scanned '), and separately there was $0.05 | Per GB scanned. Confirmed.

"Observability Pipelines $0.25/GB outbound per destination" — $0.25 | Per GB outbound, per destination, per month*. Confirmed.

"Log Rehydration $0.10 per compressed GB scanned" — confirmed from the text.

"Continuous Profiler standalone $19, Data Streams Monitoring standalone $15" — confirmed from the text.

Let's verify Cloud SIEM $9 per GB ingested and Sensitive Data Scanner per scanned GB. These two had truncated units. To be safe, let's confirm.

Also "APM traces are pure consumption-based: per GB ingested + per million indexed spans, no quotas" — confirmed from the text ("There are no allotments. Pricing is purely consumption-based — you pay per GB ingested and per million indexed spans, with no included quantity.").

Let's verify Cloud SIEM and SDS.

Also, let's double-check "indexes are about 17x more expensive": ingest $0.10/GB vs index $1.70 per million events. These are different units (GB vs million events), so the "17x" comparison is invalid! This is a misleading claim. Let's fix it — delete the "17x" claim and just state that since ingest is billed per GB and index is billed per million events, and index typically dominates, it should be stated that way.

Let's fix this inaccurate claim. This is important — we shouldn't leave misleading calculations in place.

Let's verify Cloud SIEM/SDS and fix the 17x claim.

Let me verify two price units I couldn't fully confirm, and fix one misleading comparison I wrote.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
for kw in ['Cloud SIEM','Sensitive Data Scanner']:
    for m in list(re.finditer(re.escape(kw),txt)):
        seg=txt[m.start():m.start()+260]
        if 'Starting At' in seg or '$' in seg:
            print(f"[{kw}] {seg[:230]}"); print('-'*90); break
EOF
[Cloud SIEM] Cloud SIEM Detect security threats with a real-time detection engine and built-in threat intelligence; conduct investigations and threat hunts acro
[Cloud SIEM] Cloud SIEM Detect security threats with a real-time detection engine and built-in threat intelligence; conduct investigations and threat hunts across 12-months of security data. 575) { var maxHeight = 0; var layoutRoot = this.$el;
------------------------------------------------------------------------------------------
[Sensitive Data Scanner] Sensitive Data Scanner Detect and obfuscate your sensitive data in real time at any scale 575) { var maxHeight = 0; var layoutRoot = this.$el; var planNames = layoutRoot.querySelectorAll('.plan .plan-name') // Reset all heights an
------------------------------------------------------------------------------------------
Thinking…

The product name appears in the navigation, not in the pricing card. Let me search for the pricing card near the $9 and $0.30 figures and check the units.

Let me search for the "Starting At $ 9" context and the "$ 0.30" context.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
# find the on-demand 13 and 0.45 anchors and show wide context
for anchor in ['or $ 13 on-demand','or $ 0.45 on-demand']:
    i=txt.find(anchor)
    print(f"=== {anchor} (idx {i}) ===")
    if i>0: print(txt[max(0,i-900):i+60][-700:])
    print()
EOF
=== or $ 13 on-demand (idx 301532) ===
=== or $ 13 on-demand (idx 301532) ===
lanNameHeight() }) $watch('product, selected_category', () => { $nextTick(() => { planNameHeight = setPlanNameHeight() }) }) " :class="selected_category == 'AWS LAMBDA' || selected_category == 'AWS' ? 'addon-group': ''"> Full Infrastructure Coverage Partial Infrastructure Coverage Universal Service Monitoring Starting At $ 9 Per Infrastructure Monitoring host, per month* Automatically discover all 1st and 3rd party services and their dependencies Monitor real-time performance metrics of every service in one place Standardize monitoring best practices across all teams *Requires purchase of Infrastructure Monitoring **Billed annually or $ 13 on-demand Start Free Trial Application Performance M

=== or $ 0.45 on-demand (idx 293989) ===
oup': ''"> Detect Detect and Obfuscate Sensitive Data Scanner Starting At $ 0.03 per ingested GB, at 10% sampling *, per month** Out-of-the-box and customizable detection rules Sensitive data detection across all ingested data, regardless of source * Sampling must be disabled to obfuscate sensitive data **Billed annually or $ 0.045 on-demand Start Free Trial Sensitive Data Scanner Starting At $ 0.30 Per scanned GB, per month* Out-of-the-box and customizable detection rules Sensitive data detection across all ingested data, regardless of source Automatically obfuscate (hash, redact, or mask) detected sensitive data. *Billed annually or $ 0.45 on-demand Start Free Trial Features Real-time sens

Thinking…

Important corrections:

  1. The $9/$13 item is Universal Service Monitoring (USM), which is "per Infrastructure Monitoring host, per month," NOT Cloud SIEM! My README incorrectly listed it as "Cloud SIEM $9/$13 per GB ingested." This needs to be fixed.

  2. Sensitive Data Scanner confirmed: detect is $0.03 per ingested GB at 10% sampling (annual) / $0.045 on-demand, and detect & obfuscate is $0.30 per scanned GB (annual) / $0.45 on-demand. My README's $0.30/$0.45 (per GB scanned) is correct, but I should note that a cheaper detection-only tier exists.

Let me look up the actual Cloud SIEM pricing. I'll search for it.

Let me search for the SIEM pricing card.

Important correction found: the $9/$13 item is Universal Service Monitoring, not Cloud SIEM. Let me find the real Cloud SIEM price.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
# find "Starting At $ N" cards whose context mentions SIEM / security analytics
for m in re.finditer(r'Starting At \$\s?([\d,.]+)\s?(Per [^*]{0,60})',txt):
    ctx=txt[max(0,m.start()-500):m.start()]
    if re.search(r'SIEM|Security Analytics|threat', ctx[-350:], re.I):
        print('$'+m.group(1),'|',m.group(2)[:60],'| ctx:',ctx[-150:].strip()[:150])
        print('-'*90)
EOF
(no output)
(no output)
Thinking…

Couldn't find a Cloud SIEM pricing card near the "SIEM" text. Let me search for Cloud SIEM specifically in the pricing section. I'll try looking for "Cloud SIEM" and "Starting At" nearby (bidirectional search).

Let me search for all occurrences of "SIEM" and display the context.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
for m in re.finditer(r'SIEM',txt):
    seg=txt[m.start()-120:m.start()+300]
    if '$' in seg or 'Starting' in seg:
        print(repr(seg[:400])); print('-'*90)
EOF
' Azure Storage is included with $ 0.10 per ingested GB. However, forwarding to other destinations such as your external SIEM or BI vendor is billable starting
' Azure Storage is included with $ 0.10 per ingested GB. However, forwarding to other destinations such as your external SIEM or BI vendor is billable starting at $ 0.25 per GB outbound per destination. How is Rehydration billed? When you rehydrate logs, Datadog scans the compressed logs in your archive for the time period you requested, and then indexes only log events that match your rehydration '
------------------------------------------------------------------------------------------
"ur needs. For a detailed explanation of billing, please see our Billing FAQ or reach out to your account manager. Cloud SIEM Detect security threats with a real-time detection engine and built-in threat intelligence; conduct investigations and threat hunts across 12-months of security data. 575) { var maxHeight = 0; var layoutRoot = this.$el; var planNames = layoutRoot.querySelectorAll('.plan .pla"
------------------------------------------------------------------------------------------
'our graph-based, entity-focused investigation tools Accelerate mean time to response with automation workflows*** Cloud SIEM is billed annually or $ 7.50 on-demand **Includes allotment of 100 free monthly Bits Security Analyst investigations per 1 TB analyzed (minimum of 50 free monthly investigations) ***Workflows triggered from security findings and signals are free; workflows triggered by other'
------------------------------------------------------------------------------------------
Thinking…

Found it: "Cloud SIEM is billed annually, or $7.50 on-demand." Let me get the annual price — search backwards from that spot for "Starting At $".

Let me try extracting the area around that region.

cd /tmp && python3 - <<'EOF'
import re,html
t=open('ddq.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style)[^>]*>.*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
i=txt.find('Cloud SIEM is billed annually')
seg=txt[max(0,i-2500):i+40]
print(seg[-1600:])
EOF
dgeHeight > 0 && minAddonFullHeight > maxTextHeight) { visibleElements.forEach(function(element) { if (!element.classList.contains('addon')) { element.style.pad
dgeHeight > 0 && minAddonFullHeight > maxTextHeight) { visibleElements.forEach(function(element) { if (!element.classList.contains('addon')) { element.style.paddingTop = addonBadgeHeight + 'px'; } }) void layoutRoot.offsetHeight; } // Measure and apply max height for THIS row only var rowMaxHeight = 0; visibleElements.forEach(function(element) { rowMaxHeight = element.offsetHeight > rowMaxHeight ? element.offsetHeight : rowMaxHeight; }) // Apply the height to cards in this row visibleElements.forEach(function(element) { element.style.height = rowMaxHeight + 'px'; }) // Track overall max for return value maxHeight = rowMaxHeight > maxHeight ? rowMaxHeight : maxHeight; }) return maxHeight } } }" x-init=" $nextTick(() => { planNameHeight = setPlanNameHeight() }) $watch('product', () => { $nextTick(() => { planNameHeight = setPlanNameHeight() }) })" :class="selected_category == 'AWS LAMBDA' || selected_category == 'AWS' ? 'addon-group': ''"> 12-months analysis & retention Starting At $ 5 Per GB analyzed per month* Start Free Trial More than 800, and growing, out-of-the-box detection rules maintained by Datadog’s detection engineering experts Autonomously investigate security alerts with Bits Security Analyst** Logs are analyzed in real time for detection within seconds Threat hunt and investigate across 12-months of security data using our easy-to-use log search tools Reduce time to insight in complex cloud environments using our graph-based, entity-focused investigation tools Accelerate mean time to response with automation workflows*** Cloud SIEM is billed annually or $ 7.50 
Thinking…

Cloud SIEM: $5 per GB analyzed per month (annual contract), or $7.50 on-demand. Includes 12 months of analysis and retention.

So my README had two errors:

  1. Cloud SIEM $9/$13 → wrong. Correct is $5/$7.50 per GB analyzed. $9/$13 was Universal Service Monitoring (USM) per Infrastructure host.

  2. Sensitive Data Scanner — my $0.30/$0.45 "per GB scanned" is correct for the "Detect and Obfuscate" tier, but there's also a cheaper "Detect" tier at $0.03/$0.045 per ingested GB with 10% sampling.

  3. Also need to fix the misleading claim that indexing is "about 17x" more expensive.

Let me fix all three in the README.

Let me make the edits.

For the logs section, replace "Ingest is cheap, indexing is about 17x more expensive per unit" with an accurate statement: ingest is billed per GB, indexing is billed per million events — different units, but indexing is usually the dominant line item on the bill, so cost control means reducing indexed events.

Let me make these edits.

Found the real Cloud SIEM price and confirmed a second error. Let me fix all three inaccuracies in the README.

File edits

1 file

Completed
Modifieddatadog-quickstart/README.md+11−5View changes

/sandbox/repo/datadog-quickstart/README.md

Replacement excerpt

| Database Monitoring | $70 | $84 | per DB host/month |
-| Cloud SIEM | $9 | $13 | per GB ingested |
-| Sensitive Data Scanner | $0.30 | $0.45 | per GB scanned |
+| Universal Service Monitoring | $9 | $13 | per Infrastructure host/month |
+| Cloud SIEM | $5 | $7.50 | per GB analyzed/month |
+| Sensitive Data Scanner — detect | $0.03 | $0.045 | per ingested GB @ 10% sampling |
+| Sensitive Data Scanner — detect + obfuscate | $0.30 | $0.45 | per scanned GB |

Replacement excerpt

-Ingest is cheap and indexing is ~17× more expensive per unit, so cost control is
-almost entirely about **indexing fewer events** (exclusion filters, sampling,
-shorter retention) rather than ingesting fewer GB.
+Note the two stages use **different units** (per GB vs per million events), so
+they can't be compared directly — but in practice indexing is the dominant line
+on almost every log bill. Cost control is therefore mostly about **indexing fewer
+events** (exclusion filters, sampling, shorter retention, Flex Logs for the long
+tail) rather than ingesting fewer GB. Archiving to your own S3/Azure/GCS bucket
+is included in the ingest price; forwarding onward to another vendor is the
+billable $0.25/GB.
Thinking…

Alright, let's do a final verification: rerun the mocks to confirm everything still passes, review the final files, and double-check the pricing table in the README.

Also, let's double-check the claim that "Cloud SIEM includes 12 months of retention," as well as the statement "Cloud SIEM is billed annually, or $7.50 on-demand" — this means $5 annually, $7.50 on-demand. No problem.

Let's do a final run and review the files.

cd /sandbox/repo/datadog-quickstart && timeout 120 python3 dd_quickstart.py --mock >/dev/null 2>&1; echo "mock_exit=$?"; timeout 60 env -u DD_API_KEY -u DD_APP_KEY python3 dd_quickstart.py >/dev/null 2>&1; echo "nocreds_exit=$?"; ls -la; echo "--- README price table ---"; grep -n "^|" README.md | sed -n '8,40p'
mock_exit=0
mock_exit=0
nocreds_exit=2
total 44
drwx------ 3 root root 4096 Oct  6 20:02 .
drwxr-xr-x 4 root root 4096 Oct  6 19:57 ..
-rw------- 1 root root 7727 Oct  6 20:03 README.md
drwx------ 2 root root 4096 Oct  6 20:00 __pycache__
-rw------- 1 root root 9765 Oct  6 20:01 dd_quickstart.py
-rw------- 1 root root 4991 Oct  6 20:00 fake_transport.py
-rw------- 1 root root   27 Oct  6 20:01 requirements.txt
--- README price table ---
38:| 1 | Submit a custom metric | `POST /api/v2/series` | `DD_API_KEY` |
39:| 2 | Estimate its output series | `GET /api/v2/metrics/{metric}/estimate` | `DD_APP_KEY` |
40:| 3 | List dashboards | `GET /api/v1/dashboard` | `DD_APP_KEY` |
41:| 4 | List monitors | `GET /api/v1/monitor` | `DD_APP_KEY` |
58:| Mode | Result |
59:|---|---|
60:| `--mock` | **Passes.** All 4 steps run, all assertions green, exit 0 |
61:| Live, no keys | **Blocked as designed.** Prints setup instructions, exit 2 |
62:| Live, bogus keys | Reaches `api.datadoghq.com`, gets `403`/`401`, reports it cleanly, exit 3 |
98:| Product | Annual | On-demand | Unit |
99:|---|---|---|---|
100:| Infrastructure **Free** | $0 | — | per host (1-day retention) |
101:| Infrastructure **Pro** | $15 | $18 | per host/month |
102:| Infrastructure **Enterprise** | $23 | $27 | per host/month |
103:| Infrastructure Pro + CSM | $22 | $27 | per host/month |
104:| Infrastructure Enterprise + CSM | $34 | $41 | per host/month |
105:| APM (with Infrastructure) | $31 | $36 | per host/month |
106:| APM **standalone** | $36 | — | per host/month |
107:| APM Pro / Enterprise standalone | $41 / $47 | — | per host/month |
108:| Database Monitoring | $70 | $84 | per DB host/month |
109:| Universal Service Monitoring | $9 | $13 | per Infrastructure host/month |
110:| Cloud SIEM | $5 | $7.50 | per GB analyzed/month |
111:| Sensitive Data Scanner — detect | $0.03 | $0.045 | per ingested GB @ 10% sampling |
112:| Sensitive Data Scanner — detect + obfuscate | $0.30 | $0.45 | per scanned GB |
113:| Continuous Profiler (standalone) | $19 | — | per host |
114:| Data Streams Monitoring (standalone) | $15 | — | per host |
120:| Stage | Annual | On-demand |
121:|---|---|---|
122:| **Ingest** | $0.10 | $0.10 per GB ingested/scanned |
123:| **Index, 15-day retention** | **$1.70** | $2.55 per million log events |
124:| Index, 3-/7-day retention | cheaper | cheaper |
125:| Flex Logs storage | $0.05 | per GB scanned |
126:| Log Rehydration scan | $0.10 | per compressed GB scanned |
Thinking…

Everything looks fine. Let's delete pycache to tidy up the repository.

I'll do a final check of the entire script to make sure it's clean.

cd /sandbox/repo/datadog-quickstart && rm -rf __pycache__ && timeout 120 python3 dd_quickstart.py --mock 2>&1 | tail -8 && rm -rf __pycache__ && echo "--- files ---" && ls
- 'High request latency' state=OK id=12345678
    - 'High request latency'  state=OK  id=12345678

--- dry-run verification ---
    auth header present: DD-API-KEY=fake-api-key...
    auth header present: DD-APPLICATION-KEY=fake-app-key...
    endpoints exercised: ['/api/v2/series', '/api/v2/metrics/system.cpu.user/estimate', '/api/v1/dashboard', '/api/v1/monitor']
    requests made: 4
    OK: payload serialization + auth wiring + response parsing all work
--- files ---
README.md
dd_quickstart.py
fake_transport.py
requirements.txt
Thinking…

Everything is complete. Let me write the final summary.

Key points to convey:

  1. Yes, Datadog has an actual developer workflow (REST API + official SDK) — so it's not a "stop" case.
  2. Built an example at /sandbox/repo/datadog-quickstart/
  3. Blocker: no credentials. Live mode can't be completed here. However, it's been proven that the code is correct via an in-process dry run.
  4. Pricing explanation.
  5. Easy to manage? Assessment.

Let me summarize concisely yet comprehensively.

Regarding "will it be easy for you to manage" — I should give an honest assessment:

  • Very easy for me to operate programmatically: well-designed OpenAPI-generated SDK, complete coverage, credentials via environment variables, idempotent REST, no local infrastructure needed for API work.
  • Caveats: the real operational burden isn't in the API but in cost/cardinality governance. Tag cardinality can silently explode billing. Also, many products (installing the Agent, APM tracing) require running the Agent on the host, which is out of scope for a pure API workflow. That part is infrastructure work I can't handle from here.
  • Also, billing is complex and consumption-based, so budgets/alerts should be set up.

Let me mention the blocker clearly at the top.

Keep it reasonably concise.

Done. Datadog does have a real developer workflow (REST API + official SDKs), so this wasn't a "stop" case — I built and verified the example.

Blocker (stated plainly)

There are no Datadog credentials in this environment. I checked env vars and config files; the only key present is pi's own gateway key. So live mode cannot be completed here. Add DD_API_KEY + DD_APP_KEY and it runs unchanged.

I did not work around this by faking success — instead I proved the code is correct against the real SDK, then confirmed live mode reaches api.datadoghq.com and fails only on auth (403/401).

What I built — /sandbox/repo/datadog-quickstart/

FilePurpose
dd_quickstart.pySubmit metric → estimate its cost → list dashboards → list monitors
fake_transport.pyIn-process fake HTTP layer for the credential-free dry run
README.mdPricing breakdown + run instructions
requirements.txtdatadog-api-client==2.61.0
python3 dd_quickstart.py --mock    # passes now, no creds/network
python3 dd_quickstart.py           # live, needs DD_API_KEY/DD_APP_KEY
ModeResult
--mockPasses, all assertions green, exit 0
Live, no keysPrints setup instructions, exit 2
Live, bogus keysReaches the real API, gets 403/401, reports cleanly, exit 3

--mock is not a local service stack — no server, container, port or subprocess. It patches the SDK's HTTP layer, so real serialization, auth-header injection, model validation and response parsing all execute; only the bytes are local. That caught two genuine contract bugs docs wouldn't have: the estimator path is /api/v2/metrics/{metric}/estimate (not /api/v2/series/estimate), and estimated_output_series is an int, not a list.

How pricing works

Most important: calling the API is free. Datadog doesn't meter API requests or SDK usage — you pay for what you ingest and what you monitor. This example costs nothing to run.

Prices below scraped from datadoghq.com/pricing today (US list prices; EU/US3/US5/Gov differ, and real contracts are negotiated).

  • Per-product, per-unit — per host, per GB, per million events, per 1,000 sessions.
  • Two rates everywhere: cheaper billed-annually vs on-demand monthly, which is ~20–40% higher (Infra Pro: $15 vs $18).
  • Capacity vs consumption — hosts are capacity; metrics/logs/traces are consumption. APM spans and logs are now pure consumption with no included allotments.
  • Additive — Infra + APM + Logs on one host is three separate charges.
ProductAnnualOn-demandUnit
Infrastructure Pro / Enterprise$15 / $23$18 / $27per host/mo
APM (with Infra) / standalone$31 / $36$36per host/mo
Log ingest$0.10$0.10per GB
Log index, 15-day$1.70$2.55per million events
Cloud SIEM$5$7.50per GB analyzed
Database Monitoring$70$84per DB host/mo

The trap in step 1: a "series" is a unique metric + tag-value combination, so tag cardinality multiplies — 2 tags with 3 and 4 values = 12 series, not 7. Pro allots 100 custom metrics/host; beyond that you pay per million. Adding user_id as a tag can accidentally create millions of billed series. Step 2 exists to price a metric before shipping it.

I also corrected three things mid-build rather than leave them wrong: $9/$13 is Universal Service Monitoring (not Cloud SIEM), Cloud SIEM is actually $5/$7.50 per GB analyzed, and I removed a bogus "indexing is 17× ingest" claim — the two use different units and aren't directly comparable.

Will this be easy for me to manage?

The API layer: yes, unusually easy. OpenAPI-generated SDK with full endpoint coverage, credentials from env vars, no hardcoded secrets, plain REST, and no local infrastructure needed for API work. Fully scriptable and CI-able, including a Usage Metering API for automated bill checks.

Two honest caveats:

  1. The hard part isn't the API, it's cost governance. Billing is multi-dimensional and consumption-based, so a careless tag can silently 10× the bill with nothing breaking. Budgets, spend alerts and cardinality checks need setting up deliberately — the failure mode is a surprise invoice, not an error.
  2. Most Datadog value needs an agent on your hosts. Infrastructure metrics, APM tracing and log collection require deploying datadog-agent/dd-trace to real infrastructure. That's outside a pure API workflow and outside what I can do from this sandbox — I built the API/SDK half only, as you asked.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge