Technical architecture and design rationale for Pup CLI.
Benefits:
- Performance - Compiled binary with fast startup (~3ms vs ~45ms in Go)
- Cross-platform - Single binary for macOS, Linux, Windows
- Memory safety - No garbage collector, zero-cost abstractions
- Concurrency - Async/await with tokio runtime
- Small binaries - ~26MB stripped (31% smaller than Go version)
- Strong type system - Catch errors at compile time with rich enums
Tradeoffs:
- Steeper learning curve than Go
- Longer compile times
- Smaller ecosystem for some domains
Benefits:
- Industry standard - Most popular Rust CLI framework
- Rich features - Subcommands, flags, derive macros, help generation
- Shell completion - Built-in bash/zsh/fish completion via clap_complete
- Type safety - Derive-based argument parsing
- Documentation - Excellent docs and examples
Benefits:
- Configuration management - Handles YAML config files
- Serialization - Unified serialize/deserialize across JSON, YAML
- Derive macros - Zero-boilerplate struct serialization
- Performance - Fast parsing with compile-time code generation
OAuth2 advantages:
- Security - Short-lived tokens (1 hour) vs. long-lived keys
- Granular control - Per-installation revocation
- Scope-based - Request only necessary permissions
- User context - Actions as authenticated user
- Auditable - Better audit trail with user identity
API Keys still supported:
- Legacy compatibility
- Simpler for programmatic access
- No browser requirement
pup/
├── src/ # Rust source code
│ ├── main.rs # Entry point + clap command registration
│ ├── client.rs # Datadog API client wrapper
│ ├── config.rs # Configuration management
│ ├── formatter.rs # Output formatting (JSON, YAML, table)
│ ├── util.rs # Time parsing, validation
│ ├── useragent.rs # User agent + AI agent detection
│ ├── version.rs # Version information
│ ├── auth/ # Authentication logic
│ │ ├── mod.rs # Auth module
│ │ ├── oauth.rs # OAuth2 flow + PKCE
│ │ ├── dcr.rs # Dynamic Client Registration
│ │ ├── storage.rs # Token storage (keychain + file)
│ │ └── callback.rs # OAuth callback server
│ └── commands/ # Command implementations
│ ├── mod.rs # Command registration
│ ├── metrics.rs # Metrics domain
│ ├── monitors.rs # Monitors domain
│ └── ... # 53 command modules
├── Cargo.toml # Dependencies and metadata
├── tests/ # Integration tests
│ ├── compare/ # Output comparison tests
│ └── ...
└── docs/ # Documentation
Design principles:
src/commands/contains clap command definitions (thin layer)src/client.rs,src/auth/, etc. contain business logic (testable)#[cfg(test)]modules co-located with source for unit tests
Priority order:
- OS Keychain (primary) - Most secure
- macOS: Keychain (via
keyringcrate withapple-nativefeature) - Windows: Credential Manager
- Linux: Secret Service / Keyring (via
linux-nativefeature)
- macOS: Keychain (via
- Encrypted file (fallback) - When keychain unavailable
- Location:
~/.config/pup/tokens.enc - Encryption: AES-256-GCM (via
aes-gcmcrate) - Key derivation: Machine-specific data
- Location:
Why not plaintext?
- Security risk if file system compromised
- Better to require re-auth than expose tokens
Based on RFC 6749 and RFC 7636 (PKCE):
1. DCR Registration -> client_id, client_secret
2. PKCE Generation -> verifier, challenge
3. Authorization URL -> user approval
4. Callback Server -> receive code
5. Token Exchange -> access + refresh tokens
6. Secure Storage -> keychain or encrypted file
7. Auto Refresh -> before expiration
Why PKCE?
- Prevents authorization code interception
- Required for public clients (CLI)
- S256 method more secure than "plain"
Automatic refresh triggers:
- Token expires within 5 minutes
- API call with expired token
- Manual
pup auth refresh
Refresh flow:
- Check token expiration
- Use refresh_token to get new access_token
- Update stored tokens
- Retry original API call
Fallback:
- If refresh fails, prompt user to re-authenticate
Custom user agent identifies pup CLI and detects AI coding assistants:
Format:
pup/v0.1.0 (rust; os darwin; arch arm64) # Without agent
pup/v0.1.0 (rust; os darwin; arch arm64; ai-agent claude-code) # With agent
AI Agent Detection (src/useragent.rs):
Table-driven registry detecting 12 agents via environment variables. First match wins:
- Claude Code (
CLAUDECODE,CLAUDE_CODE), Cursor (CURSOR_AGENT), Codex (CODEX,OPENAI_CODEX), OpenCode (OPENCODE), Aider (AIDER), Cline (CLINE), Windsurf (WINDSURF_AGENT), GitHub Copilot (GITHUB_COPILOT), Amazon Q (AMAZON_Q,AWS_Q_DEVELOPER), Gemini Code Assist (GEMINI_CODE_ASSIST), Sourcegraph Cody (SRC_CODY), Generic Agent (AGENT) - Manual override:
FORCE_AGENT_MODE=1or--agentflag
Agent Mode Behavior (when detected):
--helpreturns structured JSON schema instead of text- Confirmation prompts auto-approved (prevents stdin hangs)
- API responses wrapped in metadata envelope (count, truncation, warnings)
- Errors returned as structured JSON with suggestions
See LLM_GUIDE.md for the complete agent guide.
Thin wrapper around datadog-api-client Rust crate:
pub struct Client {
config: datadog_api_client::configuration::Configuration,
}
impl Client {
pub async fn query_metrics(&self, query: &str) -> Result<Response> {
let api = datadog_api_client::apis::MetricsApi::new(&self.config);
let resp = api.query_metrics(query).await
.context("failed to query metrics")?;
Ok(resp)
}
}Benefits:
- Centralized error handling
- Authentication injection
- Consistent retry logic
- Easy mocking for tests
Error wrapping pattern (using anyhow):
use anyhow::{Context, Result};
fn query_metrics(query: &str) -> Result<Response> {
let resp = client.get(url)
.send()
.context("failed to query metrics")?;
Ok(resp)
}Never expose:
- API keys in error messages
- OAuth tokens in logs
- User credentials in output
Simple commands:
metrics
|-- query # pup metrics query
|-- list # pup metrics list
+-- get # pup metrics get
Nested commands:
rum
|-- apps
| |-- list # pup rum apps list
| +-- get # pup rum apps get
|-- metrics
| +-- get # pup rum metrics get
+-- sessions
+-- search # pup rum sessions search
Global flags available on all commands:
--config- Config file path--site- Datadog site--output- Output format (json, yaml, table)--verbose- Enable debug logging--yes- Skip confirmations
Domain-specific flags:
--from,--to- Time ranges (metrics, logs, traces)--query- Search query (logs, metrics, events)--tag- Tag filter (monitors, hosts)--limit- Result limit
JSON (default):
- Machine-readable
- Preserves all fields
- No truncation
YAML:
- Human-readable
- Preserves structure
- Good for config files
Table:
- Compact display (via
comfy-tablecrate) - Truncates long values
- Good for terminals
- Command-line flags (highest priority)
- Environment variables (
DD_*,PUP_*) - Config file (
~/.config/pup/config.yaml) - Default values (lowest priority)
Location: ~/.config/pup/config.yaml
# Authentication
site: datadoghq.com
# Output
output: json
verbose: false
# Safety
read_only: false
# Defaults
default_from: 1h
default_to: nowPup uses tokio for async I/O:
#[tokio::main]
async fn main() -> Result<()> {
// All API calls are async
let monitors = client.list_monitors().await?;
Ok(())
}Rate limiting:
- Respect Datadog API limits (depends on plan)
- Implement exponential backoff for retries
- Use connection pooling (reqwest default)
Rust's ownership system ensures:
- No garbage collection pauses
- Predictable memory usage
- Zero-cost abstractions for iterators and streaming
Protected against:
- Token theft (encrypted storage)
- Code interception (PKCE)
- CSRF attacks (state parameter)
- Man-in-the-middle (HTTPS only)
- Credential exposure (never log tokens)
Not protected against:
- Compromised OS (keychain access)
- Malicious CLI binary (user must trust source)
- Datadog account compromise (outside scope)
- Never commit secrets - Gitignore tokens, keys
- Encrypt at rest - Token storage encrypted
- Validate inputs - Prevent injection attacks
- Use HTTPS - All API calls over TLS
- Minimal scopes - Request only needed permissions