Phase 3: usage metering
usage_records — one metered observation per (deployment, resource,
period, granularity). resource_kind is the BILLING kind (droplet_hours,
spaces_gb_month, ...) so it joins straight to plan_items. sub_attribution
jsonb column present from day one for forward-compat (sub-billing) but
never written in phase 3.
metering_config — per-billing-kind granularity (daily default; flip to
hourly when fine-grained billing is wanted) + retention_days.
ArcadiaCloud.Metering:
- meter_day/1 — walks every deployment-attributed resource, maps its
inventory kind to a billing kind, records that day's usage. Idempotent
via the unique (deployment, resource, period, granularity) index.
- usage_for_period/3 — SUM(qty) GROUP BY billing kind over a date range,
returns the %{kind => qty} map the quote engine takes as :usage.
Uniform accrual model so everything downstream is just SUM(qty):
- droplet -> droplet_hours, 24/day when active (0 when off)
- spaces_bucket / snapshot / droplet_backup -> *_gb_month, size/days so
the month sums to GB-months
- dns_zone -> dns_zones, 1/days so the month sums to the zone count
MeteringWorker — Oban cron 01:10 UTC daily, meters the previous
complete day.
Smoke verified: a pilot deployment with 2 droplets + 2 DNS zones + 3
snapshots attributed; 10 days metered -> 70 usage records; aggregation
gave droplet_hours 480 (2x24x10), dns_zones 0.65 (2x10/31),
snapshot_gb_month 15.48; fed into the quote engine against Studio — all
within allowance so $50 base, no overage (correct for a light partial
month).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
46
priv/repo/migrations/20260520160000_create_metering.exs
Normal file
46
priv/repo/migrations/20260520160000_create_metering.exs
Normal file
@@ -0,0 +1,46 @@
|
||||
defmodule ArcadiaCloud.Repo.Migrations.CreateMetering do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
# Per-resource-kind metering granularity. Daily by default; flip a
|
||||
# kind to hourly when fine-grained billing is wanted.
|
||||
create table(:metering_config, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :resource_kind, :string, null: false
|
||||
add :granularity, :string, null: false, default: "daily"
|
||||
add :retention_days, :integer, null: false, default: 400
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
create unique_index(:metering_config, [:resource_kind])
|
||||
|
||||
# One metered observation per (deployment, resource, period, granularity).
|
||||
# resource_kind here is the BILLING kind (droplet_hours, spaces_gb_month,
|
||||
# ...) so it joins straight to plan_items.resource_kind.
|
||||
create table(:usage_records, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :deployment_id,
|
||||
references(:cloud_deployments, type: :binary_id, on_delete: :delete_all),
|
||||
null: false
|
||||
add :resource_id, references(:cloud_resources, type: :binary_id, on_delete: :nilify_all)
|
||||
add :resource_kind, :string, null: false
|
||||
add :period_start, :utc_datetime, null: false
|
||||
add :granularity, :string, null: false, default: "daily"
|
||||
add :qty, :decimal, null: false, default: 0
|
||||
# forward-compat: tenants opting into sub-billing later can attribute
|
||||
# usage to their own end-users without a schema migration. Never
|
||||
# written in phase 3.
|
||||
add :sub_attribution, :map
|
||||
|
||||
timestamps(type: :utc_datetime, updated_at: false)
|
||||
end
|
||||
|
||||
create unique_index(:usage_records, [:deployment_id, :resource_id, :period_start, :granularity],
|
||||
name: :usage_records_unique
|
||||
)
|
||||
|
||||
create index(:usage_records, [:deployment_id, :period_start])
|
||||
create index(:usage_records, [:resource_kind])
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user