| 1 | /** |
| 2 | * What a workspace's projects may send, and what past it costs. |
| 3 | * |
| 4 | * Every workspace has `FREE_TELEMETRY_BYTES_PER_MONTH` (1 GiB, a wrangler |
| 5 | * var; `0` is no cap) a month across all its projects, counted by the |
| 6 | * workspace's meter (./meter.ts). A workspace without the g1t plan is held |
| 7 | * to it a day at a time, as a thirtieth of it, so a burst cannot spend the |
| 8 | * month in an hour. Past a day's share, OTLP is answered `429` until |
| 9 | * midnight UTC, and Cloudflare's tail events of its apps keep their |
| 10 | * requests (what the charts are counted from) without their logs. When |
| 11 | * billing cannot say which plan the workspace is on, the plan it last |
| 12 | * said holds, and a workspace it never said anything about is free. |
| 13 | * |
| 14 | * A workspace on the plan is never refused: what all its projects send |
| 15 | * past the free amount is charged by the GB (the price book's |
| 16 | * `telemetry_gb`), reported to billing through the month (`note_pending`, |
| 17 | * source `telemetry`) and charged once the month is over. |
| 18 | * |
| 19 | * Bytes are what arrived: an OTLP body once unzipped, and a tail event's |
| 20 | * rows as they are kept. |
| 21 | */ |
| 22 | |
| 23 | export const DEFAULT_FREE_TELEMETRY_BYTES_PER_MONTH = 1024 * 1024 * 1024; |
| 24 | const DAYS_PER_MONTH = 30; |
| 25 | |
| 26 | /** A gigabyte, as Cloudflare bills and the price book prices `telemetry_gb`. */ |
| 27 | export const GB = 1_000_000_000; |
| 28 | |
| 29 | /** |
| 30 | * What a GB past the free amount costs g1t when the price book cannot be |
| 31 | * read: $8.50 (services/billing migration 0059 says how it is made up). |
| 32 | */ |
| 33 | export const TELEMETRY_MICROS_PER_GB = 8_500_000; |
| 34 | |
| 35 | /** The monthly allowance from its var: the default when unset or not a whole number; 0 is none. */ |
| 36 | export function freeMonthlyBytes(raw: string | undefined): number { |
| 37 | const text = (raw ?? "").trim(); |
| 38 | const value = Number(text); |
| 39 | return text !== "" && Number.isInteger(value) && value >= 0 ? value : DEFAULT_FREE_TELEMETRY_BYTES_PER_MONTH; |
| 40 | } |
| 41 | |
| 42 | /** A day's share of a monthly allowance; null for none. */ |
| 43 | export function dailyCap(monthly: number): number | null { |
| 44 | return monthly > 0 ? Math.ceil(monthly / DAYS_PER_MONTH) : null; |
| 45 | } |
| 46 | |
| 47 | /** Which plan a workspace is on, as far as the cap goes. */ |
| 48 | export type Standing = "free" | "paid" | "held"; |
| 49 | |
| 50 | /** |
| 51 | * A workspace's daily cap: a held workspace takes nothing (0), a free one |
| 52 | * its day's share, one on the plan none. Unknown is free. |
| 53 | */ |
| 54 | export function capOf(standing: Standing | null, daily: number | null): number | null { |
| 55 | if (standing === "held") return 0; |
| 56 | if (standing === "paid") return null; |
| 57 | return daily; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Whether `adding` more bytes today would go past `cap`. A cap of 0 takes |
| 62 | * nothing: a workspace g1t staff put a hold on (or suspended). |
| 63 | */ |
| 64 | export function overCap(today: number, adding: number, cap: number | null | undefined): boolean { |
| 65 | return cap != null && cap >= 0 && today + adding > cap; |
| 66 | } |
| 67 | |
| 68 | /** What a month's `bytes` cost g1t past the free amount, at `microsPerGb`, rounded up. */ |
| 69 | export function telemetryCost(bytes: number, freeBytes: number, microsPerGb: number): number { |
| 70 | const past = Math.max(0, bytes - Math.max(0, freeBytes)); |
| 71 | return Math.ceil((past / GB) * microsPerGb); |
| 72 | } |
| 73 | |
| 74 | /** How much was sent, for the Billing page: `2.40 GB sent, 1.07 GB free`. */ |
| 75 | export function telemetryDetail(bytes: number, freeBytes: number): string { |
| 76 | return `${(bytes / GB).toFixed(2)} GB sent, ${(freeBytes / GB).toFixed(2)} GB free`; |
| 77 | } |
| 78 | |
| 79 | /** The size of a row as it is kept: its JSON. */ |
| 80 | export function sizeOf(value: unknown): number { |
| 81 | return JSON.stringify(value)?.length ?? 0; |
| 82 | } |
| 83 | |
| 84 | /** What OTLP past the day's share is told; with a cap of 0, that the workspace is held. */ |
| 85 | export function overCapMessage(cap: number): string { |
| 86 | if (cap === 0) return "g1t staff put a hold on this project's workspace, so its telemetry is not taken. Write to hey@flagon.io if you think this is a mistake."; |
| 87 | const mb = Math.round(cap / (1024 * 1024)); |
| 88 | return `This project's workspace has sent its ${mb} MB for today across its projects, a workspace on the free plan's daily share of its monthly observability. Send less, or start the g1t plan; more is taken from 00:00 UTC.`; |
| 89 | } |