focus-exporter

module
v0.0.0-...-2ff74a0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: AGPL-3.0

README

focus-exporter

CI

A standalone Go binary that pulls a provider's own cost/usage API and emits FinOps FOCUS 1.2 records as JSON or CSV.

It is gateway-independent (no shared service, no database) and domain-agnostic: each provider is a small adapter behind one interface, so the same tool exports managed-infrastructure cost, streaming-platform cost, and AI model/token cost into one normalized FOCUS schema. Fields FOCUS does not yet model natively ride as x_-prefixed extension columns, which conformant consumers ignore.

Integrations

Provider Category Grain exported Setup
PlanetScale Managed database invoice month x database x billing metric planetscale.md
Anthropic AI / LLM day x model x token bucket (cost + tokens) anthropic.md
OpenAI AI / LLM day x model x token bucket (billed cost + quantity) openai.md
Confluent Cloud Streaming (Kafka) billing line item (product x resource) confluent.md
GitHub Developer platform / CI day x product x SKU x repository (gross/net billed cost) github.md
DigitalOcean Cloud (IaaS) invoice line item (product x resource, per-project) digitalocean.md
Grafana Cloud Observability billed dimension x stack (per-stack attributed cost) grafanacloud.md
Cloudflare Cloud / edge billable-usage line item (service x day, FOCUS-native fields) cloudflare.md
OpenRouter AI gateway day x model x upstream provider (credit spend) openrouter.md
Modal Serverless GPU / compute day x object (per-resource CPU/mem/GPU cost) modal.md
RespanAI AI gateway / observability day x model x upstream provider (request spend) respanai.md
Helicone AI gateway / observability day x model x upstream provider (request spend) helicone.md

One integration behind an LLM gateway (OpenRouter) captures spend across every model and upstream provider you route through it. Per-provider setup (environment variables, credential scopes, API endpoints, and the full FOCUS mapping) lives under docs/providers/.

Why FOCUS 1.2

FOCUS 1.2 is the version with ecosystem traction (the FinOps conformance program is scoped to it, and cloud-native FOCUS exports are GA on it). Fields that FOCUS does not yet model natively are carried as x_-prefixed extension columns, which conformant consumers ignore. When later spec versions promote those semantics to native columns, the extension columns can be mapped over mechanically.

Install

go install github.com/baselinehq/focus-exporter/cmd/focus-exporter@latest

Or build from source:

git clone https://github.com/baselinehq/focus-exporter
cd focus-exporter
go build ./cmd/focus-exporter

Requires Go 1.26+. Dependencies are kept minimal: golang.org/x/text and shopspring/decimal, plus google.golang.org/grpc and protobuf for the one provider (Modal) whose billing API is gRPC-only.

Usage

focus-exporter --provider planetscale --month 2026-07 --format json

Flags:

Flag Meaning Default
--provider Provider to export (repeatable) required
--month YYYY-MM, expands to that month's [start, end) -
--start / --end Explicit window, RFC3339 or YYYY-MM-DD -
--format json or csv json
-o, --out Output file stdout
--tenant-map JSON {resourceId: owner} string attributing spend to sub-accounts -

Provide either --month or --start/--end together (not both, and --start without --end is rejected). With no window flags at all, the exporter fetches the entire period the provider makes available - except providers whose API mandates a start time (the AI/streaming ones), which require an explicit window.

Every emitted record is validated against the FOCUS 1.2 mandatory-column rules before it is written; a record that would violate them fails the run rather than producing non-conformant output. A provider that fails at fetch time is logged to stderr and skipped; a provider that cannot be built at all (unknown name or missing credentials) is a fatal error, so a misconfigured export never silently produces an empty file.

Tenancy. When one billing account fronts many downstream owners - a gateway tenant tagged per request, or resources leased and re-sold - spend is split into the FOCUS sub-account columns (SubAccountId/Name/Type) while the billing account stays the account holder. Two ways in: adapter-native metadata (Helicone HELICONE_TENANT_PROPERTY, RespanAI RESPANAI_TENANT_FIELD, OpenAI OPENAI_TENANT_GROUP=project_id, Anthropic ANTHROPIC_TENANT_GROUP=workspace_id), or --tenant-map (a {resourceId: owner} JSON string applied to every provider - the re-seller case). Opt-in; see docs/tenancy.md.

Example output (PlanetScale)

{
  "BilledCost": "30.0",
  "EffectiveCost": "30.0",
  "ListCost": "30.0",
  "BillingCurrency": "USD",
  "BillingPeriodStart": "2026-07-01T00:00:00Z",
  "BillingPeriodEnd": "2026-08-01T00:00:00Z",
  "ChargePeriodStart": "2026-07-01T00:00:00Z",
  "ChargePeriodEnd": "2026-08-01T00:00:00Z",
  "Provider": "PlanetScale",
  "ServiceName": "PlanetScale",
  "ServiceCategory": "Databases",
  "ServiceSubcategory": "Managed Database",
  "ChargeCategory": "Usage",
  "ResourceId": "7p5sqtyldrf6",
  "ResourceName": "pscale-1",
  "ResourceType": "Database",
  "RegionId": "us-east",
  "RegionName": "AWS us-east-1",
  "SkuId": "scaler_pro",
  "SkuMeter": "PS_10_AWS_ARM",
  "ChargeDescription": "PS-10-AWS-ARM for branch 'main'",
  "SkuPriceId": "scaler_pro|PS_10_AWS_ARM",
  "x_InfraProvider": "AWS"
}

FOCUS record type

pkg/focus holds the canonical FOCUS 1.2 record. The struct is generated from pkg/focus/columns.json - the single source of truth for the column set and each column's data type and nullability - by a repo-owned generator:

go generate ./...

Do not hand-edit pkg/focus/record_gen.go. To change the column set, edit columns.json and regenerate. A round-trip test asserts a full FOCUS 1.2 example survives marshal/unmarshal through the typed struct without losing a column.

Adding a provider

See AGENTS.md for the full conventions. In short:

  1. Create pkg/integrations/<provider>/ with a New(get integrations.HTTPGet, ...creds) integrations.Source constructor and a Fetch(ctx, start, end) ([]model.UsageRecord, error) that fills the FOCUS-core fields on model.UsageRecord (and Extensions for x_ columns).
  2. Expose a package-level var Provider = integrations.Provider{...} carrying Name (the --provider key), DisplayName, Slug, Category, Grain, DocsURL, and an Env entry per variable the factory reads, marking each Required and Secret. Add it to Providers() in pkg/integrations/registry (one line, no factory in the CLI). Set FormerName when a provider has rebranded.
  3. Add docs/providers/<provider>.md, a row to the Integrations table above, and hermetic tests beside the package (pkg/integrations/<provider>/<provider>_test.go), with fixtures under pkg/integrations/<provider>/testdata/.

model.UsageRecord is domain-agnostic: infrastructure providers fill the resource/region/period fields, and model providers add token detail through the Extensions bag.

Provider catalog

pkg/integrations/registry exposes what this exporter supports so other tools do not re-type the list:

registry.Providers()   // every provider, sorted, with metadata
registry.Names()       // just the --provider keys
registry.Get(slug)     // look one up
registry.Default()     // the registry the CLI builds

Each entry carries its display name, category, exported grain, docs link, and the environment variables it reads with Required and Secret flags - enough for a consumer to render a setup form without hardcoding anything. CostGraph serves this to its dashboard.

Layout

cmd/focus-exporter/          CLI (flags, provider registry, window guard, sinks)
pkg/focus/                   canonical FOCUS 1.2 Record (generated) + mapper + validator
pkg/model/                   UsageRecord (domain-agnostic) + typed FOCUS enums
pkg/sink/                    Sink interface + JSON/CSV writers
pkg/integrations/            Source interface + HTTPGet + Registry + Provider descriptor
pkg/integrations/<name>/     one adapter per provider
internal/gen/                FOCUS-type generator (go:generate)

Development

go generate ./...
go build ./...
go vet ./...
go test -race ./...

Roadmap

  • More provider adapters across AI, data, database, observability, and comms categories.
  • A stable synthetic record id (x_LineItemId) so downstream ingestion can dedup without relying on field values, which FOCUS does not guarantee to be unique.

Contributing

Contributions are welcome. See CONTRIBUTING.md for the development setup, the checks a change has to pass, and how to add a provider. Participation is governed by our Code of Conduct.

Security issues: please follow SECURITY.md and do not open a public issue. Release notes live in CHANGELOG.md.

License

focus-exporter is dual-licensed:

  • Open source: GNU AGPL-3.0. Free to use, modify, and redistribute; if you modify it and distribute it or offer it over a network, you must release your changes under the AGPL.
  • Commercial: for organizations that cannot meet the AGPL's terms (embedding in a closed-source product, offering a proprietary/hosted service without releasing source, etc.). Contact support@costgraph.ai.

See LICENSING.md for details and NOTICE.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL