# Monk Rust SDK

Production-grade Rust client for the **Monk Events API** with automatic retries, exponential backoff, client-side buffering, and circuit breaking.

[![Crates.io](https://img.shields.io/crates/v/monk-sdk.svg)](https://crates.io/crates/monk-sdk)
[![CI](https://github.com/withaltas/monk-rust-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/withaltas/monk-rust-sdk/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## Features

- **Automatic retries** with exponential backoff and full jitter
- **Circuit breaker** to avoid hammering a degraded server
- **In-memory event buffer** with background batch flushing
- **Parallel concurrent flushes** (configurable worker pool)
- **Configurable backpressure** (`FailFast` or `Block`)
- **Flush failure callbacks** for alerting / dead-letter handling
- **Graceful shutdown** waits for all in-flight flushes to complete
- **Client-side validation** (fail fast, save a round-trip)
- **Idempotency key management** (auto-generated UUIDv4 when omitted)
- **Typed request/response models** with `serde`
- **TLS via rustls** (no OpenSSL dependency)

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
monk-sdk = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

Or via the command line:

```bash
cargo add monk-sdk
```

## Quick Start

```rust
use monk_sdk::{MonkClient, Event};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = MonkClient::builder()
        .base_url("https://events.monk.dev")
        .api_key("mk_live_abc123")
        .build()?;

    let resp = client.ingest(
        Event::builder()
            .customer_id("cust_123")
            .event_name("api_call")
            .property("tokens", 42)
            .build()?
    ).await?;

    println!("accepted: {}", resp.idempotency_key);
    Ok(())
}
```

## Usage

### Single Event Ingestion

Send a single event with automatic retries on transient failures:

```rust
let resp = client.ingest(
    Event::builder()
        .customer_id("cust_123")
        .event_name("api_call")
        .property("model", "gpt-4")
        .property("tokens", 1500)
        .build()?
).await?;
```

### Batch Ingestion

Send up to 10,000 events in a single request:

```rust
let events = vec![
    Event::builder()
        .customer_id("cust_123")
        .event_name("api_call")
        .property("tokens", 42)
        .build()?,
    Event::builder()
        .external_customer_id("ext_456")
        .event_name("storage_read")
        .property("bytes", 1024)
        .build()?,
];

let resp = client.ingest_batch(events).await?;
println!("ingested {} events", resp.count);
```

### Buffered Ingestion

Enqueue events into an in-memory buffer that automatically flushes in batches.
Multiple concurrent HTTP requests are dispatched in parallel:

```rust
for i in 0..100_000 {
    client.ingest_buffered(
        Event::builder()
            .customer_id("cust_123")
            .event_name("page_view")
            .property("page", format!("/page/{i}"))
            .build()?
    ).await?;
}

// Trigger an immediate flush
client.flush();

// Graceful shutdown (flushes remaining events, waits for in-flight requests)
client.close();
```

### Query Events

List ingested events with optional filters:

```rust
let events = client.list_events()
    .event_name("api_call")
    .customer_id("cust_123")
    .start("2025-01-01")
    .end("2025-01-31")
    .limit(100)
    .offset(0)
    .send()
    .await?;

for event in &events.events {
    println!("{}: {}", event.event_name, event.timestamp);
}
```

### Query Usage

Retrieve aggregated usage data:

```rust
let usage = client.query_usage("2025-01-01", "2025-01-31")
    .event_name("api_call")
    .send()
    .await?;

println!("total: {}", usage.total_count);
for bucket in &usage.breakdown {
    println!("  {}: {}", bucket.date, bucket.count);
}
```

### Health Checks

```rust
let health = client.health().await?;
println!("liveness: {}", health.status);

let ready = client.ready().await?;
println!("readiness: {}", ready.status);
```

## Configuration

### Custom Retry Policy

```rust
use std::time::Duration;
use monk_sdk::{MonkClient, RetryConfig};

let client = MonkClient::builder()
    .base_url("https://events.monk.dev")
    .api_key("mk_live_abc123")
    .timeout(Duration::from_secs(10))
    .retry(RetryConfig {
        base_delay: Duration::from_millis(200),
        max_delay: Duration::from_secs(10),
        max_retries: 3,
        deadline: Some(Duration::from_secs(60)),
        circuit_breaker_threshold: 5,
        circuit_breaker_cooldown: Duration::from_secs(15),
    })
    .build()?;
```

### Custom Buffer Configuration

```rust
use std::time::Duration;
use monk_sdk::{MonkClient, BufferConfig, BackpressureStrategy};

let client = MonkClient::builder()
    .base_url("https://events.monk.dev")
    .api_key("mk_live_abc123")
    .buffer(BufferConfig {
        capacity: 50_000,
        max_batch_size: 5_000,
        flush_interval: Duration::from_secs(1),
        max_concurrent_flushes: 8,
        backpressure: BackpressureStrategy::Block,
    })
    .build()?;
```

### Backpressure Strategies

| Strategy | Behaviour |
|---|---|
| `FailFast` (default) | Returns `MonkError::BufferFull` immediately when the buffer is at capacity. Callers must handle the error (retry, drop, etc.). |
| `Block` | Suspends the caller until space is available. Provides natural flow control — producers slow down to match the flush rate. Best for high-throughput pipelines where you never want to lose an event. |

### Flush Failure Callback

Register a handler that fires when a batch flush fails after all retries:

```rust
let client = MonkClient::builder()
    .base_url("https://events.monk.dev")
    .api_key("mk_live_abc123")
    .on_flush_failure(|count, error| {
        eprintln!("ALERT: dropped {count} events: {error}");
        // push to dead-letter queue, increment metrics, page on-call, etc.
    })
    .build()?;
```

### Defaults

| Setting | Default |
|---|---|
| HTTP timeout | 30s |
| Retry base delay | 100ms |
| Retry max delay | 30s |
| Max retries | 5 |
| Retry deadline | 120s |
| Circuit breaker threshold | 10 consecutive failures |
| Circuit breaker cooldown | 30s |
| Buffer capacity | 10,000 events |
| Max batch size | 10,000 events |
| Flush interval | 500ms |
| Max concurrent flushes | 4 |
| Backpressure strategy | `FailFast` |

## Error Handling

All methods return `Result<T, MonkError>`. The error enum covers:

| Variant | Description | Retryable |
|---|---|---|
| `Validation` | Bad input caught client-side | No |
| `Api` | Structured error from the API | Depends on status code |
| `Network` | DNS, connection, TLS failures | Yes |
| `Timeout` | Request exceeded deadline | Yes |
| `CircuitOpen` | Server assumed unavailable | No |
| `BufferFull` | In-memory buffer at capacity | No |
| `Serialization` | Failed to encode request body | No |

```rust
use monk_sdk::MonkError;

match client.ingest(event).await {
    Ok(resp) => println!("OK: {}", resp.idempotency_key),
    Err(MonkError::Api { status, code, message, .. }) => {
        eprintln!("API error {status} [{code}]: {message}");
    }
    Err(MonkError::CircuitOpen) => {
        eprintln!("server is down, backing off");
    }
    Err(e) => eprintln!("error: {e}"),
}
```

## Architecture

```
MonkClient (Clone, Arc-wrapped)
├── ClientInner
│   ├── reqwest::Client (connection pooling)
│   ├── base_url
│   └── RetryPolicy
│       └── CircuitBreaker (Closed → Open → HalfOpen)
└── BufferWorker (background tokio task)
    ├── mpsc channel (bounded, configurable capacity)
    ├── Semaphore-limited parallel flush pool
    ├── flush on interval OR batch size threshold
    ├── on_flush_failure callback
    └── graceful shutdown: drain + wait for in-flight
```

## License

MIT
