Meter usage per tenant
Record what each tenant uses, read it back for a usage page, count a retry once and send hourly totals to your billing provider.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will record API calls per tenant, show today's total and make a retried step count once. It takes about 15 minutes.
You need
- A MainSDK app with
tenancy(see build a multi-tenant app).
1. Declare the metric
meter:
driver: local
metrics:
api_calls: { unit: call, aggregate: sum }local is for development. In production use db or redis, or mainsdk check reports MAIN-METER-0006.
2. Record after the work succeeds
import { meter } from "mainsdk";
meter.record("api_calls", 1, { feature: "export" }); // returns { id, queued: true } at oncerecord never throws because the store is down: the record is buffered.
3. Read usage
import { meter } from "mainsdk";
const usage = await meter.usage({ metric: "api_calls", window: "month", groupBy: ["feature"] });
for (const g of usage.groups ?? []) console.log(g.key.feature, g.value);4. Count a retry once
import { meter } from "mainsdk";
export function countMinutes(jobId: string, minutes: number) {
meter.record("audio_minutes", minutes, { idempotencyKey: `transcribe:${jobId}` });
}Inside a workflow step, job or inbound webhook the key defaults from that context.
5. Test it
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { meter } from "mainsdk";
it("counts a retried record once", async () => {
createTestApp({ main: "./main.yaml" });
meter.record("api_calls", 1, { idempotencyKey: "req-1" });
meter.record("api_calls", 1, { idempotencyKey: "req-1" });
await meter.flush();
assert.equal((await meter.usage({ metric: "api_calls", window: "day" })).value, 1);
});Check it worked
The test passes with a value of 1. In the app, usage.stale is false when the store answered.
Troubleshooting
MAIN-METER-0001: the metric is not undermeter.metrics. Declare it.MAIN-METER-0004: usage came from the last known value because the store is unreachable (stale: true).MAIN-METER-0007: no store for the tenant's region. Addmeter.redis.<region>.
To bill by usage, see take a payment and the meter interface for meter.export.
Was this page helpful?