Skip to content

wippy-projects/domonitor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

DigitalOcean Droplet Cost Monitor

A fault-tolerant cost monitoring system for DigitalOcean droplets using Wippy's actor model.

Features

  • Periodic Polling: Fetches droplet data from DigitalOcean API every 5 minutes
  • Cost Calculation: Computes monthly, daily, and hourly costs with detailed breakdown
  • Data Persistence: Stores cost history in SQLite database
  • HTTP API: REST endpoints to query current costs via http.endpoint
  • Threshold Alerting: Logs alerts when costs exceed configurable threshold
  • Fault Tolerance: Supervisor actor automatically restarts failed children
  • Actor Model: Clean separation of concerns via message passing

Quick Start

1. Configure Environment

Edit .env and add your DigitalOcean API token:

# Get your token from: https://cloud.digitalocean.com/account/api/tokens
DO_API_TOKEN=your_actual_token_here
DO_COST_THRESHOLD=100.00

Important: Never commit .env to version control! It's already in .gitignore.

2. Initialize Project

wippy init

3. Start Monitoring

wippy run butschster/domonitor

The supervisor will start all actors and begin monitoring.

HTTP API

The application exposes a REST API for querying cost data.

Base URL

http://localhost:8090/api/v1/domonitor

Endpoints

GET /costs/current

Returns the current calculated costs from memory store.

Entry: app.domonitor:endpoint_current_costs

Request:

curl http://localhost:8090/api/v1/domonitor/costs/current

Response (200 OK):

{
  "data": {
    "total_monthly": 150.00,
    "total_daily": 5.00,
    "total_hourly": 0.21,
    "droplet_count": 3,
    "droplets": [
      ...
    ]
  },
  "meta": {
    "source": "memory_store",
    "cached": true
  }
}

Response (404 Not Found):

{
  "error": "Not found",
  "message": "No cost data available yet. Wait for the next polling cycle."
}

Response (503 Service Unavailable):

{
  "error": "Service unavailable",
  "message": "Failed to connect to state store"
}

Architecture

Supervisor (do_supervisor)
    │
    ├── Persister (do_persister) ──→ SQLite DB + Memory Store
    │
    ├── Calculator (do_calculator)
    │       │
    │       └── costs_calculated ──→ Persister, Alerter
    │
    ├── Alerter (do_alerter) ──→ Log Alerts
    │
    └── Poller (do_poller)
            │
            └── droplets_fetched ──→ Calculator, Persister

Entry Registry

The project uses Wippy's entry registry system to define all components declaratively in _index.yaml files. Entries are identified by namespace:name format (e.g., app.domonitor:poller).

Entry Kinds Used

Kind Description Example Entry
ns.definition Namespace metadata app.domonitor:definition
env.variable Environment variable reference app.domonitor:do_api_token
env.storage.file File-based env storage (.env) app:env
env.storage.router Routes env reads across storages app:env_router
db.sql.sqlite SQLite database connection app:db
store.memory In-memory key-value store app:state_store
http.service HTTP server binding to port app:gateway
http.router Route prefix + middleware app.domonitor:api
http.endpoint Individual HTTP endpoint app.domonitor:endpoint_current_costs
function.lua Reusable Lua function app.domonitor:fetch_droplets
process.lua Actor process definition app.domonitor:poller
process.host Process execution host app.domonitor:processes
process.service Managed service with lifecycle app.domonitor:supervisor_service

Registry Files

  • src/_index.yaml - Shared infrastructure (database, store, HTTP gateway)
  • src/domonitor/_index.yaml - Application entries (actors, functions, endpoints)
  • src/domonitor/migrations/_index.yaml - Database migration entries

Finding Entries

Use registry.find() to query entries programmatically:

-- Find all Lua functions
local entries, err = registry.find({kind = "function.lua"})

-- Find endpoints in a namespace
local entries, err = registry.find({kind = "http.endpoint", namespace = "app.domonitor"})

Configuration

Environment Variables

The project supports two ways to configure environment variables:

  1. .env file (Recommended for development)
  2. OS environment variables (Recommended for production)

The .env file takes priority over OS environment variables.

Configuration via .env File

Create a .env file in the project root:

# DigitalOcean API Configuration
DO_API_TOKEN=your_digitalocean_token_here

# Cost Alerting
DO_COST_THRESHOLD=100.00

Configuration via OS Environment

export DO_API_TOKEN="your_digitalocean_api_token"
export DO_COST_THRESHOLD="100"

Available Variables

Variable Required Default Description
DO_API_TOKEN Yes - DigitalOcean API token
DO_COST_THRESHOLD No 100.00 Monthly cost alert threshold ($)

Environment Storage Architecture

The project uses a router pattern for environment storage defined in src/_index.yaml:

# File-based environment storage
- name: env
  kind: env.storage.file
  file_path: ".env"
  auto_create: true

# Router reads from file storage
- name: env_router
  kind: env.storage.router
  storages:
    - app:env  # .env file

Environment variables are accessed via entries in src/domonitor/_index.yaml:

- name: do_api_token
  kind: env.variable
  storage: app:env_router
  variable: DO_API_TOKEN
  • Reads: Routes through app:env_router to app:env (file storage)
  • Writes: Writes to .env file via env.set()
  • Security: .env file should have restricted permissions

Constants (in source)

File Constant Default Description
actors/poller.lua POLL_INTERVAL 5m API polling interval
actors/alerter.lua ALERT_COOLDOWN 3600 Seconds between alerts
actors/supervisor.lua HEALTH_CHECK_INTERVAL 30s Health check frequency

Viewing Data

Via HTTP API

The recommended way to access current cost data:

curl http://localhost:8090/api/v1/domonitor/costs/current

Via Memory Store (Lua)

Access the current_costs key directly in code:

local store = require("store")
local mem_store = store.get("app:state_store")
local costs_json = mem_store:get("current_costs")

Database Configuration

The database is configured in src/_index.yaml as app:db. By default it uses :memory: (in-memory SQLite). For persistent storage, change the file property:

- name: db
  kind: db.sql.sqlite
  file: "data/domonitor.db"  # File-based storage

Then query with:

sqlite3 data/domonitor.db "SELECT * FROM cost_history ORDER BY id DESC LIMIT 1;"

Project Structure

src/
├── _index.yaml                    # Shared infrastructure entries (app namespace)
│                                  #   - app:env (env.storage.file)
│                                  #   - app:env_router (env.storage.router)
│                                  #   - app:db (db.sql.sqlite)
│                                  #   - app:state_store (store.memory)
│                                  #   - app:gateway (http.service on :8090)
│
└── domonitor/
    ├── _index.yaml                # Application entries (app.domonitor namespace)
    │                              #   HTTP: api router, endpoint_current_costs
    │                              #   Functions: fetch_droplets, calculate_*
    │                              #   Actors: poller, calculator, persister, alerter, supervisor
    │                              #   Services: *_service with lifecycle management
    │
    ├── wippy.yaml                 # Module manifest
    ├── .env                       # Environment variables (gitignored)
    ├── README.md
    │
    ├── sdk/
    │   └── digitalocean.lua       # DO API client (fetch_droplets, fetch_droplet)
    │
    ├── calc/
    │   └── costs.lua              # Cost calculations (calculate_monthly_cost, calculate_daily_cost)
    │
    ├── handlers/
    │   └── costs.lua              # HTTP handler for GET /costs/current
    │
    ├── actors/
    │   ├── supervisor.lua         # Root supervisor - manages actor lifecycle
    │   ├── poller.lua             # Periodic API polling (5 min intervals)
    │   ├── calculator.lua         # Stateless cost computation
    │   ├── persister.lua          # DB + store writes
    │   └── alerter.lua            # Threshold-based notifications
    │
    └── migrations/
        ├── _index.yaml            # Migration entries
        └── 001_create_cost_history.lua

Troubleshooting

Actors Not Starting

  • Verify wippy.lock is current: wippy init
  • Check environment variables are set
  • Review logs for error messages

No Data in Database

  • Ensure migration ran: check for cost_history table
  • Verify DO API token is valid
  • Check network connectivity to DigitalOcean API

No Alerts

  • Verify costs exceed threshold
  • Check cooldown period (default 1 hour)
  • Review alerter logs for "suppressed by cooldown" messages

Supervision Issues

  • Supervisor logs health checks every 30 seconds
  • Check for "missing children detected" warnings
  • Review restart counts in health check logs

Development

Adding New Droplet Sizes

Edit calc/costs.lua and add to PRICING_TABLE:

local PRICING_TABLE = {
    -- ... existing entries
    ["new-size-slug"] = 99.00,
}

Changing Alert Behavior

The alerter logs structured warnings. To add webhooks or other notifications, modify actors/alerter.lua.

Manual Actor Restart

Send a message to the supervisor:

process.send(supervisor_pid, "restart_child", {name = "poller"})

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages