From a14bec89b856040262ad00553fcc6ac8fd0f22e8 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 13:05:38 -0400 Subject: [PATCH 1/7] feat(provision): add kongctl bootstrap scripts for OpenMeter catalog Replace the Go clearinghouse-bootstrap CLI with thin cross-platform kongctl scripts and a JSON catalog for idempotent Konnect metering setup. --- README.md | 10 +- openmeter-collector/provision/README.md | 93 ++++++ openmeter-collector/provision/bootstrap.ps1 | 262 ++++++++++++++++ openmeter-collector/provision/bootstrap.sh | 322 ++++++++++++++++++++ openmeter-collector/provision/catalog.json | 85 ++++++ 5 files changed, 770 insertions(+), 2 deletions(-) create mode 100644 openmeter-collector/provision/README.md create mode 100644 openmeter-collector/provision/bootstrap.ps1 create mode 100755 openmeter-collector/provision/bootstrap.sh create mode 100644 openmeter-collector/provision/catalog.json diff --git a/README.md b/README.md index 32bd28b..9a43adf 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,13 @@ To change the host signing port from `8081`, use a Compose override file. ## OpenMeter/Konnect bootstrap Provision meters, features, and the default pay-per-use plan before starting the collector. -Use the Go `clearinghouse-bootstrap` CLI or your existing Konnect setup. +Use the [`kongctl` bootstrap scripts](openmeter-collector/provision/README.md) or your existing Konnect setup. + +```bash +cd openmeter-collector/provision +./bootstrap.sh catalog +./bootstrap.sh customer demo-client demo-user "Demo User" +``` Creates: @@ -155,7 +161,7 @@ Signer computed_fee (wei) → clearinghouse_default_ppu subscription per customer ``` -Markup rules are defined in the bootstrap CLI catalog. Collector +Markup rules are defined in [`openmeter-collector/provision/catalog.json`](openmeter-collector/provision/catalog.json). Collector pipeline config: [`openmeter-collector/collector.yaml`](openmeter-collector/collector.yaml). The collector does not yet emit `billable_usd_micros` (phase 2); until then the billable meter stays empty while the catalog is ready. diff --git a/openmeter-collector/provision/README.md b/openmeter-collector/provision/README.md new file mode 100644 index 0000000..5b28462 --- /dev/null +++ b/openmeter-collector/provision/README.md @@ -0,0 +1,93 @@ +# OpenMeter / Konnect metering bootstrap + +Idempotent provisioning of the clearinghouse metering catalog (meters, features, +plan) and per-tenant customers, driven by [`kongctl`](https://developer.konghq.com/kongctl/) +against the Konnect Metering & Billing (OpenMeter) API. + +`kongctl` has no native meter resource yet ([Kong/kongctl#1334](https://github.com/Kong/kongctl/issues/1334)), +so these scripts use `kongctl api` — its authenticated passthrough to the Konnect REST API — +to drive `/v3/openmeter/*`. The catalog is data, defined in [`catalog.json`](catalog.json); +the scripts are thin and idempotent. + +## Prerequisites + +- `kongctl` and `jq` on `PATH` (the PowerShell script needs only `kongctl`). +- A Konnect Personal Access Token (`kpat_…`) with Metering & Billing access. + +## Configuration (env) + +Both scripts auto-load [`openmeter-collector/.env`](../.env) when present (override with +`ENV_FILE`). Put your Konnect PAT there as `OPENMETER_API_KEY` — no need to `source` the +file first (`source .env` does **not** export vars to child processes). + +| Variable | Purpose | Default | +| --- | --- | --- | +| `KONGCTL_DEFAULT_KONNECT_PAT` | Konnect PAT (preferred). Falls back to `OPENMETER_API_KEY`. | from `.env` | +| `OPENMETER_API_KEY` | Same PAT as the collector service. | from `.env` | +| `OPENMETER_URL` | Metering API base. | `https://us.api.konghq.com/v3/openmeter` | + +One-time setup: + +```bash +cp openmeter-collector/.env.example openmeter-collector/.env +# edit OPENMETER_API_KEY=kpat_… +``` + +## Usage + +```bash +cd openmeter-collector/provision + +# Catalog only — ensure meters, features, and the active plan. +./bootstrap.sh catalog + +# Provision one tenant customer (key = client_id:external_user_id). +./bootstrap.sh customer demo-client demo-user "Demo User" + +# Catalog + customer in one run; --subscribe also ensures a plan subscription. +./bootstrap.sh all demo-client demo-user "Demo User" --subscribe +``` + +Windows (PowerShell): + +```powershell +.\bootstrap.ps1 catalog +.\bootstrap.ps1 customer demo-client demo-user "Demo User" +.\bootstrap.ps1 all demo-client demo-user "Demo User" -Subscribe +``` + +Both scripts are safe to re-run: existing meters are left untouched; features missing +a meter link are recreated; plans are created and published when no active version exists. + +## What it provisions + +From [`catalog.json`](catalog.json): + +| Kind | Key | Notes | +| --- | --- | --- | +| Meter | `network_fee_usd_micros` | SUM of `$.network_fee_usd_micros` | +| Meter | `billable_usd_micros` | SUM of `$.billable_usd_micros` (interim = network fee until phase-2 markup) | +| Meter | `signed_ticket_count` | COUNT | +| Feature | `network_spend` | linked to `network_fee_usd_micros` meter | +| Feature | `billable_spend` | linked to `billable_usd_micros` meter | +| Plan | `clearinghouse_default_ppu` | usage-based rate card on `billable_spend` at $0.000001/unit (1 USD micro); created as draft then published | + +## Identity contract (important) + +The CloudEvent **`subject` is the compound `client_id:usage_subject`** (e.g. +`demo-client:demo-user`), which is also the customer key and its single `subject_key`. +OpenMeter attributes usage by exact subject match, and **forbids changing a customer's +`subject_keys` once it has an active subscription** — so the subject must be compound and +correct from creation. Break usage down per-tenant/user with the `client_id` / `usage_subject` +meter dimensions, not by changing the subject. The scripts therefore never mutate +`subject_keys` on existing customers; they warn if an existing customer is missing the +expected compound key. + +## Limitations + +- Customer lookup lists customers and exact-matches the key locally (the API `key` + filter is a partial match). For very large customer bases, add pagination. +- Features are immutable except for `unit_cost`; if an existing feature lacks a meter + link (e.g. created with an older bootstrap), the script deletes and recreates it. +- Subscriptions are best-effort with `--subscribe`; plan pricing changes require a new + plan version in Konnect (out of scope for this script). diff --git a/openmeter-collector/provision/bootstrap.ps1 b/openmeter-collector/provision/bootstrap.ps1 new file mode 100644 index 0000000..2a44fb2 --- /dev/null +++ b/openmeter-collector/provision/bootstrap.ps1 @@ -0,0 +1,262 @@ +#requires -version 5.1 +<# +.SYNOPSIS + Bootstrap the OpenMeter/Konnect metering catalog for the clearinghouse collector. + +.DESCRIPTION + Idempotent: creates only what is missing. Meters are immutable in OpenMeter, so this + script never updates or deletes them. Uses `kongctl api` against /v3/openmeter/*. + + Auth: $env:KONGCTL_DEFAULT_KONNECT_PAT (preferred) or $env:OPENMETER_API_KEY (Konnect kpat_…). + Endpoint: $env:OPENMETER_URL (default https://us.api.konghq.com/v3/openmeter). + +.EXAMPLE + .\bootstrap.ps1 catalog +.EXAMPLE + .\bootstrap.ps1 customer demo-client demo-user "Demo User" +.EXAMPLE + .\bootstrap.ps1 all demo-client demo-user "Demo User" -Subscribe +#> +[CmdletBinding()] +param( + [ValidateSet('catalog', 'customer', 'all')] + [string]$Command = 'catalog', + [string]$ClientId, + [string]$ExternalUserId, + [string]$DisplayName, + [switch]$Subscribe +) + +$ErrorActionPreference = 'Stop' +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$Catalog = if ($env:CATALOG) { $env:CATALOG } else { Join-Path $ScriptDir 'catalog.json' } + +function Die($m) { Write-Error $m; exit 1 } +function Info($m) { Write-Host $m } +function Warn($m) { Write-Warning $m } + +if (-not (Get-Command kongctl -ErrorAction SilentlyContinue)) { Die 'kongctl not found (https://developer.konghq.com/kongctl/)' } +if (-not (Test-Path $Catalog)) { Die "catalog not found: $Catalog" } + +# --- env file (openmeter-collector/.env) ----------------------------------- +$EnvFile = if ($env:ENV_FILE) { $env:ENV_FILE } else { Join-Path (Split-Path $ScriptDir -Parent) '.env' } +if (Test-Path $EnvFile) { + Get-Content $EnvFile | ForEach-Object { + if ($_ -match '^\s*#' -or $_ -match '^\s*$') { return } + if ($_ -match '^([^=]+)=(.*)$') { + $k = $Matches[1].Trim() + if (-not (Test-Path "Env:$k")) { Set-Item -Path "Env:$k" -Value $Matches[2] } + } + } +} + +# --- auth + endpoint ------------------------------------------------------- +$pat = if ($env:KONGCTL_DEFAULT_KONNECT_PAT) { $env:KONGCTL_DEFAULT_KONNECT_PAT } else { $env:OPENMETER_API_KEY } +if (-not $pat) { Die "no Konnect PAT — set KONGCTL_DEFAULT_KONNECT_PAT or OPENMETER_API_KEY in the environment or in $EnvFile" } +$env:KONGCTL_DEFAULT_KONNECT_PAT = $pat + +$omUrl = if ($env:OPENMETER_URL) { $env:OPENMETER_URL } else { 'https://us.api.konghq.com/v3/openmeter' } +$omUrl = $omUrl.TrimEnd('/') +$BASE = [regex]::Replace($omUrl, '(https?://[^/]+).*', '$1') +$PREFIX = [regex]::Replace($omUrl, 'https?://[^/]+', '') +if (-not $PREFIX) { $PREFIX = '/v3/openmeter' } + +$catalogObj = Get-Content -Raw $Catalog | ConvertFrom-Json + +# --- kongctl api helpers --------------------------------------------------- +function Kapi-Get($path) { + $out = & kongctl api get "$PREFIX$path" --base-url $BASE -o json 2>$null + if ($LASTEXITCODE -ne 0) { return $null } + return ($out | ConvertFrom-Json) +} +function Kapi-Send($method, $path, $bodyJson) { + $out = $bodyJson | & kongctl api $method "$PREFIX$path" --base-url $BASE -o json -f - + if ($LASTEXITCODE -ne 0) { throw "kongctl api $method $path failed" } + if ($out) { return ($out | ConvertFrom-Json) } else { return $null } +} +function Kapi-Delete($path) { + & kongctl api delete "$PREFIX$path" --base-url $BASE -o json 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { throw "kongctl api delete $path failed" } +} +function Plan-ConfigKey { + if ($catalogObj.plan -and $catalogObj.plan.key) { return $catalogObj.plan.key } + return $catalogObj.plan_key +} +function Meter-IdFor($meterKey) { + $m = Items (Kapi-Get '/meters') | Where-Object { $_.key -eq $meterKey } | Select-Object -First 1 + if (-not $m) { return $null } + return $m.id +} +function Feature-For($featureKey) { + return Items (Kapi-Get '/features') | Where-Object { $_.key -eq $featureKey } | Select-Object -First 1 +} +function Find-PlanByStatus($planKey, $status) { + return Items (Kapi-Get "/plans?filter[key]=$planKey&filter[status]=$status") | + Where-Object { $_.key -eq $planKey } | Select-Object -First 1 +} +function Feature-MeterKey($feature) { + if ($feature.meter_key) { return $feature.meter_key } + return $feature.meter_slug +} +function New-FeatureBody($key, $name, $meterId) { + return (@{ key = $key; name = $name; meter = @{ id = $meterId } } | ConvertTo-Json -Compress) +} +function Items($resp) { + if ($null -eq $resp) { return @() } + if ($resp.PSObject.Properties.Name -contains 'data') { return @($resp.data) } + return @($resp) +} + +# --- catalog --------------------------------------------------------------- +function Ensure-Meters { + $existing = (Items (Kapi-Get '/meters')).key + foreach ($m in $catalogObj.meters) { + if ($existing -contains $m.key) { Info "meter $($m.key) - exists"; continue } + $body = [ordered]@{ name = $m.name; key = $m.key; description = $m.description; event_type = $m.event_type; aggregation = $m.aggregation } + if ($m.value_property) { $body.value_property = $m.value_property } + if ($m.dimensions) { $body.dimensions = $m.dimensions } + Kapi-Send 'post' '/meters' ($body | ConvertTo-Json -Depth 6 -Compress) | Out-Null + Info "meter $($m.key) - created" + } +} +function Ensure-Features { + foreach ($f in $catalogObj.features) { + $meterKey = Feature-MeterKey $f + if (-not $meterKey) { Die "feature $($f.key) requires meter_key in catalog.json" } + $meterId = Meter-IdFor $meterKey + if (-not $meterId) { Die "meter $meterKey not found for feature $($f.key)" } + + $feat = Feature-For $f.key + if (-not $feat) { + Kapi-Send 'post' '/features' (New-FeatureBody $f.key $f.name $meterId) | Out-Null + Info "feature $($f.key) - created" + continue + } + + $linked = $feat.meter.id + if ($linked -eq $meterId) { Info "feature $($f.key) - exists"; continue } + + Warn "feature $($f.key) - exists without meter link; recreating" + try { Kapi-Delete "/features/$($feat.id)" } catch { } + Kapi-Send 'post' '/features' (New-FeatureBody $f.key $f.name $meterId) | Out-Null + Info "feature $($f.key) - recreated (meter: $meterKey)" + } +} +function Build-PlanBody { + $featMap = @{} + foreach ($feat in Items (Kapi-Get '/features')) { $featMap[$feat.key] = $feat.id } + $p = $catalogObj.plan + $phases = @() + foreach ($phase in $p.phases) { + $rateCards = @() + foreach ($rc in $phase.rate_cards) { + $rateCards += [ordered]@{ + key = $rc.key + name = $rc.name + feature = @{ id = $featMap[$rc.feature_key] } + billing_cadence = $rc.billing_cadence + price = $rc.price + } + } + $phases += [ordered]@{ key = $phase.key; name = $phase.name; rate_cards = $rateCards } + } + $body = [ordered]@{ + key = $p.key + name = $p.name + currency = $p.currency + billing_cadence = $p.billing_cadence + phases = $phases + } + if ($p.description) { $body.description = $p.description } + return ($body | ConvertTo-Json -Depth 8 -Compress) +} +function Publish-Plan($planId, $planKey) { + try { + $published = Kapi-Send 'post' "/plans/$planId/publish" '{}' + if ($published.status -eq 'active') { + Info "plan $planKey - published" + return $true + } + } + catch { } + Warn "plan $planKey - could not publish (ensure features have meter links)" + return $false +} +function Ensure-Plan { + $planKey = Plan-ConfigKey + if (-not $planKey) { Info 'plan - none configured'; return } + if (-not $catalogObj.plan) { Die 'catalog plan block missing - add .plan or remove plan_key' } + + if (Find-PlanByStatus $planKey 'active') { Info "plan $planKey - active"; return } + + $draft = Find-PlanByStatus $planKey 'draft' + if ($draft) { + Info "plan $planKey - draft exists, publishing" + Publish-Plan $draft.id $planKey | Out-Null + return + } + + $bodyJson = Build-PlanBody + $bodyObj = $bodyJson | ConvertFrom-Json + foreach ($phase in $bodyObj.phases) { + foreach ($rc in $phase.rate_cards) { + if (-not $rc.feature.id) { Die 'plan rate cards reference unknown features - run ensure_features first' } + } + } + $created = Kapi-Send 'post' '/plans' $bodyJson + Info "plan $planKey - created (draft)" + Publish-Plan $created.id $planKey | Out-Null +} +function Invoke-Catalog { + Info "== catalog ($BASE$PREFIX) ==" + Ensure-Meters; Ensure-Features; Ensure-Plan +} + +# --- customer -------------------------------------------------------------- +function Ensure-Customer($clientId, $externalUserId, $display, $subscribe) { + if (-not $clientId -or -not $externalUserId) { Die 'customer requires ' } + # CloudEvent subject = compound client_id:external_user_id = customer key = its only + # subject_key. OpenMeter forbids changing subject_keys on subscribed customers, so we + # set it at creation and never mutate it. + $compound = "$clientId`:$externalUserId" + if (-not $display) { $display = $compound } + + $cust = Items (Kapi-Get '/customers') | Where-Object { $_.key -eq $compound } | Select-Object -First 1 + if (-not $cust) { + $body = [ordered]@{ key = $compound; name = $display; usage_attribution = @{ subject_keys = @($compound) } } + $created = Kapi-Send 'post' '/customers' ($body | ConvertTo-Json -Depth 5 -Compress) + $id = $created.id + Info "customer $compound - created (subject: $compound)" + } + else { + $id = $cust.id + if ($cust.usage_attribution.subject_keys -contains $compound) { Info "customer $compound - up to date" } + else { + Warn "customer $compound exists but its subject_keys do not include '$compound'" + Warn " (OpenMeter blocks subject_key changes on subscribed customers - reconcile manually)" + } + } + if ($subscribe) { Ensure-Subscription $id $compound } +} + +function Ensure-Subscription($customerId, $label) { + $planKey = Plan-ConfigKey + if (-not $planKey) { Warn 'no plan_key in catalog; skipping subscription'; return } + $existing = Items (Kapi-Get "/subscriptions?customer_id=$customerId") | Where-Object { $_.customer_id -eq $customerId } + if ($existing) { Info "sub $label - exists"; return } + $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $body = [ordered]@{ customer_id = $customerId; plan_key = $planKey; active_from = $now } + try { + Kapi-Send 'post' '/subscriptions' ($body | ConvertTo-Json -Compress) | Out-Null + Info "sub $label - created on $planKey" + } + catch { Warn "sub $label - could not create subscription on $planKey (create manually if needed)" } +} + +# --- dispatch -------------------------------------------------------------- +switch ($Command) { + 'catalog' { Invoke-Catalog } + 'customer' { Ensure-Customer $ClientId $ExternalUserId $DisplayName $Subscribe.IsPresent } + 'all' { Invoke-Catalog; Ensure-Customer $ClientId $ExternalUserId $DisplayName $Subscribe.IsPresent } +} +Info 'done.' diff --git a/openmeter-collector/provision/bootstrap.sh b/openmeter-collector/provision/bootstrap.sh new file mode 100755 index 0000000..adfacbc --- /dev/null +++ b/openmeter-collector/provision/bootstrap.sh @@ -0,0 +1,322 @@ +#!/usr/bin/env bash +# +# Bootstrap the OpenMeter/Konnect metering catalog for the clearinghouse collector. +# +# Idempotent: creates only what is missing. Meters are immutable in OpenMeter, so +# this script never updates or deletes them — it only adds missing meters/features +# and (for customers) appends missing usage-attribution subject keys. +# +# Requires: kongctl (https://developer.konghq.com/kongctl/) and jq. +# Auth: KONGCTL_DEFAULT_KONNECT_PAT (preferred) or OPENMETER_API_KEY — a Konnect PAT (kpat_…). +# Endpoint: OPENMETER_URL (default https://us.api.konghq.com/v3/openmeter). +# +# Usage: +# ./bootstrap.sh catalog +# Ensure meters + features exist and the configured plan is present. +# +# ./bootstrap.sh customer [display_name] [--subscribe] +# Ensure an OpenMeter customer keyed : exists with +# subject_keys = [ (bare, matches the CloudEvent subject), +# : (compound, tenant-scoped)]. +# --subscribe also ensures a subscription on the catalog plan (best-effort). +# +# ./bootstrap.sh all [display_name] [--subscribe] +# catalog + customer in one run. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CATALOG="${CATALOG:-$SCRIPT_DIR/catalog.json}" + +die() { printf 'error: %s\n' "$*" >&2; exit 1; } +info() { printf '%s\n' "$*" >&2; } +warn() { printf 'warning: %s\n' "$*" >&2; } + +command -v kongctl >/dev/null 2>&1 || die "kongctl not found (https://developer.konghq.com/kongctl/)" +command -v jq >/dev/null 2>&1 || die "jq not found" +[ -f "$CATALOG" ] || die "catalog not found: $CATALOG" + +# --- env file (openmeter-collector/.env) ----------------------------------- +# Plain `source .env` does not export vars to child processes; load here so +# `./bootstrap.sh catalog` works without `set -a; source …`. +ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/../.env}" +if [ -f "$ENV_FILE" ]; then + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +fi + +# --- auth + endpoint ------------------------------------------------------- +PAT="${KONGCTL_DEFAULT_KONNECT_PAT:-${OPENMETER_API_KEY:-}}" +if [ -z "$PAT" ]; then + die "no Konnect PAT — set KONGCTL_DEFAULT_KONNECT_PAT or OPENMETER_API_KEY in the environment or in $ENV_FILE" +fi +export KONGCTL_DEFAULT_KONNECT_PAT="$PAT" + +OPENMETER_URL="${OPENMETER_URL:-https://us.api.konghq.com/v3/openmeter}" +OPENMETER_URL="${OPENMETER_URL%/}" +BASE="$(printf '%s' "$OPENMETER_URL" | sed -E 's#(https?://[^/]+).*#\1#')" +PREFIX="$(printf '%s' "$OPENMETER_URL" | sed -E 's#https?://[^/]+##')" +[ -n "$PREFIX" ] || PREFIX="/v3/openmeter" + +# --- kongctl api helpers (return response body as JSON on stdout) ---------- +kapi_get() { kongctl api get "$PREFIX$1" --base-url "$BASE" -o json; } +kapi_post() { kongctl api post "$PREFIX$1" --base-url "$BASE" -o json -f -; } +kapi_put() { kongctl api put "$PREFIX$1" --base-url "$BASE" -o json -f -; } +kapi_delete() { kongctl api delete "$PREFIX$1" --base-url "$BASE" -o json; } + +plan_config_key() { jq -r '.plan.key // .plan_key // empty' "$CATALOG"; } + +meter_id_for() { + kapi_get /meters | jq -r --arg k "$1" '(.data // .)[] | select(.key == $k) | .id' +} + +feature_for() { + kapi_get /features | jq -c --arg k "$1" '(.data // .)[] | select(.key == $k)' +} + +find_plan_by_status() { + local plan_key="$1" status="$2" + kapi_get "/plans?filter[key]=${plan_key}&filter[status]=${status}" \ + | jq -c --arg k "$plan_key" '(.data // .)[] | select(.key == $k)' | head -n 1 +} + +# --- catalog --------------------------------------------------------------- +ensure_meters() { + local existing + existing="$(kapi_get /meters | jq -r '(.data // .)[].key')" + while IFS= read -r m; do + [ -n "$m" ] || continue + local key; key="$(jq -r '.key' <<<"$m")" + if printf '%s\n' "$existing" | grep -qxF "$key"; then + info "meter $key — exists" + continue + fi + local body + body="$(jq '{name, key, description, event_type, aggregation} + + (if .value_property then {value_property} else {} end) + + (if .dimensions then {dimensions} else {} end)' <<<"$m")" + printf '%s' "$body" | kapi_post /meters >/dev/null + info "meter $key — created" + done < <(jq -c '.meters[]' "$CATALOG") +} + +feature_meter_key() { jq -r '.meter_key // .meter_slug // empty' <<<"$1"; } + +create_feature() { + local key="$1" name="$2" meter_id="$3" + local body + body="$(jq -n --arg key "$key" --arg name "$name" --arg mid "$meter_id" \ + '{key:$key, name:$name, meter:{id:$mid}}')" + printf '%s' "$body" | kapi_post /features >/dev/null +} + +ensure_features() { + while IFS= read -r f; do + [ -n "$f" ] || continue + local key meter_key meter_id feat_rec feat_id linked_meter body + key="$(jq -r '.key' <<<"$f")" + meter_key="$(feature_meter_key "$f")" + [ -n "$meter_key" ] || die "feature $key requires meter_key in catalog.json" + meter_id="$(meter_id_for "$meter_key")" + [ -n "$meter_id" ] || die "meter $meter_key not found for feature $key" + + feat_rec="$(feature_for "$key")" + if [ -z "$feat_rec" ]; then + create_feature "$key" "$(jq -r '.name' <<<"$f")" "$meter_id" + info "feature $key — created" + continue + fi + + feat_id="$(jq -r '.id' <<<"$feat_rec")" + linked_meter="$(jq -r '.meter.id // empty' <<<"$feat_rec")" + if [ "$linked_meter" = "$meter_id" ]; then + info "feature $key — exists" + continue + fi + + warn "feature $key — exists without meter link; recreating" + kapi_delete "/features/$feat_id" >/dev/null 2>&1 || true + create_feature "$key" "$(jq -r '.name' <<<"$f")" "$meter_id" + info "feature $key — recreated (meter: $meter_key)" + done < <(jq -c '.features[]' "$CATALOG") +} + +build_plan_body() { + local feat_map + feat_map="$(kapi_get /features | jq '[(.data // .)[] | {(.key): .id}] | add')" + jq --argjson feats "$feat_map" ' + .plan as $p + | { + key: $p.key, + name: $p.name, + description: ($p.description // empty), + currency: $p.currency, + billing_cadence: $p.billing_cadence, + phases: [ + $p.phases[] + | { + key, + name, + rate_cards: [ + .rate_cards[] + | { + key, + name, + feature: { id: $feats[.feature_key] }, + billing_cadence, + price + } + ] + } + ] + } + | if .description == "" then del(.description) else . end + ' "$CATALOG" +} + +publish_plan() { + local plan_id="$1" plan_key="$2" + if printf '{}' | kapi_post "/plans/$plan_id/publish" 2>/dev/null \ + | jq -e '.status == "active"' >/dev/null; then + info "plan $plan_key — published" + return 0 + fi + warn "plan $plan_key — could not publish (ensure features have meter links)" + return 1 +} + +ensure_plan() { + local plan_key + plan_key="$(plan_config_key)" + [ -n "$plan_key" ] || { info "plan — none configured"; return 0; } + jq -e '.plan' "$CATALOG" >/dev/null 2>&1 \ + || die "catalog plan block missing — add .plan or remove plan_key" + + if [ -n "$(find_plan_by_status "$plan_key" active)" ]; then + info "plan $plan_key — active" + return 0 + fi + + local draft draft_id body plan_id + draft="$(find_plan_by_status "$plan_key" draft)" + if [ -n "$draft" ]; then + draft_id="$(jq -r '.id' <<<"$draft")" + info "plan $plan_key — draft exists, publishing" + publish_plan "$draft_id" "$plan_key" || true + return 0 + fi + + body="$(build_plan_body)" + if printf '%s' "$body" | jq -e '[ + .phases[].rate_cards[].feature.id + | select(. == null or . == "") + ] | length == 0' >/dev/null; then + : + else + die "plan rate cards reference unknown features — run ensure_features first" + fi + + plan_id="$(printf '%s' "$body" | kapi_post /plans | jq -r '.id')" + info "plan $plan_key — created (draft)" + publish_plan "$plan_id" "$plan_key" || true +} + +cmd_catalog() { + info "== catalog ($BASE$PREFIX) ==" + ensure_meters + ensure_features + ensure_plan +} + +# --- customer -------------------------------------------------------------- +# Find a customer by exact key. NOTE: the list filter is a partial match, so we +# fetch and exact-match locally. For very large customer bases add pagination. +find_customer() { + kapi_get "/customers" | jq -c --arg k "$1" '(.data // .)[] | select(.key == $k)' | head -n 1 +} + +ensure_customer() { + local client_id="$1" external_user_id="$2" display="$3" subscribe="$4" + [ -n "$client_id" ] && [ -n "$external_user_id" ] || die "customer requires " + # The CloudEvent subject is the compound client_id:external_user_id (globally + # unique, tenant-scoped). It is also the customer key and its only subject_key. + # OpenMeter forbids changing subject_keys once a customer has an active + # subscription, so we set it correctly at creation and never mutate it. + local compound="$client_id:$external_user_id" + [ -n "$display" ] || display="$compound" + + local cust; cust="$(find_customer "$compound")" + local id + if [ -z "$cust" ]; then + local body + body="$(jq -n --arg key "$compound" --arg name "$display" \ + '{key:$key, name:$name, usage_attribution:{subject_keys:[$key]}}')" + id="$(printf '%s' "$body" | kapi_post /customers | jq -r '.id')" + info "customer $compound — created (subject: $compound)" + else + id="$(jq -r '.id' <<<"$cust")" + if jq -e --arg c "$compound" '(.usage_attribution.subject_keys // []) | index($c)' <<<"$cust" >/dev/null; then + info "customer $compound — up to date" + else + warn "customer $compound exists but its subject_keys do not include '$compound'" + warn " (OpenMeter blocks subject_key changes on subscribed customers — reconcile manually)" + fi + fi + + [ "$subscribe" = "1" ] && ensure_subscription "$id" "$compound" || true +} + +# Best-effort subscription on the catalog plan. Skips if the customer already has one. +ensure_subscription() { + local customer_id="$1" label="$2" + local plan_key; plan_key="$(plan_config_key)" + [ -n "$plan_key" ] || { warn "no plan_key in catalog; skipping subscription"; return 0; } + + local existing + existing="$(kapi_get "/subscriptions?customer_id=$customer_id" 2>/dev/null \ + | jq -r --arg c "$customer_id" '(.data // .)[]? | select(.customer_id == $c) | .id' 2>/dev/null || true)" + if [ -n "$existing" ]; then + info "sub $label — exists" + return 0 + fi + local now body + now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + body="$(jq -n --arg c "$customer_id" --arg p "$plan_key" --arg t "$now" \ + '{customer_id:$c, plan_key:$p, active_from:$t}')" + if printf '%s' "$body" | kapi_post /subscriptions >/dev/null 2>&1; then + info "sub $label — created on $plan_key" + else + warn "sub $label — could not create subscription on $plan_key (create manually if needed)" + fi +} + +# --- arg parsing ----------------------------------------------------------- +SUBSCRIBE=0 +ARGS=() +for a in "$@"; do + case "$a" in + --subscribe) SUBSCRIBE=1 ;; + *) ARGS+=("$a") ;; + esac +done +if [ "${#ARGS[@]}" -eq 0 ]; then set -- catalog; else set -- "${ARGS[@]}"; fi + +cmd="${1:-catalog}"; shift || true +case "$cmd" in + catalog) + cmd_catalog + ;; + customer) + ensure_customer "${1:-}" "${2:-}" "${3:-}" "$SUBSCRIBE" + ;; + all) + cmd_catalog + ensure_customer "${1:-}" "${2:-}" "${3:-}" "$SUBSCRIBE" + ;; + *) + die "unknown command '$cmd' (expected: catalog | customer | all)" + ;; +esac +info "done." diff --git a/openmeter-collector/provision/catalog.json b/openmeter-collector/provision/catalog.json new file mode 100644 index 0000000..6c5eaaa --- /dev/null +++ b/openmeter-collector/provision/catalog.json @@ -0,0 +1,85 @@ +{ + "$comment": "OpenMeter/Konnect metering catalog for the clearinghouse collector. Provisioned idempotently by bootstrap.sh / bootstrap.ps1. Meters are immutable in OpenMeter — the scripts only create missing entries, they never update or delete. Dimensions/value_property must match the CloudEvent fields emitted by openmeter-collector/collector.yaml.", + "meters": [ + { + "key": "network_fee_usd_micros", + "name": "Network fee (USD micros)", + "description": "Raw network cost in USD micros from the signer (observability).", + "event_type": "create_signed_ticket", + "aggregation": "sum", + "value_property": "$.network_fee_usd_micros", + "dimensions": { + "client_id": "$.client_id", + "usage_subject": "$.usage_subject", + "external_user_id": "$.external_user_id", + "model_id": "$.model_id", + "pipeline": "$.pipeline" + } + }, + { + "key": "billable_usd_micros", + "name": "Billable usage (USD micros)", + "description": "Post-markup billable amount in USD micros for pay-per-use billing. Interim: equals network fee until phase-2 markup is applied.", + "event_type": "create_signed_ticket", + "aggregation": "sum", + "value_property": "$.billable_usd_micros", + "dimensions": { + "client_id": "$.client_id", + "usage_subject": "$.usage_subject", + "external_user_id": "$.external_user_id", + "model_id": "$.model_id", + "pipeline": "$.pipeline" + } + }, + { + "key": "signed_ticket_count", + "name": "Signed ticket count", + "description": "Count of create_signed_ticket events (request volume).", + "event_type": "create_signed_ticket", + "aggregation": "count", + "dimensions": { + "client_id": "$.client_id", + "usage_subject": "$.usage_subject", + "model_id": "$.model_id", + "pipeline": "$.pipeline" + } + } + ], + "features": [ + { + "key": "network_spend", + "name": "Network spend", + "meter_key": "network_fee_usd_micros" + }, + { + "key": "billable_spend", + "name": "Billable spend", + "meter_key": "billable_usd_micros" + } + ], + "plan": { + "key": "clearinghouse_default_ppu", + "name": "Clearinghouse default pay-per-use", + "description": "Default pay-per-use plan billed on billable_usd_micros usage (1 USD micro = $0.000001).", + "currency": "USD", + "billing_cadence": "P1M", + "phases": [ + { + "key": "default", + "name": "Default", + "rate_cards": [ + { + "key": "billable_spend", + "name": "Billable spend", + "feature_key": "billable_spend", + "billing_cadence": "P1M", + "price": { + "type": "unit", + "amount": "0.000001" + } + } + ] + } + ] + } +} From 97552cc8721ed188b975d0da6c5c654e1b01fb26 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 13:12:22 -0400 Subject: [PATCH 2/7] docs(provision): add Konnect bootstrap test screenshots for PR review --- .../provision/docs/testing/01-create-org.png | Bin 0 -> 24405 bytes .../docs/testing/02-name-org-pat-nav.png | Bin 0 -> 9862 bytes .../provision/docs/testing/03-generate-pat.png | Bin 0 -> 16722 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 openmeter-collector/provision/docs/testing/01-create-org.png create mode 100644 openmeter-collector/provision/docs/testing/02-name-org-pat-nav.png create mode 100644 openmeter-collector/provision/docs/testing/03-generate-pat.png diff --git a/openmeter-collector/provision/docs/testing/01-create-org.png b/openmeter-collector/provision/docs/testing/01-create-org.png new file mode 100644 index 0000000000000000000000000000000000000000..0c37e843529e877c465b533971f38c6850c6d7b3 GIT binary patch literal 24405 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 literal 0 HcmV?d00001 diff --git a/openmeter-collector/provision/docs/testing/02-name-org-pat-nav.png b/openmeter-collector/provision/docs/testing/02-name-org-pat-nav.png new file mode 100644 index 0000000000000000000000000000000000000000..e7de036fada78b452af881f0679959ebc168c2bc GIT binary patch literal 9862 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 w^%z5m%fTW}Y?k%zZ!4Js~gDpHW|Bz6t_?s3FQv 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 YS~?9Y9bF(K2m(Q#sy``z{QCX>1Fr}T-2eap literal 0 HcmV?d00001 From c5609da9eb6979b8c8c18632606c757bea2b904c Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 16:41:53 -0400 Subject: [PATCH 3/7] fix(provision): update OpenMeter configuration and enhance bootstrap scripts - Updated `README.md` to reflect changes in OpenMeter environment variables, including the addition of `OPENMETER_URL`. - Modified `openmeter-collector/.env.example` to include `OPENMETER_URL` and clarify its usage. - Enhanced `bootstrap.ps1` and `bootstrap.sh` scripts to derive `OPENMETER_URL` from `OPENMETER_INGEST_URL` if not set, improving flexibility. - Added error handling and response validation in API helper functions within the bootstrap scripts for better reliability. --- README.md | 2 +- openmeter-collector/.env.example | 7 ++- openmeter-collector/provision/README.md | 26 ++++++++- openmeter-collector/provision/bootstrap.ps1 | 54 ++++++++++++++--- openmeter-collector/provision/bootstrap.sh | 65 +++++++++++++++++++-- 5 files changed, 135 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9a43adf..8a7e7c0 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Each service documents its variables in its own `.env.example`: | --- | --- | --- | | `kafka` | [`kafka/.env.example`](kafka/.env.example) | `KAFKA_ADVERTISED_ADDR` | | `remote-signer` | [`remote-signer/.env.example`](remote-signer/.env.example) | `REMOTE_SIGNER_WEBHOOK_URL`, `WEBHOOK_SECRET`, `SIGNER_*`, `KAFKA_BROKERS`, `KAFKA_GATEWAY_TOPIC` | -| `openmeter-collector` | [`openmeter-collector/.env.example`](openmeter-collector/.env.example) | `KAFKA_BROKERS`, `KAFKA_GATEWAY_TOPIC`, `OPENMETER_INGEST_URL`, `OPENMETER_API_KEY`, `ETH_USD_PRICE` | +| `openmeter-collector` | [`openmeter-collector/.env.example`](openmeter-collector/.env.example) | `KAFKA_BROKERS`, `KAFKA_GATEWAY_TOPIC`, `OPENMETER_URL`, `OPENMETER_INGEST_URL`, `OPENMETER_API_KEY`, `ETH_USD_PRICE` | Signer state (keystore, `.eth-password`, chain DB) is stored under [`remote-signer/data/`](remote-signer/data/), bind-mounted to `/data` in the container. diff --git a/openmeter-collector/.env.example b/openmeter-collector/.env.example index d57e5ec..2c79adc 100644 --- a/openmeter-collector/.env.example +++ b/openmeter-collector/.env.example @@ -8,8 +8,11 @@ KAFKA_BROKERS=kafka:9092 KAFKA_GATEWAY_TOPIC=livepeer-gateway-events # --- OpenMeter / Konnect (required) --- -# Konnect: https://.api.konghq.com/v3/openmeter/events -# Self-hosted: https:///api/v1/events +# Match the region of your Konnect org (US vs EU). Bootstrap scripts read OPENMETER_URL; +# the collector reads OPENMETER_INGEST_URL (same host, /events suffix). +# US: https://us.api.konghq.com/v3/openmeter +# EU: https://eu.api.konghq.com/v3/openmeter +OPENMETER_URL=https://us.api.konghq.com/v3/openmeter OPENMETER_INGEST_URL=https://us.api.konghq.com/v3/openmeter/events OPENMETER_API_KEY= ETH_USD_PRICE=3500 diff --git a/openmeter-collector/provision/README.md b/openmeter-collector/provision/README.md index 5b28462..09cea86 100644 --- a/openmeter-collector/provision/README.md +++ b/openmeter-collector/provision/README.md @@ -24,13 +24,17 @@ file first (`source .env` does **not** export vars to child processes). | --- | --- | --- | | `KONGCTL_DEFAULT_KONNECT_PAT` | Konnect PAT (preferred). Falls back to `OPENMETER_API_KEY`. | from `.env` | | `OPENMETER_API_KEY` | Same PAT as the collector service. | from `.env` | -| `OPENMETER_URL` | Metering API base. | `https://us.api.konghq.com/v3/openmeter` | +| `OPENMETER_URL` | Metering API base. **Must match your Konnect org region** (US vs EU). | `https://us.api.konghq.com/v3/openmeter` | + +If `OPENMETER_URL` is unset, the scripts derive it from `OPENMETER_INGEST_URL` (strip `/events`). One-time setup: ```bash cp openmeter-collector/.env.example openmeter-collector/.env # edit OPENMETER_API_KEY=kpat_… +# EU orgs: OPENMETER_URL=https://eu.api.konghq.com/v3/openmeter +# OPENMETER_INGEST_URL=https://eu.api.konghq.com/v3/openmeter/events ``` ## Usage @@ -91,3 +95,23 @@ expected compound key. link (e.g. created with an older bootstrap), the script deletes and recreates it. - Subscriptions are best-effort with `--subscribe`; plan pricing changes require a new plan version in Konnect (out of scope for this script). + +## Konnect first-time setup + +Walkthrough for provisioning against a fresh Konnect org (screenshots from a manual test run): + +1. **Create org** — from the org picker, click **+ Create** to provision a clean Metering & Billing workspace. + + ![Create new Konnect org](docs/testing/01-create-org.png) + +2. **Name the org** — e.g. *Clearinghouse Example Org*. + + ![Org name in profile menu](docs/testing/02-name-org-pat-nav.png) + +3. **Create a Personal Access Token** — Profile menu → **Personal access tokens** → **Generate**. Copy the `kpat_…` token immediately (shown once). + + ![Generate PAT modal](docs/testing/03-generate-pat.png) + +4. **Configure `.env`** — copy `openmeter-collector/.env.example` to `openmeter-collector/.env`, set `OPENMETER_API_KEY`, and set `OPENMETER_URL` / `OPENMETER_INGEST_URL` to your org's region (`us` or `eu`). + +5. **Run bootstrap** — `cd openmeter-collector/provision && ./bootstrap.sh catalog`. Re-run to confirm idempotency (`exists` / `active` lines, no errors). diff --git a/openmeter-collector/provision/bootstrap.ps1 b/openmeter-collector/provision/bootstrap.ps1 index 2a44fb2..59ddacd 100644 --- a/openmeter-collector/provision/bootstrap.ps1 +++ b/openmeter-collector/provision/bootstrap.ps1 @@ -45,7 +45,11 @@ if (Test-Path $EnvFile) { if ($_ -match '^\s*#' -or $_ -match '^\s*$') { return } if ($_ -match '^([^=]+)=(.*)$') { $k = $Matches[1].Trim() - if (-not (Test-Path "Env:$k")) { Set-Item -Path "Env:$k" -Value $Matches[2] } + $v = $Matches[2].Trim() + if (($v.StartsWith('"') -and $v.EndsWith('"')) -or ($v.StartsWith("'") -and $v.EndsWith("'"))) { + $v = $v.Substring(1, $v.Length - 2) + } + if (-not (Test-Path "Env:$k")) { Set-Item -Path "Env:$k" -Value $v } } } } @@ -55,6 +59,9 @@ $pat = if ($env:KONGCTL_DEFAULT_KONNECT_PAT) { $env:KONGCTL_DEFAULT_KONNECT_PAT if (-not $pat) { Die "no Konnect PAT — set KONGCTL_DEFAULT_KONNECT_PAT or OPENMETER_API_KEY in the environment or in $EnvFile" } $env:KONGCTL_DEFAULT_KONNECT_PAT = $pat +if (-not $env:OPENMETER_URL -and $env:OPENMETER_INGEST_URL) { + $env:OPENMETER_URL = $env:OPENMETER_INGEST_URL.TrimEnd('/').Replace('/events', '') +} $omUrl = if ($env:OPENMETER_URL) { $env:OPENMETER_URL } else { 'https://us.api.konghq.com/v3/openmeter' } $omUrl = $omUrl.TrimEnd('/') $BASE = [regex]::Replace($omUrl, '(https?://[^/]+).*', '$1') @@ -64,19 +71,48 @@ if (-not $PREFIX) { $PREFIX = '/v3/openmeter' } $catalogObj = Get-Content -Raw $Catalog | ConvertFrom-Json # --- kongctl api helpers --------------------------------------------------- +function Kapi-Err($method, $path, $detail) { + Die "kongctl api $method $path failed — check OPENMETER_URL ($omUrl) and your PAT: $detail" +} +function Kapi-Check($method, $path, $obj) { + if ($null -eq $obj) { Kapi-Err $method $path 'empty response' } + $msg = $obj.message + if (-not $msg) { $msg = $obj.detail } + if (-not $msg) { $msg = $obj.title } + $hasData = $obj.PSObject.Properties.Name -contains 'data' -or $obj.PSObject.Properties.Name -contains 'items' + if ($msg -and -not $hasData) { Kapi-Err $method $path $msg } +} function Kapi-Get($path) { - $out = & kongctl api get "$PREFIX$path" --base-url $BASE -o json 2>$null - if ($LASTEXITCODE -ne 0) { return $null } - return ($out | ConvertFrom-Json) + $errFile = [System.IO.Path]::GetTempFileName() + try { + $out = & kongctl api get "$PREFIX$path" --base-url $BASE -o json 2>$errFile + if ($LASTEXITCODE -ne 0) { Kapi-Err get $path (Get-Content -Raw $errFile) } + if (-not $out) { Kapi-Err get $path 'empty response' } + $obj = $out | ConvertFrom-Json + Kapi-Check get $path $obj + return $obj + } + finally { Remove-Item -Force $errFile -ErrorAction SilentlyContinue } } function Kapi-Send($method, $path, $bodyJson) { - $out = $bodyJson | & kongctl api $method "$PREFIX$path" --base-url $BASE -o json -f - - if ($LASTEXITCODE -ne 0) { throw "kongctl api $method $path failed" } - if ($out) { return ($out | ConvertFrom-Json) } else { return $null } + $errFile = [System.IO.Path]::GetTempFileName() + try { + $out = $bodyJson | & kongctl api $method "$PREFIX$path" --base-url $BASE -o json -f - 2>$errFile + if ($LASTEXITCODE -ne 0) { Kapi-Err $method $path (Get-Content -Raw $errFile) } + if (-not $out) { return $null } + $obj = $out | ConvertFrom-Json + Kapi-Check $method $path $obj + return $obj + } + finally { Remove-Item -Force $errFile -ErrorAction SilentlyContinue } } function Kapi-Delete($path) { - & kongctl api delete "$PREFIX$path" --base-url $BASE -o json 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { throw "kongctl api delete $path failed" } + $errFile = [System.IO.Path]::GetTempFileName() + try { + & kongctl api delete "$PREFIX$path" --base-url $BASE -o json 2>$errFile | Out-Null + if ($LASTEXITCODE -ne 0) { Kapi-Err delete $path (Get-Content -Raw $errFile) } + } + finally { Remove-Item -Force $errFile -ErrorAction SilentlyContinue } } function Plan-ConfigKey { if ($catalogObj.plan -and $catalogObj.plan.key) { return $catalogObj.plan.key } diff --git a/openmeter-collector/provision/bootstrap.sh b/openmeter-collector/provision/bootstrap.sh index adfacbc..2f80e40 100755 --- a/openmeter-collector/provision/bootstrap.sh +++ b/openmeter-collector/provision/bootstrap.sh @@ -16,8 +16,7 @@ # # ./bootstrap.sh customer [display_name] [--subscribe] # Ensure an OpenMeter customer keyed : exists with -# subject_keys = [ (bare, matches the CloudEvent subject), -# : (compound, tenant-scoped)]. +# subject_keys = [:] (matches the CloudEvent subject). # --subscribe also ensures a subscription on the catalog plan (best-effort). # # ./bootstrap.sh all [display_name] [--subscribe] @@ -54,6 +53,9 @@ if [ -z "$PAT" ]; then fi export KONGCTL_DEFAULT_KONNECT_PAT="$PAT" +if [ -z "${OPENMETER_URL:-}" ] && [ -n "${OPENMETER_INGEST_URL:-}" ]; then + OPENMETER_URL="${OPENMETER_INGEST_URL%/events}" +fi OPENMETER_URL="${OPENMETER_URL:-https://us.api.konghq.com/v3/openmeter}" OPENMETER_URL="${OPENMETER_URL%/}" BASE="$(printf '%s' "$OPENMETER_URL" | sed -E 's#(https?://[^/]+).*#\1#')" @@ -61,10 +63,61 @@ PREFIX="$(printf '%s' "$OPENMETER_URL" | sed -E 's#https?://[^/]+##')" [ -n "$PREFIX" ] || PREFIX="/v3/openmeter" # --- kongctl api helpers (return response body as JSON on stdout) ---------- -kapi_get() { kongctl api get "$PREFIX$1" --base-url "$BASE" -o json; } -kapi_post() { kongctl api post "$PREFIX$1" --base-url "$BASE" -o json -f -; } -kapi_put() { kongctl api put "$PREFIX$1" --base-url "$BASE" -o json -f -; } -kapi_delete() { kongctl api delete "$PREFIX$1" --base-url "$BASE" -o json; } +kapi_err() { + local method="$1" path="$2" err="$3" + die "kongctl api $method $path failed — check OPENMETER_URL ($OPENMETER_URL) and your PAT: $err" +} + +kapi_check() { + local method="$1" path="$2" body="$3" + if [ -z "$body" ]; then + kapi_err "$method" "$path" "empty response" + fi + if printf '%s' "$body" | jq -e 'type == "object" and (.message // .detail // .title // empty) != "" and (.data // .items // null) == null' >/dev/null 2>&1; then + kapi_err "$method" "$path" "$(printf '%s' "$body" | jq -r '.message // .detail // .title')" + fi +} + +kapi_get() { + local path="$1" body err + err="$(mktemp)" + if ! body="$(kongctl api get "$PREFIX$path" --base-url "$BASE" -o json 2>"$err")"; then + kapi_err get "$path" "$(cat "$err")" + fi + rm -f "$err" + kapi_check get "$path" "$body" + printf '%s' "$body" +} +kapi_post() { + local path="$1" body err + err="$(mktemp)" + if ! body="$(kongctl api post "$PREFIX$path" --base-url "$BASE" -o json -f - 2>"$err")"; then + kapi_err post "$path" "$(cat "$err")" + fi + rm -f "$err" + kapi_check post "$path" "$body" + printf '%s' "$body" +} +kapi_put() { + local path="$1" body err + err="$(mktemp)" + if ! body="$(kongctl api put "$PREFIX$path" --base-url "$BASE" -o json -f - 2>"$err")"; then + kapi_err put "$path" "$(cat "$err")" + fi + rm -f "$err" + kapi_check put "$path" "$body" + printf '%s' "$body" +} +kapi_delete() { + local path="$1" body err + err="$(mktemp)" + if ! body="$(kongctl api delete "$PREFIX$path" --base-url "$BASE" -o json 2>"$err")"; then + kapi_err delete "$path" "$(cat "$err")" + fi + rm -f "$err" + [ -n "$body" ] && kapi_check delete "$path" "$body" || true + printf '%s' "$body" +} plan_config_key() { jq -r '.plan.key // .plan_key // empty' "$CATALOG"; } From e08ac37f515a543477535c75604d563b88c22960 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 16:42:22 -0400 Subject: [PATCH 4/7] Revert "docs(provision): add Konnect bootstrap test screenshots for PR review" This reverts commit 97552cc8721ed188b975d0da6c5c654e1b01fb26. --- .../provision/docs/testing/01-create-org.png | Bin 24405 -> 0 bytes .../docs/testing/02-name-org-pat-nav.png | Bin 9862 -> 0 bytes .../provision/docs/testing/03-generate-pat.png | Bin 16722 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openmeter-collector/provision/docs/testing/01-create-org.png delete mode 100644 openmeter-collector/provision/docs/testing/02-name-org-pat-nav.png delete mode 100644 openmeter-collector/provision/docs/testing/03-generate-pat.png diff --git a/openmeter-collector/provision/docs/testing/01-create-org.png b/openmeter-collector/provision/docs/testing/01-create-org.png deleted file mode 100644 index 0c37e843529e877c465b533971f38c6850c6d7b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24405 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 diff --git a/openmeter-collector/provision/docs/testing/02-name-org-pat-nav.png b/openmeter-collector/provision/docs/testing/02-name-org-pat-nav.png deleted file mode 100644 index e7de036fada78b452af881f0679959ebc168c2bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9862 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 w^%z5m%fTW}Y?k%zZ!4Js~gDpHW|Bz6t_?s3FQv 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 YS~?9Y9bF(K2m(Q#sy``z{QCX>1Fr}T-2eap From d51ba3eb259e55def4e37ad27a3167e45c9f800b Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 16:54:50 -0400 Subject: [PATCH 5/7] fix(provision): make best-effort paths non-fatal and align catalog/docs with collector - Add soft kapi helpers (bash + PowerShell) for publish, subscription, and feature-recreate delete so optional paths warn instead of aborting the run - Fix header comment: scripts warn on missing subject_keys, never mutate them - Derive OPENMETER_URL from ingest URL using trailing-only /events strip (ps1) - Remove usage_subject from catalog dimensions; use external_user_id to match collector.yaml; clarify billable_usd_micros is phase-2 (not yet emitted) - Fix provision README identity contract and remove broken screenshot links --- openmeter-collector/provision/README.md | 18 +--- openmeter-collector/provision/bootstrap.ps1 | 97 +++++++++-------- openmeter-collector/provision/bootstrap.sh | 114 +++++++++++--------- openmeter-collector/provision/catalog.json | 6 +- 4 files changed, 123 insertions(+), 112 deletions(-) diff --git a/openmeter-collector/provision/README.md b/openmeter-collector/provision/README.md index 09cea86..302f8b0 100644 --- a/openmeter-collector/provision/README.md +++ b/openmeter-collector/provision/README.md @@ -70,7 +70,7 @@ From [`catalog.json`](catalog.json): | Kind | Key | Notes | | --- | --- | --- | | Meter | `network_fee_usd_micros` | SUM of `$.network_fee_usd_micros` | -| Meter | `billable_usd_micros` | SUM of `$.billable_usd_micros` (interim = network fee until phase-2 markup) | +| Meter | `billable_usd_micros` | SUM of `$.billable_usd_micros` (not emitted by collector until phase-2; meter stays empty until then) | | Meter | `signed_ticket_count` | COUNT | | Feature | `network_spend` | linked to `network_fee_usd_micros` meter | | Feature | `billable_spend` | linked to `billable_usd_micros` meter | @@ -78,11 +78,11 @@ From [`catalog.json`](catalog.json): ## Identity contract (important) -The CloudEvent **`subject` is the compound `client_id:usage_subject`** (e.g. +The CloudEvent **`subject` is the compound `client_id:external_user_id`** (e.g. `demo-client:demo-user`), which is also the customer key and its single `subject_key`. OpenMeter attributes usage by exact subject match, and **forbids changing a customer's `subject_keys` once it has an active subscription** — so the subject must be compound and -correct from creation. Break usage down per-tenant/user with the `client_id` / `usage_subject` +correct from creation. Break usage down per-tenant/user with the `client_id` / `external_user_id` meter dimensions, not by changing the subject. The scripts therefore never mutate `subject_keys` on existing customers; they warn if an existing customer is missing the expected compound key. @@ -98,20 +98,10 @@ expected compound key. ## Konnect first-time setup -Walkthrough for provisioning against a fresh Konnect org (screenshots from a manual test run): +Walkthrough for provisioning against a fresh Konnect org: 1. **Create org** — from the org picker, click **+ Create** to provision a clean Metering & Billing workspace. - - ![Create new Konnect org](docs/testing/01-create-org.png) - 2. **Name the org** — e.g. *Clearinghouse Example Org*. - - ![Org name in profile menu](docs/testing/02-name-org-pat-nav.png) - 3. **Create a Personal Access Token** — Profile menu → **Personal access tokens** → **Generate**. Copy the `kpat_…` token immediately (shown once). - - ![Generate PAT modal](docs/testing/03-generate-pat.png) - 4. **Configure `.env`** — copy `openmeter-collector/.env.example` to `openmeter-collector/.env`, set `OPENMETER_API_KEY`, and set `OPENMETER_URL` / `OPENMETER_INGEST_URL` to your org's region (`us` or `eu`). - 5. **Run bootstrap** — `cd openmeter-collector/provision && ./bootstrap.sh catalog`. Re-run to confirm idempotency (`exists` / `active` lines, no errors). diff --git a/openmeter-collector/provision/bootstrap.ps1 b/openmeter-collector/provision/bootstrap.ps1 index 59ddacd..b49e5ba 100644 --- a/openmeter-collector/provision/bootstrap.ps1 +++ b/openmeter-collector/provision/bootstrap.ps1 @@ -60,7 +60,7 @@ if (-not $pat) { Die "no Konnect PAT — set KONGCTL_DEFAULT_KONNECT_PAT or OPEN $env:KONGCTL_DEFAULT_KONNECT_PAT = $pat if (-not $env:OPENMETER_URL -and $env:OPENMETER_INGEST_URL) { - $env:OPENMETER_URL = $env:OPENMETER_INGEST_URL.TrimEnd('/').Replace('/events', '') + $env:OPENMETER_URL = ($env:OPENMETER_INGEST_URL.TrimEnd('/') -replace '/events/?$', '') } $omUrl = if ($env:OPENMETER_URL) { $env:OPENMETER_URL } else { 'https://us.api.konghq.com/v3/openmeter' } $omUrl = $omUrl.TrimEnd('/') @@ -74,46 +74,56 @@ $catalogObj = Get-Content -Raw $Catalog | ConvertFrom-Json function Kapi-Err($method, $path, $detail) { Die "kongctl api $method $path failed — check OPENMETER_URL ($omUrl) and your PAT: $detail" } -function Kapi-Check($method, $path, $obj) { - if ($null -eq $obj) { Kapi-Err $method $path 'empty response' } - $msg = $obj.message - if (-not $msg) { $msg = $obj.detail } - if (-not $msg) { $msg = $obj.title } - $hasData = $obj.PSObject.Properties.Name -contains 'data' -or $obj.PSObject.Properties.Name -contains 'items' - if ($msg -and -not $hasData) { Kapi-Err $method $path $msg } +function Kapi-Warn($method, $path, $detail) { + Warn "kongctl api $method $path failed — check OPENMETER_URL ($omUrl) and your PAT: $detail" } -function Kapi-Get($path) { - $errFile = [System.IO.Path]::GetTempFileName() - try { - $out = & kongctl api get "$PREFIX$path" --base-url $BASE -o json 2>$errFile - if ($LASTEXITCODE -ne 0) { Kapi-Err get $path (Get-Content -Raw $errFile) } - if (-not $out) { Kapi-Err get $path 'empty response' } - $obj = $out | ConvertFrom-Json - Kapi-Check get $path $obj - return $obj - } - finally { Remove-Item -Force $errFile -ErrorAction SilentlyContinue } +function Kapi-BodyError($obj) { + if ($obj.message) { return $obj.message } + if ($obj.detail) { return $obj.detail } + return $obj.title } -function Kapi-Send($method, $path, $bodyJson) { +function Kapi-BodyIsError($obj) { + if ($null -eq $obj) { return $true } + $msg = Kapi-BodyError $obj + if (-not $msg) { return $false } + $hasData = $obj.PSObject.Properties.Name -contains 'data' -or $obj.PSObject.Properties.Name -contains 'items' + return -not $hasData +} +function Kapi-Run($soft, $method, $path, $bodyJson) { $errFile = [System.IO.Path]::GetTempFileName() try { - $out = $bodyJson | & kongctl api $method "$PREFIX$path" --base-url $BASE -o json -f - 2>$errFile - if ($LASTEXITCODE -ne 0) { Kapi-Err $method $path (Get-Content -Raw $errFile) } - if (-not $out) { return $null } + $out = if ($method -eq 'delete') { + & kongctl api delete "$PREFIX$path" --base-url $BASE -o json 2>$errFile + } + elseif ($method -eq 'get') { + & kongctl api get "$PREFIX$path" --base-url $BASE -o json 2>$errFile + } + else { + $bodyJson | & kongctl api $method "$PREFIX$path" --base-url $BASE -o json -f - 2>$errFile + } + if ($LASTEXITCODE -ne 0) { + if ($soft) { Kapi-Warn $method $path (Get-Content -Raw $errFile); return $null } + Kapi-Err $method $path (Get-Content -Raw $errFile) + } + if (-not $out) { + if ($soft) { Kapi-Warn $method $path 'empty response'; return $null } + Kapi-Err $method $path 'empty response' + } $obj = $out | ConvertFrom-Json - Kapi-Check $method $path $obj + if (Kapi-BodyIsError $obj) { + if ($soft) { Kapi-Warn $method $path (Kapi-BodyError $obj); return $null } + Kapi-Err $method $path (Kapi-BodyError $obj) + } return $obj } finally { Remove-Item -Force $errFile -ErrorAction SilentlyContinue } } -function Kapi-Delete($path) { - $errFile = [System.IO.Path]::GetTempFileName() - try { - & kongctl api delete "$PREFIX$path" --base-url $BASE -o json 2>$errFile | Out-Null - if ($LASTEXITCODE -ne 0) { Kapi-Err delete $path (Get-Content -Raw $errFile) } - } - finally { Remove-Item -Force $errFile -ErrorAction SilentlyContinue } -} +function Kapi-Get($path) { return Kapi-Run $false get $path $null } +function Kapi-Send($method, $path, $bodyJson) { return Kapi-Run $false $method $path $bodyJson } +function Kapi-Delete($path) { Kapi-Run $false delete $path $null | Out-Null } +function Kapi-Get-Soft($path) { return Kapi-Run $true get $path $null } +function Kapi-Send-Soft($method, $path, $bodyJson) { return Kapi-Run $true $method $path $bodyJson } +function Kapi-Delete-Soft($path) { Kapi-Run $true delete $path $null | Out-Null } function Plan-ConfigKey { if ($catalogObj.plan -and $catalogObj.plan.key) { return $catalogObj.plan.key } return $catalogObj.plan_key @@ -173,7 +183,7 @@ function Ensure-Features { if ($linked -eq $meterId) { Info "feature $($f.key) - exists"; continue } Warn "feature $($f.key) - exists without meter link; recreating" - try { Kapi-Delete "/features/$($feat.id)" } catch { } + Kapi-Delete-Soft "/features/$($feat.id)" Kapi-Send 'post' '/features' (New-FeatureBody $f.key $f.name $meterId) | Out-Null Info "feature $($f.key) - recreated (meter: $meterKey)" } @@ -207,14 +217,11 @@ function Build-PlanBody { return ($body | ConvertTo-Json -Depth 8 -Compress) } function Publish-Plan($planId, $planKey) { - try { - $published = Kapi-Send 'post' "/plans/$planId/publish" '{}' - if ($published.status -eq 'active') { - Info "plan $planKey - published" - return $true - } + $published = Kapi-Send-Soft 'post' "/plans/$planId/publish" '{}' + if ($published -and $published.status -eq 'active') { + Info "plan $planKey - published" + return $true } - catch { } Warn "plan $planKey - could not publish (ensure features have meter links)" return $false } @@ -278,15 +285,17 @@ function Ensure-Customer($clientId, $externalUserId, $display, $subscribe) { function Ensure-Subscription($customerId, $label) { $planKey = Plan-ConfigKey if (-not $planKey) { Warn 'no plan_key in catalog; skipping subscription'; return } - $existing = Items (Kapi-Get "/subscriptions?customer_id=$customerId") | Where-Object { $_.customer_id -eq $customerId } + $existing = Items (Kapi-Get-Soft "/subscriptions?customer_id=$customerId") | Where-Object { $_.customer_id -eq $customerId } if ($existing) { Info "sub $label - exists"; return } $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $body = [ordered]@{ customer_id = $customerId; plan_key = $planKey; active_from = $now } - try { - Kapi-Send 'post' '/subscriptions' ($body | ConvertTo-Json -Compress) | Out-Null + $created = Kapi-Send-Soft 'post' '/subscriptions' ($body | ConvertTo-Json -Compress) + if ($created) { Info "sub $label - created on $planKey" } - catch { Warn "sub $label - could not create subscription on $planKey (create manually if needed)" } + else { + Warn "sub $label - could not create subscription on $planKey (create manually if needed)" + } } # --- dispatch -------------------------------------------------------------- diff --git a/openmeter-collector/provision/bootstrap.sh b/openmeter-collector/provision/bootstrap.sh index 2f80e40..dd63cc8 100755 --- a/openmeter-collector/provision/bootstrap.sh +++ b/openmeter-collector/provision/bootstrap.sh @@ -3,8 +3,8 @@ # Bootstrap the OpenMeter/Konnect metering catalog for the clearinghouse collector. # # Idempotent: creates only what is missing. Meters are immutable in OpenMeter, so -# this script never updates or deletes them — it only adds missing meters/features -# and (for customers) appends missing usage-attribution subject keys. +# this script never updates or deletes them — it only adds missing meters/features. +# For customers it never mutates subject_keys on existing records (warns only). # # Requires: kongctl (https://developer.konghq.com/kongctl/) and jq. # Auth: KONGCTL_DEFAULT_KONNECT_PAT (preferred) or OPENMETER_API_KEY — a Konnect PAT (kpat_…). @@ -68,57 +68,71 @@ kapi_err() { die "kongctl api $method $path failed — check OPENMETER_URL ($OPENMETER_URL) and your PAT: $err" } -kapi_check() { - local method="$1" path="$2" body="$3" - if [ -z "$body" ]; then - kapi_err "$method" "$path" "empty response" - fi - if printf '%s' "$body" | jq -e 'type == "object" and (.message // .detail // .title // empty) != "" and (.data // .items // null) == null' >/dev/null 2>&1; then - kapi_err "$method" "$path" "$(printf '%s' "$body" | jq -r '.message // .detail // .title')" - fi +kapi_warn() { + local method="$1" path="$2" err="$3" + warn "kongctl api $method $path failed — check OPENMETER_URL ($OPENMETER_URL) and your PAT: $err" } -kapi_get() { - local path="$1" body err - err="$(mktemp)" - if ! body="$(kongctl api get "$PREFIX$path" --base-url "$BASE" -o json 2>"$err")"; then - kapi_err get "$path" "$(cat "$err")" - fi - rm -f "$err" - kapi_check get "$path" "$body" - printf '%s' "$body" +kapi_body_error() { + printf '%s' "$1" | jq -r '.message // .detail // .title // empty' } -kapi_post() { - local path="$1" body err - err="$(mktemp)" - if ! body="$(kongctl api post "$PREFIX$path" --base-url "$BASE" -o json -f - 2>"$err")"; then - kapi_err post "$path" "$(cat "$err")" - fi - rm -f "$err" - kapi_check post "$path" "$body" - printf '%s' "$body" + +kapi_body_is_error() { + printf '%s' "$1" | jq -e 'type == "object" and (.message // .detail // .title // empty) != "" and (.data // .items // null) == null' >/dev/null 2>&1 } -kapi_put() { - local path="$1" body err + +kapi_run() { + local mode="$1" method="$2" path="$3" + shift 3 + local body err err="$(mktemp)" - if ! body="$(kongctl api put "$PREFIX$path" --base-url "$BASE" -o json -f - 2>"$err")"; then - kapi_err put "$path" "$(cat "$err")" - fi + case "$method" in + get) + if ! body="$(kongctl api get "$PREFIX$path" --base-url "$BASE" -o json 2>"$err")"; then + if [ "$mode" = soft ]; then kapi_warn get "$path" "$(cat "$err")"; rm -f "$err"; return 1; fi + kapi_err get "$path" "$(cat "$err")" + fi + ;; + post) + if ! body="$(kongctl api post "$PREFIX$path" --base-url "$BASE" -o json -f - 2>"$err")"; then + if [ "$mode" = soft ]; then kapi_warn post "$path" "$(cat "$err")"; rm -f "$err"; return 1; fi + kapi_err post "$path" "$(cat "$err")" + fi + ;; + put) + if ! body="$(kongctl api put "$PREFIX$path" --base-url "$BASE" -o json -f - 2>"$err")"; then + if [ "$mode" = soft ]; then kapi_warn put "$path" "$(cat "$err")"; rm -f "$err"; return 1; fi + kapi_err put "$path" "$(cat "$err")" + fi + ;; + delete) + if ! body="$(kongctl api delete "$PREFIX$path" --base-url "$BASE" -o json 2>"$err")"; then + if [ "$mode" = soft ]; then kapi_warn delete "$path" "$(cat "$err")"; rm -f "$err"; return 1; fi + kapi_err delete "$path" "$(cat "$err")" + fi + ;; + *) die "unknown kapi method: $method" ;; + esac rm -f "$err" - kapi_check put "$path" "$body" - printf '%s' "$body" -} -kapi_delete() { - local path="$1" body err - err="$(mktemp)" - if ! body="$(kongctl api delete "$PREFIX$path" --base-url "$BASE" -o json 2>"$err")"; then - kapi_err delete "$path" "$(cat "$err")" + if [ -z "$body" ]; then + if [ "$mode" = soft ]; then kapi_warn "$method" "$path" "empty response"; return 1; fi + kapi_err "$method" "$path" "empty response" + fi + if kapi_body_is_error "$body"; then + if [ "$mode" = soft ]; then kapi_warn "$method" "$path" "$(kapi_body_error "$body")"; return 1; fi + kapi_err "$method" "$path" "$(kapi_body_error "$body")" fi - rm -f "$err" - [ -n "$body" ] && kapi_check delete "$path" "$body" || true printf '%s' "$body" } +kapi_get() { kapi_run hard get "$1"; } +kapi_post() { kapi_run hard post "$1"; } +kapi_put() { kapi_run hard put "$1"; } +kapi_delete() { kapi_run hard delete "$1"; } +kapi_get_soft() { kapi_run soft get "$1"; } +kapi_post_soft() { kapi_run soft post "$1"; } +kapi_delete_soft() { kapi_run soft delete "$1"; } + plan_config_key() { jq -r '.plan.key // .plan_key // empty' "$CATALOG"; } meter_id_for() { @@ -190,7 +204,7 @@ ensure_features() { fi warn "feature $key — exists without meter link; recreating" - kapi_delete "/features/$feat_id" >/dev/null 2>&1 || true + kapi_delete_soft "/features/$feat_id" >/dev/null 2>&1 || true create_feature "$key" "$(jq -r '.name' <<<"$f")" "$meter_id" info "feature $key — recreated (meter: $meter_key)" done < <(jq -c '.features[]' "$CATALOG") @@ -230,9 +244,9 @@ build_plan_body() { } publish_plan() { - local plan_id="$1" plan_key="$2" - if printf '{}' | kapi_post "/plans/$plan_id/publish" 2>/dev/null \ - | jq -e '.status == "active"' >/dev/null; then + local plan_id="$1" plan_key="$2" resp + if resp="$(printf '{}' | kapi_post_soft "/plans/$plan_id/publish")" \ + && printf '%s' "$resp" | jq -e '.status == "active"' >/dev/null; then info "plan $plan_key — published" return 0 fi @@ -327,8 +341,8 @@ ensure_subscription() { local plan_key; plan_key="$(plan_config_key)" [ -n "$plan_key" ] || { warn "no plan_key in catalog; skipping subscription"; return 0; } - local existing - existing="$(kapi_get "/subscriptions?customer_id=$customer_id" 2>/dev/null \ + local existing resp + existing="$(kapi_get_soft "/subscriptions?customer_id=$customer_id" 2>/dev/null \ | jq -r --arg c "$customer_id" '(.data // .)[]? | select(.customer_id == $c) | .id' 2>/dev/null || true)" if [ -n "$existing" ]; then info "sub $label — exists" @@ -338,7 +352,7 @@ ensure_subscription() { now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" body="$(jq -n --arg c "$customer_id" --arg p "$plan_key" --arg t "$now" \ '{customer_id:$c, plan_key:$p, active_from:$t}')" - if printf '%s' "$body" | kapi_post /subscriptions >/dev/null 2>&1; then + if resp="$(printf '%s' "$body" | kapi_post_soft /subscriptions)" && [ -n "$resp" ]; then info "sub $label — created on $plan_key" else warn "sub $label — could not create subscription on $plan_key (create manually if needed)" diff --git a/openmeter-collector/provision/catalog.json b/openmeter-collector/provision/catalog.json index 6c5eaaa..1b7bccc 100644 --- a/openmeter-collector/provision/catalog.json +++ b/openmeter-collector/provision/catalog.json @@ -10,7 +10,6 @@ "value_property": "$.network_fee_usd_micros", "dimensions": { "client_id": "$.client_id", - "usage_subject": "$.usage_subject", "external_user_id": "$.external_user_id", "model_id": "$.model_id", "pipeline": "$.pipeline" @@ -19,13 +18,12 @@ { "key": "billable_usd_micros", "name": "Billable usage (USD micros)", - "description": "Post-markup billable amount in USD micros for pay-per-use billing. Interim: equals network fee until phase-2 markup is applied.", + "description": "Post-markup billable amount in USD micros for pay-per-use billing. Not emitted by the collector until phase-2 markup; meter stays empty until then.", "event_type": "create_signed_ticket", "aggregation": "sum", "value_property": "$.billable_usd_micros", "dimensions": { "client_id": "$.client_id", - "usage_subject": "$.usage_subject", "external_user_id": "$.external_user_id", "model_id": "$.model_id", "pipeline": "$.pipeline" @@ -39,7 +37,7 @@ "aggregation": "count", "dimensions": { "client_id": "$.client_id", - "usage_subject": "$.usage_subject", + "external_user_id": "$.external_user_id", "model_id": "$.model_id", "pipeline": "$.pipeline" } From 7f9385dd0c22dec2e4dcbb40b3f236625410938a Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 17:11:43 -0400 Subject: [PATCH 6/7] refactor(provision): update subscription creation structure in bootstrap scripts - Modified the subscription creation logic in both PowerShell and Bash scripts to use a new structured format for the request body, enhancing clarity and consistency. - The new format encapsulates customer and plan details in a more organized manner, improving maintainability of the code. --- openmeter-collector/provision/bootstrap.ps1 | 6 ++++-- openmeter-collector/provision/bootstrap.sh | 7 +++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openmeter-collector/provision/bootstrap.ps1 b/openmeter-collector/provision/bootstrap.ps1 index b49e5ba..2b7f5fd 100644 --- a/openmeter-collector/provision/bootstrap.ps1 +++ b/openmeter-collector/provision/bootstrap.ps1 @@ -287,8 +287,10 @@ function Ensure-Subscription($customerId, $label) { if (-not $planKey) { Warn 'no plan_key in catalog; skipping subscription'; return } $existing = Items (Kapi-Get-Soft "/subscriptions?customer_id=$customerId") | Where-Object { $_.customer_id -eq $customerId } if ($existing) { Info "sub $label - exists"; return } - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - $body = [ordered]@{ customer_id = $customerId; plan_key = $planKey; active_from = $now } + $body = [ordered]@{ + customer = [ordered]@{ key = $label } + plan = [ordered]@{ key = $planKey } + } $created = Kapi-Send-Soft 'post' '/subscriptions' ($body | ConvertTo-Json -Compress) if ($created) { Info "sub $label - created on $planKey" diff --git a/openmeter-collector/provision/bootstrap.sh b/openmeter-collector/provision/bootstrap.sh index dd63cc8..2a06737 100755 --- a/openmeter-collector/provision/bootstrap.sh +++ b/openmeter-collector/provision/bootstrap.sh @@ -348,10 +348,9 @@ ensure_subscription() { info "sub $label — exists" return 0 fi - local now body - now="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - body="$(jq -n --arg c "$customer_id" --arg p "$plan_key" --arg t "$now" \ - '{customer_id:$c, plan_key:$p, active_from:$t}')" + local body + body="$(jq -n --arg ck "$label" --arg pk "$plan_key" \ + '{customer:{key:$ck}, plan:{key:$pk}}')" if resp="$(printf '%s' "$body" | kapi_post_soft /subscriptions)" && [ -n "$resp" ]; then info "sub $label — created on $plan_key" else From 87c17a390f23fd90201bd8161d35d745a6408ef4 Mon Sep 17 00:00:00 2001 From: John | Elite Encoder Date: Tue, 30 Jun 2026 17:38:14 -0400 Subject: [PATCH 7/7] fix(provision): update env file paths in bootstrap scripts and README - Changed the paths for the environment file in both PowerShell and Bash bootstrap scripts to point to the repo-root `.env` instead of the previous relative paths. - Updated the README to reflect the new location of the `.env` file, clarifying the setup instructions for users. --- openmeter-collector/provision/README.md | 6 +++--- openmeter-collector/provision/bootstrap.ps1 | 4 ++-- openmeter-collector/provision/bootstrap.sh | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openmeter-collector/provision/README.md b/openmeter-collector/provision/README.md index 302f8b0..3fb0483 100644 --- a/openmeter-collector/provision/README.md +++ b/openmeter-collector/provision/README.md @@ -16,7 +16,7 @@ the scripts are thin and idempotent. ## Configuration (env) -Both scripts auto-load [`openmeter-collector/.env`](../.env) when present (override with +Both scripts auto-load the repo-root [`.env`](../../.env) when present (override with `ENV_FILE`). Put your Konnect PAT there as `OPENMETER_API_KEY` — no need to `source` the file first (`source .env` does **not** export vars to child processes). @@ -31,7 +31,7 @@ If `OPENMETER_URL` is unset, the scripts derive it from `OPENMETER_INGEST_URL` ( One-time setup: ```bash -cp openmeter-collector/.env.example openmeter-collector/.env +cp .env.example .env # at the repo root # edit OPENMETER_API_KEY=kpat_… # EU orgs: OPENMETER_URL=https://eu.api.konghq.com/v3/openmeter # OPENMETER_INGEST_URL=https://eu.api.konghq.com/v3/openmeter/events @@ -103,5 +103,5 @@ Walkthrough for provisioning against a fresh Konnect org: 1. **Create org** — from the org picker, click **+ Create** to provision a clean Metering & Billing workspace. 2. **Name the org** — e.g. *Clearinghouse Example Org*. 3. **Create a Personal Access Token** — Profile menu → **Personal access tokens** → **Generate**. Copy the `kpat_…` token immediately (shown once). -4. **Configure `.env`** — copy `openmeter-collector/.env.example` to `openmeter-collector/.env`, set `OPENMETER_API_KEY`, and set `OPENMETER_URL` / `OPENMETER_INGEST_URL` to your org's region (`us` or `eu`). +4. **Configure `.env`** — copy `.env.example` to `.env` at the repo root, set `OPENMETER_API_KEY`, and set `OPENMETER_URL` / `OPENMETER_INGEST_URL` to your org's region (`us` or `eu`). 5. **Run bootstrap** — `cd openmeter-collector/provision && ./bootstrap.sh catalog`. Re-run to confirm idempotency (`exists` / `active` lines, no errors). diff --git a/openmeter-collector/provision/bootstrap.ps1 b/openmeter-collector/provision/bootstrap.ps1 index 2b7f5fd..9ed4639 100644 --- a/openmeter-collector/provision/bootstrap.ps1 +++ b/openmeter-collector/provision/bootstrap.ps1 @@ -38,8 +38,8 @@ function Warn($m) { Write-Warning $m } if (-not (Get-Command kongctl -ErrorAction SilentlyContinue)) { Die 'kongctl not found (https://developer.konghq.com/kongctl/)' } if (-not (Test-Path $Catalog)) { Die "catalog not found: $Catalog" } -# --- env file (openmeter-collector/.env) ----------------------------------- -$EnvFile = if ($env:ENV_FILE) { $env:ENV_FILE } else { Join-Path (Split-Path $ScriptDir -Parent) '.env' } +# --- env file (repo-root .env) --------------------------------------------- +$EnvFile = if ($env:ENV_FILE) { $env:ENV_FILE } else { Join-Path (Split-Path (Split-Path $ScriptDir -Parent) -Parent) '.env' } if (Test-Path $EnvFile) { Get-Content $EnvFile | ForEach-Object { if ($_ -match '^\s*#' -or $_ -match '^\s*$') { return } diff --git a/openmeter-collector/provision/bootstrap.sh b/openmeter-collector/provision/bootstrap.sh index 2a06737..6bcae6a 100755 --- a/openmeter-collector/provision/bootstrap.sh +++ b/openmeter-collector/provision/bootstrap.sh @@ -35,10 +35,10 @@ command -v kongctl >/dev/null 2>&1 || die "kongctl not found (https://developer. command -v jq >/dev/null 2>&1 || die "jq not found" [ -f "$CATALOG" ] || die "catalog not found: $CATALOG" -# --- env file (openmeter-collector/.env) ----------------------------------- +# --- env file (repo-root .env) --------------------------------------------- # Plain `source .env` does not export vars to child processes; load here so # `./bootstrap.sh catalog` works without `set -a; source …`. -ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/../.env}" +ENV_FILE="${ENV_FILE:-$SCRIPT_DIR/../../.env}" if [ -f "$ENV_FILE" ]; then set -a # shellcheck disable=SC1090