UNPKG

@limitrate/cli

Version:

CLI dashboard for LimitRate rate limiting inspection

344 lines (262 loc) 6.24 kB
# @limitrate/cli CLI dashboard and event storage for LimitRate. ## Installation The CLI is automatically available when you install any LimitRate package: ```bash npx limitrate inspect ``` Or install globally: ```bash pnpm add -g @limitrate/cli # or npm install -g @limitrate/cli ``` ## Commands ### `npx limitrate inspect` Launch the interactive dashboard to view real-time statistics: ```bash npx limitrate inspect ``` **Displays**: - Total API cost spent (last 48 hours) - Request count per endpoint - Top spenders by user - Cost-exceeded events - Rate-exceeded events - Recent blocked requests **Navigation**: - `↑/↓` — Scroll through list - `Tab` — Switch between tabs - `q` — Quit ### `npx limitrate clear` Clear all stored events: ```bash npx limitrate clear ``` This removes the SQLite database at `.limitrate/events.db`. ## Event Storage ### Automatic Storage When you use `@limitrate/express` with the `onEvent` handler, events are automatically saved to SQLite: ```typescript import { saveEvent } from '@limitrate/cli'; app.use(limitrate({ // ... config onEvent: (event) => { saveEvent(event); // Store for CLI dashboard // Your custom logic if (event.type === 'cost_exceeded') { console.log('Cost cap exceeded!', event); } } })); ``` ### Manual Storage You can also manually save events: ```typescript import { saveEvent } from '@limitrate/cli'; saveEvent({ timestamp: Date.now(), type: 'rate_exceeded', user: 'user-123', plan: 'free', endpoint: 'POST|/api/ask', ip: '1.2.3.4', value: 11, threshold: 10, window: '1m' }); ``` ### Storage Location Events are stored in SQLite at: ``` .limitrate/events.db ``` **Retention**: Events older than 48 hours are automatically pruned. ## Event Types ### `allowed` Request passed all rate limits and cost caps: ```typescript { timestamp: 1638360000000, type: 'allowed', user: 'user-123', plan: 'free', endpoint: 'POST|/api/ask', ip: '1.2.3.4' } ``` ### `rate_exceeded` User hit rate limit: ```typescript { timestamp: 1638360000000, type: 'rate_exceeded', user: 'user-123', plan: 'free', endpoint: 'POST|/api/ask', ip: '1.2.3.4', value: 11, // Current request count threshold: 10, // Limit window: '1m' // Time window } ``` ### `cost_exceeded` User hit cost cap: ```typescript { timestamp: 1638360000000, type: 'cost_exceeded', user: 'user-123', plan: 'free', endpoint: 'POST|/api/ask', ip: '1.2.3.4', value: 0.11, // Current cost ($) threshold: 0.10, // Cap ($) window: '1h', // Time window estimatedCost: 0.003 // Cost of this request } ``` ### `ip_blocked` IP on blocklist: ```typescript { timestamp: 1638360000000, type: 'ip_blocked', user: 'anonymous', plan: 'free', endpoint: 'POST|/api/ask', ip: '1.2.3.4' } ``` ## Programmatic API ### `saveEvent(event: LimitRateEvent): void` Save an event to SQLite: ```typescript import { saveEvent } from '@limitrate/cli'; saveEvent({ timestamp: Date.now(), type: 'rate_exceeded', user: 'user-123', plan: 'free', endpoint: 'POST|/api/ask', ip: '1.2.3.4', value: 11, threshold: 10, window: '1m' }); ``` ### `getEvents(filter?: EventFilter): LimitRateEvent[]` Query stored events: ```typescript import { getEvents } from '@limitrate/cli'; // Get all events from last 24 hours const events = getEvents({ since: Date.now() - 24 * 60 * 60 * 1000 }); // Get cost-exceeded events for a specific user const costEvents = getEvents({ type: 'cost_exceeded', user: 'user-123' }); // Get events for a specific endpoint const endpointEvents = getEvents({ endpoint: 'POST|/api/ask' }); ``` ### `clearEvents(): void` Clear all events: ```typescript import { clearEvents } from '@limitrate/cli'; clearEvents(); ``` ### `getStats(): DashboardStats` Get aggregated statistics: ```typescript import { getStats } from '@limitrate/cli'; const stats = getStats(); console.log(stats); // { // totalCost: 12.45, // totalRequests: 1234, // endpointCounts: { // 'POST|/api/ask': 567, // 'GET|/api/data': 667 // }, // topSpenders: [ // { user: 'user-123', cost: 5.67, requests: 345 }, // { user: 'user-456', cost: 3.21, requests: 234 } // ], // recentBlocked: [ // { timestamp: 1638360000000, user: 'user-789', reason: 'rate_exceeded' } // ] // } ``` ## Example Integration ```typescript import express from 'express'; import { limitrate } from '@limitrate/express'; import { saveEvent, getStats } from '@limitrate/cli'; const app = express(); app.use(limitrate({ identifyUser: (req) => req.user?.id || req.ip, identifyPlan: (req) => req.user?.plan || 'free', store: { type: 'redis', url: process.env.REDIS_URL }, policies: { free: { endpoints: { 'POST|/api/ask': { rate: { maxPerMinute: 10, actionOnExceed: 'block' }, cost: { estimateCost: (ctx) => 0.003, hourlyCap: 0.10, actionOnExceed: 'block' } } } } }, onEvent: (event) => { // Save to SQLite for CLI dashboard saveEvent(event); // Send alerts if (event.type === 'cost_exceeded') { console.log(`🚨 User ${event.user} exceeded cost cap`); } } })); // Admin endpoint to get stats app.get('/admin/stats', (req, res) => { const stats = getStats(); res.json(stats); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); console.log('View dashboard: npx limitrate inspect'); }); ``` ## Development ### Database Schema The SQLite database has a single `events` table: ```sql CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL, type TEXT NOT NULL, user TEXT NOT NULL, plan TEXT NOT NULL, endpoint TEXT NOT NULL, ip TEXT, value REAL, threshold REAL, window TEXT, estimated_cost REAL ); CREATE INDEX idx_timestamp ON events(timestamp); CREATE INDEX idx_type ON events(type); CREATE INDEX idx_user ON events(user); CREATE INDEX idx_endpoint ON events(endpoint); ``` ### Auto-Pruning Events older than 48 hours are automatically deleted on every write to keep the database small. ## License Apache-2.0