11# (C) Datadog, Inc. 2024-present
22# All rights reserved
33# Licensed under a 3-clause BSD style license (see LICENSE)
4- """CLI interface for DynamicD."""
4+ """CLI implementation for DynamicD."""
55
66from __future__ import annotations
77
88import os
99from typing import TYPE_CHECKING
1010
1111import click
12- import requests
1312
14- from ddev .cli .meta .scripts .dynamicd .constants import (
15- DEFAULT_DURATION_SECONDS ,
16- DEFAULT_METRICS_PER_BATCH ,
17- SCENARIOS ,
18- )
13+ from ddev .cli .meta .scripts ._dynamicd .constants import SCENARIOS
1914
2015if TYPE_CHECKING :
2116 from ddev .cli .application import Application
2217
2318
24- def validate_org (api_key : str , app_key : str | None , site : str ) -> tuple [bool , str , bool ]:
19+ def _validate_org (api_key : str , app_key : str | None , site : str ) -> tuple [bool , str , bool ]:
2520 """Validate API key and return (is_internal_org, org_name, key_valid).
2621
2722 Checks if the API key belongs to a Datadog internal org (HQ or Staging).
2823 Note: Org lookup requires an Application Key. If not provided, we can only
2924 validate the API key works but cannot determine the org name.
3025 """
26+ import requests
27+
3128 # First validate the API key
3229 try :
3330 resp = requests .get (
@@ -76,118 +73,11 @@ def validate_org(api_key: str, app_key: str | None, site: str) -> tuple[bool, st
7673 return False , f"(unexpected error: { type (e ).__name__ } : { e } )" , True
7774
7875
79- @click .command ("dynamicd" , short_help = "Generate realistic fake telemetry data using AI" )
80- @click .argument ("integration" )
81- @click .option (
82- "--scenario" ,
83- "-s" ,
84- type = click .Choice (list (SCENARIOS .keys ())),
85- default = None ,
86- help = "Scenario to simulate. If not provided, shows interactive menu." ,
87- )
88- @click .option (
89- "--duration" ,
90- "-d" ,
91- type = int ,
92- default = DEFAULT_DURATION_SECONDS ,
93- help = "Duration in seconds. Default: 0 (run forever until Ctrl+C)" ,
94- )
95- @click .option (
96- "--rate" ,
97- "-r" ,
98- type = int ,
99- default = DEFAULT_METRICS_PER_BATCH ,
100- help = f"Target metrics per batch (batches sent every 10s). Default: { DEFAULT_METRICS_PER_BATCH } " ,
101- )
102- @click .option (
103- "--save" ,
104- is_flag = True ,
105- help = "Save the generated script to the integration's fake_data/ directory" ,
106- )
107- @click .option (
108- "--show-only" ,
109- is_flag = True ,
110- help = "Only show the generated script, don't execute it" ,
111- )
112- @click .option (
113- "--timeout" ,
114- type = int ,
115- default = None ,
116- help = "Execution timeout in seconds (for testing). Default: no timeout" ,
117- )
118- @click .option (
119- "--all-metrics" ,
120- is_flag = True ,
121- help = "Generate ALL metrics from metadata.csv, not just dashboard metrics. Use for load testing." ,
122- )
123- @click .option (
124- "--sandbox/--no-sandbox" ,
125- default = True ,
126- help = "Run script in Docker container for isolation (default: enabled). Use --no-sandbox to run directly." ,
127- )
128- @click .pass_obj
129- def dynamicd (
130- app : Application ,
131- integration : str ,
132- scenario : str | None ,
133- duration : int ,
134- rate : int ,
135- save : bool ,
136- show_only : bool ,
137- timeout : int | None ,
138- all_metrics : bool ,
139- sandbox : bool ,
140- ):
141- """Generate realistic fake telemetry data for an integration using AI.
76+ def _get_api_keys (app : Application ) -> tuple [str , str ]:
77+ """Get and validate API keys from config/environment.
14278
143- DynamicD uses Claude to analyze your integration's metrics and generate
144- a sophisticated simulator that produces realistic, scenario-aware data.
145-
146- \b
147- Examples:
148- # Interactive scenario selection
149- ddev meta scripts dynamicd ibm_mq
150-
151- # Specific scenario
152- ddev meta scripts dynamicd ibm_mq --scenario incident
153-
154- # Save the script for later use
155- ddev meta scripts dynamicd ibm_mq --scenario healthy --save
156-
157- # Just show the generated script
158- ddev meta scripts dynamicd ibm_mq --show-only
79+ Returns (llm_api_key, dd_api_key) or aborts if not configured.
15980 """
160- from ddev .cli .meta .scripts .dynamicd .context_builder import build_context
161- from ddev .cli .meta .scripts .dynamicd .executor import (
162- execute_script ,
163- is_docker_available ,
164- save_script ,
165- validate_script_syntax ,
166- )
167- from ddev .cli .meta .scripts .dynamicd .generator import GeneratorError , generate_simulator_script
168-
169- # Get the integration
170- try :
171- intg = app .repo .integrations .get (integration )
172- except OSError :
173- app .abort (f"Unknown integration: { integration } " )
174-
175- # Check for metrics
176- if not intg .has_metrics :
177- app .abort (f"Integration '{ integration } ' has no metrics defined in metadata.csv" )
178-
179- # Validate numeric options
180- if duration < 0 :
181- app .abort ("Duration cannot be negative" )
182- if rate <= 0 :
183- app .abort ("Rate must be a positive number" )
184-
185- # Handle sandbox mode (default: enabled)
186- use_sandbox = sandbox
187- if use_sandbox and not is_docker_available ():
188- app .display_error ("Docker is not available. Install Docker or use --no-sandbox." )
189- app .abort ()
190-
19181 # Get LLM API key from config or environment variable
19282 llm_api_key = app .config .raw_data .get ("dynamicd" , {}).get ("llm_api_key" )
19383 if not llm_api_key :
@@ -208,13 +98,16 @@ def dynamicd(
20898 )
20999 app .abort ()
210100
211- # Get Datadog site and app key
212- dd_site = app .config .org .config .get ("site" , "datadoghq.com" )
213- dd_app_key = app .config .org .config .get ("app_key" )
101+ return llm_api_key , dd_api_key
102+
214103
215- # Validate org and warn if internal Datadog org
104+ def _validate_and_warn_internal_org (app : Application , dd_api_key : str , dd_app_key : str | None , dd_site : str ) -> None :
105+ """Validate Datadog org and warn if it's an internal Datadog org.
106+
107+ Aborts if the API key is invalid or user declines to continue for internal orgs.
108+ """
216109 app .display_info ("Validating Datadog API key..." )
217- is_internal_org , org_name , key_valid = validate_org (dd_api_key , dd_app_key , dd_site )
110+ is_internal_org , org_name , key_valid = _validate_org (dd_api_key , dd_app_key , dd_site )
218111
219112 if not key_valid :
220113 app .display_error (f"API key validation failed: { org_name } " )
@@ -225,11 +118,10 @@ def dynamicd(
225118 app .display_info ("" )
226119
227120 if is_internal_org :
228- app .display_warning ( "=" * 60 )
229- app .display_warning ("WARNING: You are about to send fake data to a Datadog internal org!" )
121+ app .display_header ( "WARNING" , line_style = "bold yellow" )
122+ app .display_warning ("You are about to send fake data to a Datadog internal org!" )
230123 app .display_warning (f" Org: { org_name } " )
231124 app .display_warning (f" Site: { dd_site } " )
232- app .display_warning ("=" * 60 )
233125 app .display_warning ("" )
234126 confirm = click .prompt (
235127 "Are you sure you want to continue? Type 'y' to proceed" ,
@@ -240,6 +132,58 @@ def dynamicd(
240132 app .display_info ("Aborted by user." )
241133 app .abort ()
242134
135+
136+ def run_dynamicd (
137+ app : Application ,
138+ integration : str ,
139+ scenario : str | None ,
140+ duration : int ,
141+ rate : int ,
142+ save : bool ,
143+ show_only : bool ,
144+ timeout : int | None ,
145+ all_metrics : bool ,
146+ sandbox : bool ,
147+ ) -> None :
148+ """Run the DynamicD command logic."""
149+ from ddev .cli .meta .scripts ._dynamicd .context_builder import build_context
150+ from ddev .cli .meta .scripts ._dynamicd .executor import (
151+ execute_script ,
152+ is_docker_available ,
153+ save_script ,
154+ validate_script_syntax ,
155+ )
156+ from ddev .cli .meta .scripts ._dynamicd .generator import GeneratorError , generate_simulator_script
157+
158+ # Validate integration
159+ try :
160+ intg = app .repo .integrations .get (integration )
161+ except OSError :
162+ app .abort (f"Unknown integration: { integration } " )
163+
164+ if not intg .has_metrics :
165+ app .abort (f"Integration '{ integration } ' has no metrics defined in metadata.csv" )
166+
167+ # Validate options
168+ if duration < 0 :
169+ app .abort ("Duration cannot be negative" )
170+ if rate <= 0 :
171+ app .abort ("Rate must be a positive number" )
172+
173+ # Validate sandbox availability
174+ use_sandbox = sandbox
175+ if use_sandbox and not is_docker_available ():
176+ app .display_error ("Docker is not available. Install Docker or use --no-sandbox." )
177+ app .abort ()
178+
179+ # Get and validate API keys
180+ llm_api_key , dd_api_key = _get_api_keys (app )
181+ dd_site = app .config .org .config .get ("site" , "datadoghq.com" )
182+ dd_app_key = app .config .org .config .get ("app_key" )
183+
184+ # Validate org and warn if internal
185+ _validate_and_warn_internal_org (app , dd_api_key , dd_app_key , dd_site )
186+
243187 # Interactive scenario selection if not provided
244188 if scenario is None :
245189 scenario = _select_scenario_interactive (app )
@@ -249,10 +193,7 @@ def dynamicd(
249193 # Type narrowing: scenario is guaranteed to be str after the above check
250194 assert scenario is not None
251195
252- app .display_info ("" )
253- app .display_info (f"╔{ '═' * 60 } ╗" )
254- app .display_info (f"║{ 'DynamicD - Smart Fake Data Generator' :^60} ║" )
255- app .display_info (f"╚{ '═' * 60 } ╝" )
196+ app .display_header ("DynamicD - Smart Fake Data Generator" )
256197 app .display_info ("" )
257198 app .display_info (f" Integration: { intg .display_name } " )
258199 app .display_info (f" Scenario: { scenario } " )
@@ -300,12 +241,9 @@ def on_status(msg: str) -> None:
300241
301242 # Show only mode
302243 if show_only :
303- app .display_info ("" )
304- app .display_info ("=" * 70 )
305- app .display_info ("GENERATED SCRIPT" )
306- app .display_info ("=" * 70 )
244+ app .display_header ("Generated Script" )
307245 click .echo (script )
308- app .display_info ( "=" * 70 )
246+ app .display_header ( "" )
309247 return
310248
311249 # Save if requested
0 commit comments