Skip to content

Commit afe8c15

Browse files
jwfingclaude
andcommitted
insta usage: show the org by default, --proj for a single project
Default `insta usage` to whole-org usage (org of the linked project) with a per-project cost breakdown; `--proj [id]` shows one project — the linked one, or a given project id. Both over the current billing cycle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 57a05a5 commit afe8c15

2 files changed

Lines changed: 42 additions & 15 deletions

File tree

src/commands/metrics.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,51 @@ export async function metrics(component: string, group: string | undefined, opts
2525
// Customer-facing name for each internal billing dimension (the platform stores RAM as `ram`).
2626
const DIMENSION_LABEL: Record<string, string> = { ram: 'memory' }
2727

28-
// insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) we charge
29-
// on, over a window. Shows the billed dimensions, not the raw provider (fly/neon) meters.
30-
export async function usage(opts: { from?: string; to?: string; json?: boolean }): Promise<void> {
31-
const api = await ApiClient.load()
32-
const p = await requireProject()
33-
const res = await api.request('GET', `/projects/${p.projectId}/usage${qs({ from: opts.from, to: opts.to })}`)
34-
if (opts.json) return printJson(res)
35-
// Window defaults to the current billing cycle. `to` is the exclusive next-cycle start, so show
36-
// the inclusive last day (to − 1 day) — e.g. an org created on the 5th reads "…-05 → …next-04".
28+
type Dim = { dimension: string; quantity: number; unit: string; costUsd?: number }
29+
30+
// Window line. Defaults to the current billing cycle; `to` is the exclusive next-cycle start, so
31+
// show the inclusive last day (to − 1 day) — e.g. an org created on the 5th reads "…-05 → …next-04".
32+
function cycleLine(res: { from: number; to: number }): string {
3733
const day = (sec: number) => new Date(sec * 1000).toISOString().slice(0, 10)
38-
info(`billing cycle ${day(res.from)}${day(res.to - 86400)}`)
39-
if (!res.dimensions?.length) return info('(no usage recorded)')
40-
for (const d of res.dimensions) {
34+
return `billing cycle ${day(res.from)}${day(res.to - 86400)}`
35+
}
36+
37+
function printDimensions(dims: Dim[]): void {
38+
for (const d of dims) {
4139
const label = DIMENSION_LABEL[d.dimension] ?? d.dimension
4240
const cost = d.costUsd != null ? ` ($${Number(d.costUsd).toFixed(4)})` : ''
4341
info(`${label}: ${d.quantity} ${d.unit}${cost}`)
4442
}
45-
info(`total: $${Number(res.totalCostUsd ?? 0).toFixed(4)}`)
43+
}
44+
45+
// insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the
46+
// current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj
47+
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw fly/neon meters.
48+
export async function usage(opts: { from?: string; to?: string; json?: boolean; proj?: string | boolean }): Promise<void> {
49+
const api = await ApiClient.load()
50+
const p = await requireProject()
51+
52+
if (opts.proj !== undefined && opts.proj !== false) {
53+
const projectId = typeof opts.proj === 'string' ? opts.proj : p.projectId
54+
const res = await api.request('GET', `/projects/${projectId}/usage${qs({ from: opts.from, to: opts.to })}`)
55+
if (opts.json) return printJson(res)
56+
info(cycleLine(res))
57+
if (!res.dimensions?.length) return info('(no usage recorded)')
58+
printDimensions(res.dimensions)
59+
return info(`total: $${Number(res.totalCostUsd ?? 0).toFixed(4)}`)
60+
}
61+
62+
// Default: the whole org (the linked project's org), with a per-project cost breakdown.
63+
const res = await api.request('GET', `/orgs/${p.orgId}/usage${qs({ from: opts.from, to: opts.to })}`)
64+
if (opts.json) return printJson(res)
65+
info(cycleLine(res))
66+
if (!res.org?.dimensions?.length) return info('(no usage recorded)')
67+
printDimensions(res.org.dimensions)
68+
info(`total: $${Number(res.org.totalCostUsd ?? 0).toFixed(4)}`)
69+
if (res.projects?.length) {
70+
info('by project:')
71+
for (const pr of res.projects) info(` ${pr.name}: $${Number(pr.totalCostUsd ?? 0).toFixed(4)}`)
72+
}
4673
}
4774

4875
// insta logs <db|compute> [group]

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ program.command('metrics <target> [group]').description('Service metrics (target
102102
program.command('logs <target> [group]').description('Service runtime logs (target: db|compute)')
103103
.option('--branch <b>').option('--limit <n>').option('--region <r>').option('--instance <i>').option('--json')
104104
.action(guard((target, group, o) => obs.logs(target, group, o)))
105-
program.command('usage').description('Resource usage aggregated by meter (with cost)')
106-
.option('--from <unix>').option('--to <unix>').option('--json')
105+
program.command('usage').description('Usage for the current billing cycle by billing dimension (org by default; --proj for one project)')
106+
.option('--from <unix>').option('--to <unix>').option('--proj [id]', 'show one project (the linked one, or a given id) instead of the whole org').option('--json')
107107
.action(guard((o) => obs.usage(o)))
108108
const bill = program.command('billing').description('Current billing cycle summary (tier / credit / used / overage)')
109109
.option('--org <id>', 'target org (default: linked project\'s org)').option('--json')

0 commit comments

Comments
 (0)