Skip to content

[Hubs] msexports_ExecuteETL drops ReservationDetails ingestion when manifest dataRowCount is null (manifestVersion 2024-04-01) #2173

Description

@haflidif

🐛 Problem

msexports_ExecuteETL silently skips all Cost Management ReservationDetails ingestion when the manifest emits dataRowCount: null (the default for manifestVersion: 2024-04-01). The Set Has No Rows SetVariable activity sets hasNoRows = true, which causes the downstream Detect Channel switch to route to "ignore", skipping every Copy/Ingest activity. The pipeline reports Succeeded, so there's no operator signal that data was dropped.

Affects FinOps hubs v14.0 (and likely earlier v13.x — same expression is in main).

Current expression in src/templates/finops-hub/modules/Microsoft.CostManagement/Exports/app.bicep for the Set Has No Rows activity:

@or(
  equals(activity('Read Manifest').output.firstRow.blobCount, null),
  equals(activity('Read Manifest').output.firstRow.blobCount, 0),
  and(
    contains(activity('Read Manifest').output.firstRow, 'dataRowCount'),
    or(
      equals(activity('Read Manifest').output.firstRow.dataRowCount, null),   <-- treats null as zero
      equals(activity('Read Manifest').output.firstRow.dataRowCount, 0)
    )
  )
)

manifestVersion: 2024-04-01 emits dataRowCount: null to mean "row count not pre-computed", not "zero rows". blobCount and byteCount are always populated when data exists, so it's incorrect to treat null as zero rows.

👣 Repro steps

  1. Create a Cost Management export of type ReservationDetails (dataVersion: 2023-03-01) on an EA billing account scope (Microsoft.Billing/billingAccounts/{enrollment}).
  2. Let it run on its daily schedule, or trigger any "Export selected dates" run.
  3. Inspect storage at msexports/billingAccounts/{enrollment}/{exportName}/{YYYYMMDD-YYYYMMDD}/{run}/{guid}/manifest.json — manifest shows manifestVersion: 2024-04-01, byteCount > 0, blobCount: 1, dataRowCount: null, with a real CSV alongside.
  4. In ADF, the msexports_ExecuteETL run reports Succeeded. Drill into activity outputs:
    • Set Has No RowshasNoRows = true
    • Detect Channel → output "ignore"
    • Copy CSV → Parquet activities — skipped
  5. Confirm CommitmentDiscountUsage_raw (and therefore CommitmentDiscountUsage_final_v1_2) in ADX stays empty despite multiple succeeded export runs.

Reproduced on FinOps hubs v14.0 with dataVersion: 2023-03-01, apiVersion: 2025-03-01, EA billing account scope.

🤔 Expected

The pipeline should ingest the data because real data exists (byteCount > 0, blobCount: 1, real CSV in storage). Either:

  • Remove the dataRowCount check entirely and rely on blobCount (and optionally byteCount), or
  • Stop treating dataRowCount: null as 0 — only the explicit numeric value 0 should mean "no rows".

Suggested replacement expression (simplest):

@or(
  equals(coalesce(activity('Read Manifest').output.firstRow.blobCount, 0), 0),
  equals(coalesce(activity('Read Manifest').output.firstRow.byteCount, 0), 0)
)

Alternative if you want to preserve the dataRowCount short-circuit for older manifest versions:

@or(
  equals(coalesce(activity('Read Manifest').output.firstRow.blobCount, 0), 0),
  and(
    contains(activity('Read Manifest').output.firstRow, 'dataRowCount'),
    equals(activity('Read Manifest').output.firstRow.dataRowCount, 0)   // explicit zero only
  )
)

🔧 Environment

  • FinOps hub version: 14.0
  • Billing account type: EA (Enterprise Agreement)
  • Power BI report type: ADX (Azure Data Explorer)
  • Cost Management export: ReservationDetails, dataVersion: 2023-03-01, apiVersion: 2025-03-01, manifestVersion: 2024-04-01

ℹ️ Additional context

Sample failing manifest:

{
  "manifestVersion": "2024-04-01",
  "byteCount": 1972417,
  "blobCount": 1,
  "dataRowCount": null,
  "exportConfig": {
    "exportName": "...-daily-reservationdetails",
    "dataVersion": "2023-03-01",
    "apiVersion": "2025-03-01",
    "type": "ReservationDetails",
    "timeFrame": "MonthToDate",
    "granularity": "Daily"
  },
  "blobs": [
    { "blobName": ".../<file>.csv", "byteCount": 1972417 }
  ]
}

Activity outputs from a failing pipeline run:

  • Set Has No RowshasNoRows: true
  • Detect Channel"ignore"
  • All downstream Copy/Ingest activities skipped (zero data flows through)

Workaround we applied: edited the Set Has No Rows expression in ADF Studio to use blobCount + byteCount only and dropped the dataRowCount check. After publishing, we re-triggered msexports_ExecuteETL per historical month folder via az datafactory pipeline create-run. ~11 months / 21 runs reprocessed successfully and CommitmentDiscountUsage_final_v1_2 populated as expected (~40k rows so far, monthly cadence of ~5,500–6,500 rows per month, ~65–99 distinct reservations). The manual edit will be overwritten by the next Deploy-FinOpsHub, so an upstream fix would be very welcome.

Why this only affected ReservationDetails in our hub: other EA-scope exports on the same factory (FocusCost, PriceSheet, ReservationRecommendations, ReservationTransactions) kept ingesting correctly. Either their manifests still emit a numeric dataRowCount, or they omit the field entirely (which makes contains(...) false and skips the third branch).

🙋‍♀️ Ask for the community

We could use your help:

  1. Please vote this issue up (👍) to prioritize it.
  2. If you see CommitmentDiscountUsage_raw / CommitmentDiscountUsage_final_v1_2 empty in your hub despite the export running, drop a comment with your hub version, scope type, and the dataRowCount value from one of your ReservationDetails manifests.

Metadata

Metadata

Assignees

Labels

Needs: Attention 👋Issue or PR needs to be reviewed by the author or it will be closed due to no activitySkill: Data factoryData Factory integrationTool: FinOps hubsData pipeline solutionType: CleanupFixes to clean up any technical debt

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions