diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..72c306d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "alert-manager-api", + "image": "mcr.microsoft.com/devcontainers/rust:1", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": true, + "configureZshAsDefaultShell": true + } + }, + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates", + "vadimcn.vscode-lldb", + "streetsidesoftware.code-spell-checker", + "usernamehw.errorlens" + ], + "settings": { + "rust-analyzer.checkOnSave.command": "clippy", + "rust-analyzer.checkOnSave.extraArgs": ["--all-features"], + "[rust]": { + "editor.formatOnSave": true + } + } + } + }, + "postCreateCommand": "cargo build", + "remoteUser": "vscode" +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d103bbe --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Code Owners +# These owners will be requested for review on pull requests + +* @rlgrpe diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7785f9a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: "[BUG] " +labels: bug +assignees: '' +--- + +## Description + +A clear and concise description of what the bug is. + +## Steps to Reproduce + +1. Use this code... +2. Call this method... +3. See error + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. + +## Environment + +- Rust version: [e.g., 1.75.0] +- Crate version: [e.g., 0.1.2] +- OS: [e.g., Ubuntu 22.04] +- Alertmanager version: [e.g., 0.27.0] + +## Additional Context + +Add any other context about the problem here. + +## Minimal Reproducible Example + +```rust +// Paste your code here +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11c4f2e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: "[FEATURE] " +labels: enhancement +assignees: '' +--- + +## Problem Statement + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Proposed Solution + +A clear and concise description of what you want to happen. + +## Alternatives Considered + +A clear and concise description of any alternative solutions or features you've considered. + +## Use Case + +Describe the use case that motivates this feature. + +## Additional Context + +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d77382c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,30 @@ +## Summary + +Brief description of the changes. + +## Changes + +- Change 1 +- Change 2 + +## Type of Change + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes + +## Related Issues + +Fixes #(issue number) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..197b929 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + +jobs: + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --all-targets --all-features -- -D warnings -D clippy::cognitive_complexity + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --all-features -- --show-output + + test-features: + name: Test Features + runs-on: ubuntu-latest + strategy: + matrix: + features: + - native-tls + - rustls-tls + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --no-default-features --features ${{ matrix.features }} + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@cargo-tarpaulin + - run: cargo tarpaulin --all-features --out xml + - uses: codecov/codecov-action@v4 + with: + files: cobertura.xml + fail_ci_if_error: false + + security: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo generate-lockfile + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + tech-debt: + name: Tech Debt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for TODO/FIXME + run: | + if grep -rn "TODO\|FIXME" --include="*.rs" src/; then + echo "::warning::Found TODO/FIXME comments in source code" + fi + + large-files: + name: Large Files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for large files + run: | + find . -type f -size +1M -not -path "./.git/*" | while read file; do + echo "::error file=$file::File exceeds 1MB" + exit 1 + done || true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..023ac9c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +name: Release + +on: + push: + tags: + - 'v*' + +env: + CARGO_TERM_COLOR: always + +jobs: + verify: + name: Verify Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Get version from tag + id: tag_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Get version from Cargo.toml + id: cargo_version + run: | + VERSION=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + + - name: Verify versions match + env: + TAG_VERSION: ${{ steps.tag_version.outputs.VERSION }} + CARGO_VERSION: ${{ steps.cargo_version.outputs.VERSION }} + run: | + if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then + echo "Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" + exit 1 + fi + + test: + name: Test + needs: verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test --all-features + + release: + name: Create Release + needs: [verify, test] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get previous tag + id: prev_tag + run: | + PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + echo "TAG=$PREV_TAG" >> $GITHUB_OUTPUT + + - name: Generate changelog + id: changelog + env: + PREV_TAG: ${{ steps.prev_tag.outputs.TAG }} + run: | + if [ -n "$PREV_TAG" ]; then + CHANGELOG=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s" --no-merges) + else + CHANGELOG=$(git log --pretty=format:"- %s" --no-merges) + fi + echo "CHANGELOG<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + body: | + ## Changes + + ${{ steps.changelog.outputs.CHANGELOG }} + + ## Installation + + Add to your `Cargo.toml`: + + ```toml + [dependencies] + alert-manager-api = { git = "https://github.com/rlgrpe/alert-manager-api", tag = "${{ github.ref_name }}" } + ``` + draft: false + prerelease: ${{ contains(github.ref, '-') }} diff --git a/.gitignore b/.gitignore index b169f3d..8ff0eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target .idea -Cargo.lock \ No newline at end of file +Cargo.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..264ffcb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +# Pre-commit hooks configuration for alert-manager-api +# Install: pre-commit install +# Run manually: pre-commit run --all-files +# Update hooks: pre-commit autoupdate + +repos: + # Rust code formatting with rustfmt + - repo: local + hooks: + - id: rustfmt + name: rustfmt + entry: cargo fmt -- + language: system + types: [rust] + pass_filenames: false + + # Rust linting with clippy + - repo: local + hooks: + - id: clippy + name: clippy + entry: cargo clippy --all-targets --all-features -- -D warnings + language: system + types: [rust] + pass_filenames: false + + # Common pre-commit hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + name: Trim trailing whitespace + - id: end-of-file-fixer + name: Fix end of file + - id: check-yaml + name: Check YAML + - id: check-json + name: Check JSON + - id: check-toml + name: Check TOML + - id: check-added-large-files + name: Check for large files + args: ['--maxkb=1000'] + - id: check-merge-conflict + name: Check merge conflicts + - id: detect-private-key + name: Detect private keys diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2339845 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,82 @@ +# Agents + +Instructions for AI agents working on this codebase. + +## Build Commands + +```bash +# Build the project +cargo build + +# Build with all features +cargo build --all-features + +# Build for release +cargo build --release +``` + +## Test Commands + +```bash +# Run all tests +cargo test + +# Run tests with output +cargo test -- --show-output + +# Run tests for specific feature +cargo test --no-default-features --features native-tls +cargo test --no-default-features --features rustls-tls +``` + +## Lint Commands + +```bash +# Check formatting +cargo fmt --all -- --check + +# Run clippy +cargo clippy --all-targets --all-features -- -D warnings + +# Run pre-commit hooks +pre-commit run --all-files +``` + +## Documentation + +```bash +# Generate documentation +cargo doc --all-features --no-deps + +# Open documentation +cargo doc --all-features --no-deps --open +``` + +## Project Structure + +``` +alert-manager-api/ +├── src/ +│ ├── lib.rs # Library entry point, public exports +│ ├── client.rs # AlertmanagerClient implementation +│ ├── types.rs # Alert, AlertSeverity types +│ └── errors.rs # Error types and handling +├── examples/ # Usage examples +│ ├── basic.rs +│ ├── batch_alerts.rs +│ ├── resolve_alert.rs +│ └── custom_middleware.rs +└── Cargo.toml # Dependencies and features +``` + +## Features + +- `native-tls` (default) - Uses system TLS +- `rustls-tls` - Uses rustls for TLS + +## Key Types + +- `AlertmanagerClient` - Main client for sending alerts +- `Alert` - Represents an alert with labels and annotations +- `AlertSeverity` - Enum for Critical, Warning, Info +- `AlertmanagerError` - Error type with retry hints diff --git a/Cargo.toml b/Cargo.toml index b5cfd51..ffc915c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.2" edition = "2021" description = "Alertmanager API client for pushing alerts" license = "MIT" +repository = "https://github.com/rlgrpe/alert-manager-api" +homepage = "https://github.com/rlgrpe/alert-manager-api" keywords = ["alertmanager", "prometheus", "monitoring", "alerts"] categories = ["api-bindings", "web-programming"] @@ -38,4 +40,5 @@ tracing = "0.1" [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -wiremock = "0.6" \ No newline at end of file +wiremock = "0.6" +reqwest-retry = "0.7" diff --git a/README.md b/README.md new file mode 100644 index 0000000..448992a --- /dev/null +++ b/README.md @@ -0,0 +1,223 @@ +# alert-manager-api + +A Rust async client library for [Prometheus Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/). + +## Features + +- Async/await API built on `tokio` and `reqwest` +- Builder pattern for constructing alerts +- Batch alert sending +- Alert resolution support +- Custom middleware support via `reqwest-middleware` +- Comprehensive error handling with retry hints +- `tracing` integration for observability +- TLS support (native-tls or rustls) + +## Installation + +Add to your `Cargo.toml`: + +```toml +[dependencies] +alert-manager-api = { git = "https://github.com/rlgrpe/alert-manager-api.git", tag = "v0.1.2" } +``` + +For rustls instead of native-tls: + +```toml +[dependencies] +alert-manager-api = { git = "https://github.com/rlgrpe/alert-manager-api.git", tag = "v0.1.2", default-features = false, features = ["rustls-tls"] } +``` + +## Quick Start + +```rust +use alert_manager_api::{AlertmanagerClient, Alert, AlertSeverity}; +use std::time::Duration; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a client + let client = AlertmanagerClient::new( + Url::parse("http://localhost:9093")?, + Duration::from_secs(10), + )?; + + // Build and send an alert + let alert = Alert::new("HighMemoryUsage") + .with_severity(AlertSeverity::Warning) + .with_label("service", "my-app") + .with_label("instance", "localhost:8080") + .with_summary("Memory usage is above 90%") + .with_description("The service is using more than 90% of available memory"); + + client.push_alert(alert).await?; + Ok(()) +} +``` + +## API Overview + +### AlertmanagerClient + +The main client for interacting with Alertmanager. + +```rust +// Create with default HTTP client +let client = AlertmanagerClient::new(url, timeout)?; + +// Create with custom middleware (for retry, logging, etc.) +let client = AlertmanagerClient::with_client(middleware_client, url); + +// Push a single alert +client.push_alert(alert).await?; + +// Push multiple alerts in one request +client.push_alerts(vec![alert1, alert2]).await?; +``` + +### Alert + +Represents an alert to send to Alertmanager. + +```rust +let alert = Alert::new("AlertName") + // Labels identify the alert (used for deduplication and routing) + .with_label("service", "api") + .with_label("env", "production") + .with_severity(AlertSeverity::Critical) + + // Annotations provide additional context + .with_summary("Brief summary") + .with_description("Detailed description") + .with_annotation("runbook_url", "https://wiki.example.com/runbook") + + // Optional: link back to alert source + .with_generator_url("http://prometheus:9090/graph?...") + + // Optional: custom timestamps + .with_starts_at(start_time) + .with_ends_at(end_time); +``` + +### AlertSeverity + +Predefined severity levels: + +```rust +AlertSeverity::Critical // "critical" +AlertSeverity::Warning // "warning" +AlertSeverity::Info // "info" +``` + +### Resolving Alerts + +To resolve (clear) an alert, send it with `ends_at` set: + +```rust +// Option 1: Use resolve() to set ends_at to now +let resolved = Alert::new("HighMemoryUsage") + .with_label("service", "my-app") + .resolve(); + +// Option 2: Set a specific end time +let resolved = Alert::new("HighMemoryUsage") + .with_label("service", "my-app") + .with_ends_at(chrono::Utc::now()); + +client.push_alert(resolved).await?; +``` + +**Note:** Labels must match the original alert exactly for resolution to work. + +## Error Handling + +The library provides detailed error types: + +```rust +use alert_manager_api::{AlertmanagerError, Result}; + +match client.push_alert(alert).await { + Ok(()) => println!("Alert sent"), + Err(e) => { + // Check if the error is retryable + if e.is_retryable() { + // Network errors, timeouts, 5xx responses + // Consider implementing retry logic + } + + match e { + AlertmanagerError::Api { status, message } => { + eprintln!("Alertmanager returned {}: {}", status, message); + } + AlertmanagerError::Request(e) => { + eprintln!("HTTP request failed: {}", e); + } + AlertmanagerError::Serialize(e) => { + eprintln!("Failed to serialize alert: {}", e); + } + AlertmanagerError::BuildHttpClient(e) => { + eprintln!("Failed to build HTTP client: {}", e); + } + } + } +} +``` + +## Alert Deduplication + +Alertmanager deduplicates alerts based on their **labels**. Two alerts with identical labels are considered the same alert: + +```rust +// These are the SAME alert (identical labels) +Alert::new("HighCPU").with_label("instance", "host1") +Alert::new("HighCPU").with_label("instance", "host1").with_summary("Different text") + +// These are DIFFERENT alerts (different labels) +Alert::new("HighCPU").with_label("instance", "host1") +Alert::new("HighCPU").with_label("instance", "host2") +``` + +## Custom Middleware + +For retry logic, logging, or other middleware: + +```rust +use reqwest_middleware::ClientBuilder; +use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff}; + +let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3); + +let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + +let alertmanager = AlertmanagerClient::with_client( + client, + Url::parse("http://localhost:9093")?, +); +``` + +## Examples + +See the [examples](./examples) directory for more usage patterns: + +- `basic.rs` - Simple alert sending +- `batch_alerts.rs` - Sending multiple alerts +- `resolve_alert.rs` - Resolving alerts +- `custom_middleware.rs` - Using retry middleware + +Run examples with: + +```bash +cargo run --example basic +``` + +## License + +MIT + +--- + +[GitHub Repository](https://github.com/rlgrpe/alert-manager-api) diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..2c7a5c7 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,38 @@ +//! Basic example: sending a single alert to Alertmanager. +//! +//! Run with: cargo run --example basic +//! +//! Requires Alertmanager running at http://localhost:9093 + +use alert_manager_api::{Alert, AlertSeverity, AlertmanagerClient}; +use std::time::Duration; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a client pointing to your Alertmanager instance + let client = AlertmanagerClient::new( + Url::parse("http://localhost:9093")?, + Duration::from_secs(10), + )?; + + // Build an alert using the builder pattern + let alert = Alert::new("HighMemoryUsage") + .with_severity(AlertSeverity::Warning) + .with_label("service", "my-application") + .with_label("instance", "localhost:8080") + .with_label("env", "production") + .with_summary("Memory usage is above 90%") + .with_description( + "The service 'my-application' on instance 'localhost:8080' \ + is using more than 90% of available memory. Consider scaling \ + or investigating memory leaks.", + ) + .with_generator_url("http://prometheus:9090/graph?g0.expr=process_resident_memory_bytes"); + + // Send the alert + client.push_alert(alert).await?; + + println!("Alert sent successfully!"); + Ok(()) +} diff --git a/examples/batch_alerts.rs b/examples/batch_alerts.rs new file mode 100644 index 0000000..fc3287a --- /dev/null +++ b/examples/batch_alerts.rs @@ -0,0 +1,49 @@ +//! Batch example: sending multiple alerts in a single request. +//! +//! Run with: cargo run --example batch_alerts +//! +//! Requires Alertmanager running at http://localhost:9093 + +use alert_manager_api::{Alert, AlertSeverity, AlertmanagerClient}; +use std::time::Duration; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = AlertmanagerClient::new( + Url::parse("http://localhost:9093")?, + Duration::from_secs(10), + )?; + + // Create multiple alerts + let alerts = vec![ + Alert::new("HighCPUUsage") + .with_severity(AlertSeverity::Warning) + .with_label("service", "api-gateway") + .with_label("instance", "prod-1") + .with_summary("CPU usage above 80%"), + Alert::new("HighCPUUsage") + .with_severity(AlertSeverity::Critical) + .with_label("service", "api-gateway") + .with_label("instance", "prod-2") + .with_summary("CPU usage above 95%"), + Alert::new("DiskSpaceLow") + .with_severity(AlertSeverity::Warning) + .with_label("service", "database") + .with_label("instance", "db-primary") + .with_summary("Disk space below 20%") + .with_description("Primary database server is running low on disk space."), + Alert::new("ServiceDown") + .with_severity(AlertSeverity::Critical) + .with_label("service", "payment-processor") + .with_label("instance", "prod-1") + .with_summary("Service is not responding") + .with_annotation("runbook_url", "https://wiki.example.com/runbooks/payment"), + ]; + + // Send all alerts in a single HTTP request + client.push_alerts(alerts).await?; + + println!("All alerts sent successfully!"); + Ok(()) +} diff --git a/examples/custom_middleware.rs b/examples/custom_middleware.rs new file mode 100644 index 0000000..edf5c4b --- /dev/null +++ b/examples/custom_middleware.rs @@ -0,0 +1,73 @@ +//! Custom middleware example: using reqwest-middleware for retry logic. +//! +//! Run with: cargo run --example custom_middleware +//! +//! Requires Alertmanager running at http://localhost:9093 +//! +//! This example requires additional dependencies: +//! ```toml +//! [dev-dependencies] +//! reqwest-retry = "0.7" +//! ``` + +use alert_manager_api::{Alert, AlertSeverity, AlertmanagerClient, AlertmanagerError}; +use reqwest_middleware::ClientBuilder; +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; +use std::time::Duration; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Configure retry policy + let retry_policy = ExponentialBackoff::builder() + .retry_bounds(Duration::from_millis(100), Duration::from_secs(5)) + .build_with_max_retries(3); + + // Build the base HTTP client + let base_client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .connect_timeout(Duration::from_secs(5)) + .build()?; + + // Wrap with retry middleware + let middleware_client = ClientBuilder::new(base_client) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + + // Create Alertmanager client with our custom middleware + let client = + AlertmanagerClient::with_client(middleware_client, Url::parse("http://localhost:9093")?); + + let alert = Alert::new("NetworkLatencyHigh") + .with_severity(AlertSeverity::Warning) + .with_label("service", "api-gateway") + .with_label("region", "us-east-1") + .with_summary("Network latency above threshold") + .with_description("P99 latency is above 500ms for the past 5 minutes."); + + // The middleware will automatically retry on transient failures + match client.push_alert(alert).await { + Ok(()) => println!("Alert sent successfully!"), + Err(e) => { + // At this point, retries have been exhausted + eprintln!("Failed to send alert after retries: {}", e); + + // You can still check if it was retryable for logging purposes + if e.is_retryable() { + eprintln!("This was a transient error (retries exhausted)"); + } + + match e { + AlertmanagerError::Api { status, message } => { + eprintln!("Alertmanager returned HTTP {}: {}", status, message); + } + AlertmanagerError::Request(req_err) => { + eprintln!("Request error: {}", req_err); + } + _ => {} + } + } + } + + Ok(()) +} diff --git a/examples/resolve_alert.rs b/examples/resolve_alert.rs new file mode 100644 index 0000000..82e4cdf --- /dev/null +++ b/examples/resolve_alert.rs @@ -0,0 +1,50 @@ +//! Resolve example: sending and then resolving an alert. +//! +//! Run with: cargo run --example resolve_alert +//! +//! Requires Alertmanager running at http://localhost:9093 + +use alert_manager_api::{Alert, AlertSeverity, AlertmanagerClient}; +use std::time::Duration; +use url::Url; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = AlertmanagerClient::new( + Url::parse("http://localhost:9093")?, + Duration::from_secs(10), + )?; + + // First, send an alert + let alert = Alert::new("DatabaseConnectionPoolExhausted") + .with_severity(AlertSeverity::Critical) + .with_label("service", "user-service") + .with_label("instance", "prod-1") + .with_label("database", "postgres-primary") + .with_summary("Connection pool exhausted") + .with_description("All database connections are in use. New requests will fail."); + + println!("Sending alert..."); + client.push_alert(alert).await?; + println!("Alert sent!"); + + // Simulate the issue being resolved + println!("\nWaiting 5 seconds to simulate issue resolution..."); + tokio::time::sleep(Duration::from_secs(5)).await; + + // Resolve the alert by sending it with ends_at set + // IMPORTANT: Labels must match exactly for the resolution to work + let resolved_alert = Alert::new("DatabaseConnectionPoolExhausted") + .with_severity(AlertSeverity::Critical) + .with_label("service", "user-service") + .with_label("instance", "prod-1") + .with_label("database", "postgres-primary") + .with_summary("Connection pool exhausted") + .resolve(); // This sets ends_at to the current time + + println!("Sending resolution..."); + client.push_alert(resolved_alert).await?; + println!("Alert resolved!"); + + Ok(()) +}