[SPIKE] OSAC sovereign cloud integration — usage line item table - #6178
[SPIKE] OSAC sovereign cloud integration — usage line item table#6178martinpovolny wants to merge 6 commits into
Conversation
Add rules distilled from the RTU (rates_to_usage) production incident, 3 months of commit history analysis, Slack incident review, and insights-host-inventory patterns: - Quick reference section (key commands, project structure, dev stack) - Feature flags: gate risky changes behind Unleash (default OFF) - SQL templates: document all 3 directories and sync rules - API changes: require OpenAPI spec updates - Migrations: one per PR, use AddIndexConcurrently for large tables - Partitioned tables: Django FK constraints gotchas with cascade_delete() - On-prem parity: test with ONPREM=True and ONPREM=False - "When modifying..." cross-file sync table
Share Claude Code configuration across the team by checking in .claude/. ## What's included - .claude/settings.json — session-start hook config - .claude/hooks/session_start.py — loads .env, checks git branch freshness - .claude/commands/koku-doctor.md — dev environment health check - .claude/commands/koku-prime.md — codebase orientation for new sessions - .claude/commands/koku-sql-check.md — verify trino_sql ↔ self_hosted_sql sync - .claude/rules/cost-pipeline.md — OCP cost pipeline call chain reference - .claude/rules/sql-templates.md — 13-file mirror list between SQL dirs - .claude/rules/partitioned-tables.md — FK constraint gotchas - .gitignore — un-ignore .claude/ (was fully ignored), keep settings.local.json ignored ## .gitignore change: migrating personal settings Previously .claude/ was fully gitignored. Now only .claude/settings.local.json is ignored (for personal permission overrides). If you already have a .claude/settings.json with personal settings: 1. Rename it: mv .claude/settings.json .claude/settings.local.json 2. Pull this branch — the shared settings.json will appear 3. Your settings.local.json merges on top automatically If you have custom commands in .claude/commands/ with the same names as the new shared ones, your local versions will be overwritten by git pull. Back them up first if needed. The shared settings.json only configures a session-start hook. Personal permission allowlists stay in settings.local.json (already gitignored).
Covers interactive and one-shot modes, cluster/namespace reference, schema structure (public vs orgNNNNNN), common queries (customer lookup, cost model investigation, cross-tenant patterns), and curl fallback.
Adds a new self-hosted model (OSACUsageLineItemDaily) and a conditional
UNION in the OCP summarization SQL template to process OSAC data from
the cost-event-consumer pipeline.
This is a spike/PoC — not production-ready. See the cost_ai_grid_poc
repo for the full integration design and results.
Changes:
- New model: OSACUsageLineItemDaily in self_hosted_models.py
- New UNION: conditional on {% if osac_exists %} in the summarization SQL
- Wiring: osac_exists table check in ocp_report_db_accessor.py
Related: myersCody/cost_ai_grid_poc#44
There was a problem hiding this comment.
Code Review
This pull request introduces support for OSAC Sovereign Cloud daily usage data in the self-hosted/on-prem pipeline, including a new OSACUsageLineItemDaily model, a table existence check in ocp_report_db_accessor.py, and a UNION clause in the daily summary SQL template. It also adds extensive development documentation and tooling configurations for Claude Code. The reviewer feedback highlights two important issues: first, the OSAC query in the daily summary SQL template lacks a GROUP BY aggregation, which will cause duplicate rows, and discards the cost_amount data; second, the table existence check for the OSAC table should be gated to run only in on-prem mode, as it is a PostgreSQL-only table and does not exist in Trino.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| SELECT null as uuid, | ||
| {{report_period_id}} as report_period_id, | ||
| {{cluster_id}} as cluster_id, | ||
| {{cluster_alias}} as cluster_alias, | ||
| 'OSAC' as data_source, | ||
| o.usage_start, | ||
| o.usage_start as usage_end, | ||
| COALESCE(o.project_id, o.tenant_id, '') as namespace, | ||
| COALESCE(o.resource_id, '') as node, | ||
| COALESCE(o.resource_id, '') as resource_id, | ||
| '{}'::text as pod_labels, | ||
| -- CPU: map vm_cpu_core_seconds to core-hours | ||
| CASE WHEN o.meter_name IN ('vm_cpu_core_seconds') | ||
| THEN o.value / 3600.0 END as pod_usage_cpu_core_hours, | ||
| CASE WHEN o.meter_name IN ('vm_cpu_core_seconds') | ||
| THEN o.value / 3600.0 END as pod_request_cpu_core_hours, | ||
| CASE WHEN o.meter_name IN ('vm_cpu_core_seconds') | ||
| THEN o.value / 3600.0 END as pod_effective_usage_cpu_core_hours, | ||
| NULL as pod_limit_cpu_core_hours, | ||
| -- Memory: map vm_memory_gib_seconds to gigabyte-hours | ||
| CASE WHEN o.meter_name IN ('vm_memory_gib_seconds') | ||
| THEN o.value / 3600.0 END as pod_usage_memory_gigabyte_hours, | ||
| CASE WHEN o.meter_name IN ('vm_memory_gib_seconds') | ||
| THEN o.value / 3600.0 END as pod_request_memory_gigabyte_hours, | ||
| CASE WHEN o.meter_name IN ('vm_memory_gib_seconds') | ||
| THEN o.value / 3600.0 END as pod_effective_usage_memory_gigabyte_hours, | ||
| NULL as pod_limit_memory_gigabyte_hours, | ||
| -- Node capacity (from uptime meters) | ||
| NULL as node_capacity_cpu_cores, | ||
| CASE WHEN o.meter_name IN ('vm_uptime_seconds', 'bm_uptime_seconds') | ||
| THEN o.value / 3600.0 END as node_capacity_cpu_core_hours, | ||
| NULL as node_capacity_memory_gigabytes, | ||
| NULL as node_capacity_memory_gigabyte_hours, | ||
| NULL as cluster_capacity_cpu_core_hours, | ||
| NULL as cluster_capacity_memory_gigabyte_hours, | ||
| -- No storage data from OSAC | ||
| NULL as persistentvolumeclaim, | ||
| NULL as persistentvolume, | ||
| NULL as storageclass, | ||
| '{}'::text as volume_labels, | ||
| NULL as persistentvolumeclaim_capacity_gigabyte, | ||
| NULL as persistentvolumeclaim_capacity_gigabyte_months, | ||
| NULL as volume_request_storage_gigabyte_months, | ||
| NULL as persistentvolumeclaim_usage_gigabyte_months, | ||
| o.source::uuid as source_uuid, | ||
| NULL as infrastructure_usage_cost, | ||
| NULL as csi_volume_handle, | ||
| NULL as cost_category_id, | ||
| o.source, | ||
| o.year, | ||
| o.month, | ||
| EXTRACT(DAY FROM o.usage_start)::text as day | ||
| FROM {{schema | sqlsafe}}.openshift_osac_usage_line_items_daily o | ||
| WHERE o.source = {{source}} | ||
| AND o.year = {{year}} | ||
| AND lpad(o.month, 2, '0') = {{month}} | ||
| AND o.usage_start >= {{start_date}} | ||
| AND o.usage_start < {{end_date}} + INTERVAL '1 day' | ||
|
|
There was a problem hiding this comment.
The current OSAC query does not perform any GROUP BY aggregation. Since openshift_osac_usage_line_items_daily stores daily metering data per meter (e.g., separate rows for CPU, memory, and uptime), querying it without aggregation will insert multiple separate rows for the same resource on the same day into the daily summary table. This significantly increases the row count and degrades query performance.
Additionally, the pre-calculated cost_amount from the OSAC table is currently mapped to NULL for infrastructure_usage_cost, which discards the actual cost data.
We should aggregate the metrics using SUM and group by the resource and day, while also mapping cost_amount to infrastructure_usage_cost as a JSON object.
SELECT null as uuid,
{{report_period_id}} as report_period_id,
{{cluster_id}} as cluster_id,
{{cluster_alias}} as cluster_alias,
'OSAC' as data_source,
o.usage_start,
o.usage_start as usage_end,
COALESCE(o.project_id, o.tenant_id, '') as namespace,
COALESCE(o.resource_id, '') as node,
COALESCE(o.resource_id, '') as resource_id,
'{}'::text as pod_labels,
-- CPU: map vm_cpu_core_seconds to core-hours
SUM(CASE WHEN o.meter_name IN ('vm_cpu_core_seconds')
THEN o.value / 3600.0 END) as pod_usage_cpu_core_hours,
SUM(CASE WHEN o.meter_name IN ('vm_cpu_core_seconds')
THEN o.value / 3600.0 END) as pod_request_cpu_core_hours,
SUM(CASE WHEN o.meter_name IN ('vm_cpu_core_seconds')
THEN o.value / 3600.0 END) as pod_effective_usage_cpu_core_hours,
NULL as pod_limit_cpu_core_hours,
-- Memory: map vm_memory_gib_seconds to gigabyte-hours
SUM(CASE WHEN o.meter_name IN ('vm_memory_gib_seconds')
THEN o.value / 3600.0 END) as pod_usage_memory_gigabyte_hours,
SUM(CASE WHEN o.meter_name IN ('vm_memory_gib_seconds')
THEN o.value / 3600.0 END) as pod_request_memory_gigabyte_hours,
SUM(CASE WHEN o.meter_name IN ('vm_memory_gib_seconds')
THEN o.value / 3600.0 END) as pod_effective_usage_memory_gigabyte_hours,
NULL as pod_limit_memory_gigabyte_hours,
-- Node capacity (from uptime meters)
NULL as node_capacity_cpu_cores,
SUM(CASE WHEN o.meter_name IN ('vm_uptime_seconds', 'bm_uptime_seconds')
THEN o.value / 3600.0 END) as node_capacity_cpu_core_hours,
NULL as node_capacity_memory_gigabytes,
NULL as node_capacity_memory_gigabyte_hours,
NULL as cluster_capacity_cpu_core_hours,
NULL as cluster_capacity_memory_gigabyte_hours,
-- No storage data from OSAC
NULL as persistentvolumeclaim,
NULL as persistentvolume,
NULL as storageclass,
'{}'::text as volume_labels,
NULL as persistentvolumeclaim_capacity_gigabyte,
NULL as persistentvolumeclaim_capacity_gigabyte_months,
NULL as volume_request_storage_gigabyte_months,
NULL as persistentvolumeclaim_usage_gigabyte_months,
o.source::uuid as source_uuid,
json_build_object('raw', SUM(COALESCE(o.cost_amount, 0.0)))::text as infrastructure_usage_cost,
NULL as csi_volume_handle,
NULL as cost_category_id,
o.source,
o.year,
o.month,
EXTRACT(DAY FROM o.usage_start)::text as day
FROM {{schema | sqlsafe}}.openshift_osac_usage_line_items_daily o
WHERE o.source = {{source}}
AND o.year = {{year}}
AND lpad(o.month, 2, '0') = {{month}}
AND o.usage_start >= {{start_date}}
AND o.usage_start < {{end_date}} + INTERVAL '1 day'
GROUP BY o.usage_start,
COALESCE(o.project_id, o.tenant_id, ''),
COALESCE(o.resource_id, ''),
o.source,
o.year,
o.month| storage_exists = trino_table_exists(self.schema, "openshift_storage_usage_line_items_daily") | ||
| osac_exists = trino_table_exists(self.schema, "openshift_osac_usage_line_items_daily") |
There was a problem hiding this comment.
Checking for the existence of openshift_osac_usage_line_items_daily using trino_table_exists is inappropriate and risky because this table is a self-hosted PostgreSQL-only table (defined in self_hosted_models.py) and does not exist in Trino. In SaaS mode (where ONPREM is False), this check will unnecessarily query Trino, causing overhead.
We should gate this check so that it is only performed when running in on-prem/self-hosted mode.
| storage_exists = trino_table_exists(self.schema, "openshift_storage_usage_line_items_daily") | |
| osac_exists = trino_table_exists(self.schema, "openshift_osac_usage_line_items_daily") | |
| is_onprem = self.get_sql_folder_name() == "self_hosted_sql" | |
| storage_exists = trino_table_exists(self.schema, "openshift_storage_usage_line_items_daily") | |
| osac_exists = is_onprem and trino_table_exists(self.schema, "openshift_osac_usage_line_items_daily") |
|
🤖 CI Triager — Diagnosis Check: Evidence: Action: Run pipenv run make make-migrations
git add koku/reporting/migrations/0352_osacusagelineitemdaily.py
git commit -m "Add migration for OSACUsageLineItemDaily model"Generated automatically. Review before applying. |
|
🤖 CI Triager — Diagnosis Check: Root cause: The Evidence: Action: Run pipenv run make make-migrations
git add koku/reporting/migrations/0352_osacusagelineitemdaily.py
git commit -m "Add migration for OSACUsageLineItemDaily model"Generated automatically. Review before applying. |
|
🤖 CI Triager — Diagnosis Check: Root cause: The PipelineRun failed with Evidence: Action: This is a Konflux infrastructure issue. Re-trigger the pipeline by pushing a new commit or asking a maintainer to re-run the check. Note: the Generated automatically. Review before applying. |
| CASE WHEN o.meter_name IN ('vm_cpu_core_seconds') | ||
| THEN o.value / 3600.0 END as pod_request_cpu_core_hours, | ||
| CASE WHEN o.meter_name IN ('vm_cpu_core_seconds') | ||
| THEN o.value / 3600.0 END as pod_effective_usage_cpu_core_hours, |
There was a problem hiding this comment.
Overall I really like the direction you are taking with a new table & a new data type. That feels correct to me.
I don't know much nit picking you want me to do, but I will say this section doesn't really match the cost model feasibility doc.
If you are just trying to showcase we can connect dots, then feel free to ignore this comment.
| unit = models.CharField(max_length=64, null=True) | ||
| cost_type = models.CharField(max_length=32, null=True) | ||
| koku_metric = models.CharField(max_length=128, null=True) | ||
| cost_amount = models.FloatField(null=True) |
There was a problem hiding this comment.
We calculated the cost on the POC side, but don't seem to do anything with it on this side of the fence.
Are you just trying to insert metering information on this side and utilize the cost model to populate the costs?
If we want to maintain the cost calculated within the POC we could explore a similar flow to to back populate costs to the ocp daily summary table like what we do with cloud infrastructure costs.
koku-ci-triager-bot
left a comment
There was a problem hiding this comment.
🤖 CI Triager Suggestion
Check: Sanity
Root cause: reorder-python-imports and black require from django.db import migrations, models to be split into separate imports.
Accept the suggestion below with one click.
| # Generated by Django 5.2.15 on 2026-07-11 10:46 | ||
|
|
||
| import uuid | ||
| from django.db import migrations, models |
There was a problem hiding this comment.
reorder-python-imports modified this file because it requires each import to be on its own line.
| from django.db import migrations, models | |
| from django.db import migrations | |
| from django.db import models |
|
🤖 CI Triager — Diagnosis Check: Root cause: Evidence: Action: This PR modifies production OCP code ( Generated automatically. Review before applying. |
koku-ci-triager-bot
left a comment
There was a problem hiding this comment.
🤖 CI Triager — Suggestion
Check: Units - 3.11
Commit: fef4808
Root cause: test_postgres_tables_are_partitioned fails because OSACUsageLineItemDaily inherits OCPLineItemBase which defines a PartitionInfo inner class. Django models with PartitionInfo must be created via a partitioned migration — the migration must call set_pg_extended_mode before and unset_pg_extended_mode after CreateModel. The migration was auto-generated without these calls, so PostgreSQL creates a plain (non-partitioned) table.
Evidence:
FAIL: test_postgres_tables_are_partitioned (...) (model=<class 'reporting.provider.ocp.self_hosted_models.OSACUsageLineItemDaily'>)
AssertionError: False is not true : Model {model.__name__} is not partitioned.
Are `set_pg_extended_mode` and `unset_pg_extended_mode` used in the model's migration?
Fix: Add the RunPython calls around CreateModel as in 0351_create_ocp_cost_breakdown_p.py.
Accept the suggestion below to apply the fix.
| # Generated by Django 5.2.15 on 2026-07-11 10:46 | ||
|
|
||
| import uuid | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('reporting', '0351_create_ocp_cost_breakdown_p'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='OSACUsageLineItemDaily', | ||
| fields=[ | ||
| ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), | ||
| ('report_period_start', models.DateTimeField(null=True)), | ||
| ('report_period_end', models.DateTimeField(null=True)), | ||
| ('interval_start', models.DateTimeField(db_index=True, null=True)), | ||
| ('interval_end', models.DateTimeField(null=True)), | ||
| ('usage_start', models.DateField(db_index=True, null=True)), | ||
| ('source', models.CharField(db_index=True, max_length=64, null=True)), | ||
| ('year', models.CharField(max_length=4, null=True)), | ||
| ('month', models.CharField(max_length=2, null=True)), | ||
| ('manifestid', models.CharField(max_length=256, null=True)), | ||
| ('reportnumhours', models.IntegerField(null=True)), | ||
| ('resource_type', models.CharField(max_length=64, null=True)), | ||
| ('resource_id', models.CharField(max_length=256, null=True)), | ||
| ('tenant_id', models.CharField(max_length=256, null=True)), | ||
| ('project_id', models.CharField(max_length=256, null=True)), | ||
| ('meter_name', models.CharField(max_length=128, null=True)), | ||
| ('value', models.FloatField(null=True)), | ||
| ('unit', models.CharField(max_length=64, null=True)), | ||
| ('cost_type', models.CharField(max_length=32, null=True)), | ||
| ('koku_metric', models.CharField(max_length=128, null=True)), | ||
| ('cost_amount', models.FloatField(null=True)), | ||
| ('currency', models.CharField(max_length=8, null=True)), | ||
| ], | ||
| options={ | ||
| 'db_table': 'openshift_osac_usage_line_items_daily', | ||
| 'indexes': [models.Index(fields=['source', 'year', 'month'], name='osac_daily_src_yr_mo_idx')], | ||
| }, | ||
| ), | ||
| ] |
There was a problem hiding this comment.
| # Generated by Django 5.2.15 on 2026-07-11 10:46 | |
| import uuid | |
| from django.db import migrations, models | |
| class Migration(migrations.Migration): | |
| dependencies = [ | |
| ('reporting', '0351_create_ocp_cost_breakdown_p'), | |
| ] | |
| operations = [ | |
| migrations.CreateModel( | |
| name='OSACUsageLineItemDaily', | |
| fields=[ | |
| ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), | |
| ('report_period_start', models.DateTimeField(null=True)), | |
| ('report_period_end', models.DateTimeField(null=True)), | |
| ('interval_start', models.DateTimeField(db_index=True, null=True)), | |
| ('interval_end', models.DateTimeField(null=True)), | |
| ('usage_start', models.DateField(db_index=True, null=True)), | |
| ('source', models.CharField(db_index=True, max_length=64, null=True)), | |
| ('year', models.CharField(max_length=4, null=True)), | |
| ('month', models.CharField(max_length=2, null=True)), | |
| ('manifestid', models.CharField(max_length=256, null=True)), | |
| ('reportnumhours', models.IntegerField(null=True)), | |
| ('resource_type', models.CharField(max_length=64, null=True)), | |
| ('resource_id', models.CharField(max_length=256, null=True)), | |
| ('tenant_id', models.CharField(max_length=256, null=True)), | |
| ('project_id', models.CharField(max_length=256, null=True)), | |
| ('meter_name', models.CharField(max_length=128, null=True)), | |
| ('value', models.FloatField(null=True)), | |
| ('unit', models.CharField(max_length=64, null=True)), | |
| ('cost_type', models.CharField(max_length=32, null=True)), | |
| ('koku_metric', models.CharField(max_length=128, null=True)), | |
| ('cost_amount', models.FloatField(null=True)), | |
| ('currency', models.CharField(max_length=8, null=True)), | |
| ], | |
| options={ | |
| 'db_table': 'openshift_osac_usage_line_items_daily', | |
| 'indexes': [models.Index(fields=['source', 'year', 'month'], name='osac_daily_src_yr_mo_idx')], | |
| }, | |
| ), | |
| ] | |
| # Generated by Django 5.2.15 on 2026-07-11 10:46 | |
| import uuid | |
| from django.db import migrations, models | |
| from koku.database import set_pg_extended_mode | |
| from koku.database import unset_pg_extended_mode | |
| class Migration(migrations.Migration): | |
| dependencies = [ | |
| ("reporting", "0351_create_ocp_cost_breakdown_p"), | |
| ] | |
| operations = [ | |
| migrations.RunPython(code=set_pg_extended_mode, reverse_code=unset_pg_extended_mode), | |
| migrations.CreateModel( | |
| name="OSACUsageLineItemDaily", | |
| fields=[ | |
| ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), | |
| ("report_period_start", models.DateTimeField(null=True)), | |
| ("report_period_end", models.DateTimeField(null=True)), | |
| ("interval_start", models.DateTimeField(db_index=True, null=True)), | |
| ("interval_end", models.DateTimeField(null=True)), | |
| ("usage_start", models.DateField(db_index=True, null=True)), | |
| ("source", models.CharField(db_index=True, max_length=64, null=True)), | |
| ("year", models.CharField(max_length=4, null=True)), | |
| ("month", models.CharField(max_length=2, null=True)), | |
| ("manifestid", models.CharField(max_length=256, null=True)), | |
| ("reportnumhours", models.IntegerField(null=True)), | |
| ("resource_type", models.CharField(max_length=64, null=True)), | |
| ("resource_id", models.CharField(max_length=256, null=True)), | |
| ("tenant_id", models.CharField(max_length=256, null=True)), | |
| ("project_id", models.CharField(max_length=256, null=True)), | |
| ("meter_name", models.CharField(max_length=128, null=True)), | |
| ("value", models.FloatField(null=True)), | |
| ("unit", models.CharField(max_length=64, null=True)), | |
| ("cost_type", models.CharField(max_length=32, null=True)), | |
| ("koku_metric", models.CharField(max_length=128, null=True)), | |
| ("cost_amount", models.FloatField(null=True)), | |
| ("currency", models.CharField(max_length=8, null=True)), | |
| ], | |
| options={ | |
| "db_table": "openshift_osac_usage_line_items_daily", | |
| "indexes": [models.Index(fields=["source", "year", "month"], name="osac_daily_src_yr_mo_idx")], | |
| }, | |
| ), | |
| migrations.RunPython(code=unset_pg_extended_mode, reverse_code=set_pg_extended_mode), | |
| ] |
|
🤖 CI Triager — Diagnosis Check: Root cause: The Evidence: Action: Run Generated automatically. Review before applying. |
|
🤖 CI Triager — Diagnosis Check: Root cause: The new Evidence: Action: Choose one of:
Generated automatically. Review before applying. |
Summary
Spike/PoC for integrating OSAC sovereign cloud cost data into Koku's
on-prem pipeline. Adds a new self-hosted line item table for OSAC data
and wires it into the existing OCP summarization SQL.
This is a spike — not production-ready. The full design, adversarial
review, and test results are in the cost_ai_grid_poc repo:
myersCody/cost_ai_grid_poc#44
Changes
New model:
OSACUsageLineItemDailykoku/reporting/provider/ocp/self_hosted_models.pyA new self-hosted model for daily OSAC usage data (VMs, clusters, bare
metal, MaaS). Written by the
koku-synctool from the cost-event-consumerpipeline. Columns: resource_type, resource_id, tenant_id, meter_name,
value, unit, cost_type, koku_metric, cost_amount, currency.
SQL template UNION
koku/masu/database/self_hosted_sql/openshift/reporting_ocpusagelineitem_daily_summary.sqlConditional
{% if osac_exists %}UNION block that reads from the OSACtable and maps meters to Koku's daily summary columns (cpu core-hours,
memory GiB-hours, infrastructure cost).
DB accessor wiring
koku/masu/database/ocp_report_db_accessor.pyAdds
osac_existstable check and passes it to the SQL template params.Verified
Data flows end-to-end:
Open Design Questions (from rate incorporation research)
Based on Cody's rate incorporation research
(docs):
1.
source_typediscriminator on PriceListRecommendation: extend Koku's
PriceListmodel with asource_typefield (
operatorvsevent_watcher) to scope rate definitions.Why: OSAC event-driven meters don't map 1:1 to Prometheus-based
metrics. 7 Koku metrics (usage, effective-usage, storage, GPU) are
permanently Prometheus-only and should not appear in OSAC price lists.
A
source_typediscriminator structurally excludes them, gives REQ-13custom metrics a clean home, and preserves Koku's existing markup/
distribution/UI machinery.
Status: Proposed — needs design review.
2. Double-billing prevention
Problem: when both Prometheus (via koku-metrics-operator) and our
event watcher meter the same resource (VM, cluster), costs are counted
twice. The
source_typediscriminator alone doesn't prevent this.Needed: a precedence mechanism that picks one source per resource,
or deduplicates at the summary layer. Not urgent for PoC (single
source), but critical for production where both pipelines may coexist.
3. Node/cluster cost allocation model
Koku's approach: ratio-based allocation — distributes node costs
proportionally across pods/projects based on resource requests.
OSAC's approach: flat per-instance accrual — each VM/cluster gets
a fixed hourly rate based on its instance type.
Decision needed: do we match Koku's allocation model for OSAC
resources, or accept the divergence? Matching would require significant
rework in the cost-event-consumer; accepting means OSAC costs won't
align with Prometheus-based OCP costs at the project level.
4. Metric-level findings (fixed)
→ renamed tonode_cost_per_monthrates with hourly divisorsnode_cost_per_hour(fixed in cost_ai_grid_poc)CPU/memory meters produce $0 when OSAC drops cores/memory_gib→ enriched from instance_type catalog (fixed in cost_ai_grid_poc)
vm_cost_per_hour— clean 1:1 mapping, works as-iscluster_worker_node_countmeter — referenced in design docs butnever emitted in code. Decision: implement or remove.
Related
docs/koku-integration/strategy.mdin PR Add basic tox.ini file for running different envs and linting. Closes #30 #44