Skip to content

Latest commit

 

History

History
354 lines (274 loc) · 9.92 KB

File metadata and controls

354 lines (274 loc) · 9.92 KB

Architecture & Design Decisions

Technical architecture and design rationale for Pup CLI.

Technology Choices

Why Rust?

Benefits:

  1. Performance - Compiled binary with fast startup (~3ms vs ~45ms in Go)
  2. Cross-platform - Single binary for macOS, Linux, Windows
  3. Memory safety - No garbage collector, zero-cost abstractions
  4. Concurrency - Async/await with tokio runtime
  5. Small binaries - ~26MB stripped (31% smaller than Go version)
  6. 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

Why Clap?

Benefits:

  1. Industry standard - Most popular Rust CLI framework
  2. Rich features - Subcommands, flags, derive macros, help generation
  3. Shell completion - Built-in bash/zsh/fish completion via clap_complete
  4. Type safety - Derive-based argument parsing
  5. Documentation - Excellent docs and examples

Why Serde + serde_yaml?

Benefits:

  1. Configuration management - Handles YAML config files
  2. Serialization - Unified serialize/deserialize across JSON, YAML
  3. Derive macros - Zero-boilerplate struct serialization
  4. Performance - Fast parsing with compile-time code generation

Why OAuth2 over API Keys?

OAuth2 advantages:

  1. Security - Short-lived tokens (1 hour) vs. long-lived keys
  2. Granular control - Per-installation revocation
  3. Scope-based - Request only necessary permissions
  4. User context - Actions as authenticated user
  5. Auditable - Better audit trail with user identity

API Keys still supported:

  • Legacy compatibility
  • Simpler for programmatic access
  • No browser requirement

Project Structure

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

Authentication Architecture

Token Storage Strategy

Priority order:

  1. OS Keychain (primary) - Most secure
    • macOS: Keychain (via keyring crate with apple-native feature)
    • Windows: Credential Manager
    • Linux: Secret Service / Keyring (via linux-native feature)
  2. Encrypted file (fallback) - When keychain unavailable
    • Location: ~/.config/pup/tokens.enc
    • Encryption: AES-256-GCM (via aes-gcm crate)
    • Key derivation: Machine-specific data

Why not plaintext?

  • Security risk if file system compromised
  • Better to require re-auth than expose tokens

OAuth2 Flow

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"

Token Refresh Strategy

Automatic refresh triggers:

  • Token expires within 5 minutes
  • API call with expired token
  • Manual pup auth refresh

Refresh flow:

  1. Check token expiration
  2. Use refresh_token to get new access_token
  3. Update stored tokens
  4. Retry original API call

Fallback:

  • If refresh fails, prompt user to re-authenticate

User Agent & Agent Mode

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=1 or --agent flag

Agent Mode Behavior (when detected):

  • --help returns 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.

API Client Wrapper

Design Pattern

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 Handling

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

Command Structure

Patterns

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

Flag Consistency

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

Output Formatting

Three Formats

JSON (default):

  • Machine-readable
  • Preserves all fields
  • No truncation

YAML:

  • Human-readable
  • Preserves structure
  • Good for config files

Table:

  • Compact display (via comfy-table crate)
  • Truncates long values
  • Good for terminals

Configuration Management

Precedence Order

  1. Command-line flags (highest priority)
  2. Environment variables (DD_*, PUP_*)
  3. Config file (~/.config/pup/config.yaml)
  4. Default values (lowest priority)

Config File

Location: ~/.config/pup/config.yaml

# Authentication
site: datadoghq.com

# Output
output: json
verbose: false

# Safety
read_only: false

# Defaults
default_from: 1h
default_to: now

Performance Considerations

Async Runtime

Pup 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)

Memory Management

Rust's ownership system ensures:

  • No garbage collection pauses
  • Predictable memory usage
  • Zero-cost abstractions for iterators and streaming

Security Architecture

Threat Model

Protected against:

  1. Token theft (encrypted storage)
  2. Code interception (PKCE)
  3. CSRF attacks (state parameter)
  4. Man-in-the-middle (HTTPS only)
  5. Credential exposure (never log tokens)

Not protected against:

  • Compromised OS (keychain access)
  • Malicious CLI binary (user must trust source)
  • Datadog account compromise (outside scope)

Security Best Practices

  1. Never commit secrets - Gitignore tokens, keys
  2. Encrypt at rest - Token storage encrypted
  3. Validate inputs - Prevent injection attacks
  4. Use HTTPS - All API calls over TLS
  5. Minimal scopes - Request only needed permissions

References