pg_exporter 0.11.1

PostgreSQL metric exporter for Prometheus
Documentation
# GitHub Copilot Instructions for pg_exporter

## Project Overview
This is a PostgreSQL metrics exporter written in Rust. It collects metrics from PostgreSQL and exposes them in Prometheus format. Safety and reliability are paramount - all code must handle edge cases gracefully without panics.

## Language & Tooling
- **Language**: Rust (latest stable)
- **Formatting**: `cargo fmt --all -- --check`
- **Linting**: `cargo clippy --all-targets --all-features -- -D warnings`
- **Testing**: `just test` (runs clippy, fmt, and all tests)
- **Build Tool**: `just` command runner (see `.justfile`)

## Critical Safety Rules

### 1. Always Use `try_get()` Instead of `get()`
**NEVER** use `row.get()` - it panics on NULL or type mismatches.

```rust
// ❌ WRONG - Will panic on NULL
let value: i64 = row.get("column");

// ✅ CORRECT - Safe handling
let value: i64 = row.try_get("column").unwrap_or(0);
```

### 2. Always Cast SQL Numeric Columns
PostgreSQL NUMERIC type doesn't directly map to Rust types. Always use explicit casts.

```rust
// ❌ WRONG - Type mismatch
SELECT total_time FROM pg_stat_statements

// ✅ CORRECT - Explicit cast
SELECT total_time::double precision FROM pg_stat_statements
SELECT calls::bigint FROM pg_stat_statements
```

### 3. Always Check Denominator Before Division
Prevent division by zero in both SQL and Rust.

```rust
// ❌ WRONG - Division by zero
let ratio = hits / (hits + misses);

// ✅ CORRECT - Safe division
let total = hits + misses;
let ratio = if total > 0 { hits as f64 / total as f64 } else { 0.0 };
```

SQL version:
```sql
-- ✅ CORRECT
CASE WHEN (shared_blks_hit + shared_blks_read) > 0
     THEN shared_blks_hit::double precision / (shared_blks_hit + shared_blks_read)
     ELSE 0.0
END as cache_hit_ratio
```

### 4. Always Check Extension Availability
Handle missing extensions gracefully with `fetch_optional()`.

```rust
// ✅ CORRECT - Check extension exists
let result = sqlx::query("SELECT * FROM pg_stat_statements LIMIT 1")
    .fetch_optional(&pool)
    .await?;

if result.is_none() {
    warn!("pg_stat_statements extension not available");
    return Ok(());
}
```

## Code Style

### Comments
- Only comment code that needs clarification
- Don't over-comment - code should be self-documenting
- Use doc comments (`///`) for public APIs

### Error Handling
- Use `anyhow::Result` for application errors
- Log warnings for non-critical issues with `warn!()`
- Use `debug!()` for verbose operational info
- Never panic in production code

### Async Patterns
```rust
#[instrument(skip(self, pool))]
async fn collect(&self, pool: &PgPool) -> Result<()> {
    // Implementation
}
```

### Metrics Registration
All collectors must implement the `Collector` trait:
```rust
impl Collector for MyCollector {
    fn register(&self, registry: &Registry) -> Result<()> {
        registry.register(Box::new(self.metric.clone()))?;
        Ok(())
    }

    fn collect<'a>(&'a self, pool: &'a PgPool) -> BoxFuture<'a, Result<()>> {
        Box::pin(async move {
            // Implementation
        })
    }
}
```

## Testing Requirements

### Required Test Categories for New Collectors
Every collector **must** include these tests:

1. **Registration Test** - Verify metrics register without errors
2. **Extension Availability Test** - Handle missing extensions gracefully
3. **NULL Value Handling Test** - Test with NULL values, utility statements
4. **Type Conversion Test** - Verify SQL type conversions work
5. **Realistic Workload Test** - Test with actual data

### Test Setup
Tests run against local PostgreSQL on `localhost:5432`:
```rust
#[tokio::test]
async fn test_my_collector() {
    let pool = sqlx::postgres::PgPoolOptions::new()
        .max_connections(5)
        .connect("postgresql://postgres:postgres@localhost:5432/postgres")
        .await
        .expect("Failed to connect to test database");
    
    // Test implementation
}
```

### Edge Cases to Test
- NULL values in all columns
- Empty result sets
- Utility statements (VACUUM, ANALYZE)
- Zero values (division by zero)
- Missing extensions/tables
- Type mismatches

## Pre-Commit Workflow

Before committing:
```bash
# 1. Start PostgreSQL
just postgres

# 2. Verify test database setup
./scripts/setup-local-test-db.sh

# 3. Run all checks (clippy, fmt, tests)
just test

# 4. Commit (pre-commit hook runs automatically)
git commit
```

Install pre-commit hook:
```bash
cp scripts/pre-commit-hook.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```

## Git Commit Signing

**CRITICAL: This project requires signed commits!**

### DO NOT use --no-gpg-sign

❌ **WRONG:**
```bash
git commit --no-gpg-sign -m "message"
```

✅ **CORRECT:**
```bash
git commit -m "message"  # Uses configured signing automatically
```

### Configuration

The user has SSH commit signing configured:
```bash
git config --global commit.gpgsign true
git config --global gpg.format ssh
```

All commits will be automatically signed with SSH. The pre-commit hook and
branch protection will enforce this.

### Why This Matters

- Branch protection requires signed commits
- Bypassing signature breaks the workflow
- Always respect the user's signing configuration
- Never override with --no-gpg-sign

## Common Patterns

### Collector Structure
```rust
#[derive(Clone)]
pub struct MyCollector {
    metric: GaugeVec,
    counter: IntGaugeVec,
}

impl MyCollector {
    pub fn new() -> Self {
        let labels = vec!["label1", "label2"];
        
        let metric = GaugeVec::new(
            Opts::new("metric_name", "Help text")
                .namespace("postgres"),
            &labels,
        )
        .expect("metric_name");
        
        Self { metric }
    }
}
```

### Safe Row Extraction
```rust
let value: i64 = row.try_get("column").unwrap_or(0);
let text: String = row.try_get("text_col").unwrap_or_default();
let nullable: Option<i64> = row.try_get("nullable_col").ok();
```

### Tracing
```rust
use tracing::{debug, warn, instrument};

#[instrument(skip(pool))]
async fn my_function(pool: &PgPool) -> Result<()> {
    debug!("Starting operation");
    warn!("Non-critical issue detected");
    Ok(())
}
```

## Documentation References
- [CONTRIBUTING.md]../CONTRIBUTING.md - Complete development guide
- [tests/TESTING.md]../tests/TESTING.md - Testing patterns and examples

## Zero Tolerance for Panics
All code must gracefully handle:
- Missing extensions/tables
- NULL values
- Type mismatches
- Division by zero
- Empty result sets
- Utility statements

**If you're not sure, test it locally with `./scripts/setup-local-test-db.sh` before committing.**

---

**Remember**: Every production panic is a test we didn't write.