Skip to content

feat: pluggable usage storage backend for cluster support #7

@sadnow

Description

@sadnow

Context

Parent Issue: #5 (Distributed agent cluster)
Depends On: #6 (Resource monitoring)
Priority: HIGH (foundational for cluster)

Problem

Current UsageTracker persists to local JSON file (oh-my-opencode-usage.json) per machine. In a cluster, each node has its own usage data with no aggregation.

Goal

Centralize usage ingestion and storage so all nodes report to a single source of truth.

Implementation Plan

Phase 1: Pluggable Storage Interface

Create adapter pattern for UsageTracker:

interface StorageAdapter {
  init?(config: unknown): Promise<void>
  saveRecord(record: UsageRecord): Promise<void>
  getAllSummaries(): Promise<Record<string, ProviderUsageSummary>>
  getProviderTrends(provider: string, rangeDays: number): Promise<TrendData>
  getAllRecords(format?: string): Promise<UsageRecord[]>
  flush?(): Promise<void>
}

Adapters to implement:

  1. FileAdapter - Wraps current storage.ts logic (default, backward compatible)
  2. HttpIngestAdapter - Posts usage events to central service

Phase 2: Central Ingestion Service

Minimal HTTP server with endpoints:

  • POST /ingest/usage - Accept usage events from nodes
  • GET /api/usage/summary - Aggregated summaries (all providers)
  • GET /api/usage/provider/:provider/trends - Provider trends
  • GET /api/usage/export - Export as JSON/CSV

Storage: SQLite initially (simple, file-based), migrate to Postgres/TimescaleDB later

Phase 3: WebUI Integration

Modify WebUI routes to query central service when configured:

  • Check config: usage.backend === "http"
  • If HTTP: fetch from central service
  • If file: use local UsageTracker (backward compatible)

Files to Modify

New Files

  • src/features/usage-tracker/adapters/index.ts - Interface definition
  • src/features/usage-tracker/adapters/file-adapter.ts - Current behavior
  • src/features/usage-tracker/adapters/http-ingest-adapter.ts - HTTP client
  • src/server/ingest-service/index.ts - Central service
  • src/server/ingest-service/db.ts - SQLite schema/queries
  • src/server/ingest-service/routes.ts - API endpoints

Modified Files

  • src/features/usage-tracker/tracker.ts - Accept adapter in constructor
  • src/webui/server.ts - Route to central service when configured
  • src/config/schema.ts - Add usage backend config

Configuration

{
  "usage": {
    "backend": "file" | "http",  // Default: "file"
    "http": {
      "endpoint": "http://master-node:3001",
      "retryAttempts": 3,
      "localBufferSize": 1000  // Queue locally if service unavailable
    }
  }
}

Testing Strategy

  1. Unit tests: Adapter interface, file adapter (existing behavior)
  2. Integration test:
    • Start central service (in-memory SQLite)
    • Configure node with HttpIngestAdapter
    • Record usage events
    • Query central service, verify aggregation
  3. Load test: 5 nodes posting usage concurrently

Success Criteria

  • FileAdapter maintains exact current behavior (no regressions)
  • HttpIngestAdapter posts events to central service
  • Central service aggregates usage from multiple nodes
  • WebUI shows cluster-wide usage when configured
  • Graceful fallback if central service unavailable (local buffering)
  • All existing tests pass

Migration Path

  1. Deploy central ingestion service
  2. Update node configs to use HTTP backend
  3. Restart nodes (they start reporting to central service)
  4. WebUI automatically shows cluster-wide data

Backward compatible: Nodes without config continue using file storage

Future Enhancements

  • Postgres/TimescaleDB backend (better for time-series)
  • Authentication/authorization for ingestion endpoint
  • Compression for usage events (reduce network overhead)
  • Batch ingestion (reduce HTTP overhead)
  • Real-time streaming (WebSocket/SSE for live updates)

Related Issues


Priority: HIGH
Estimated Effort: 3-4 days

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions