From 534d9c20664192bc8bc9492eb0ca2df80eebba9a Mon Sep 17 00:00:00 2001 From: William Poteat Date: Wed, 30 Oct 2024 11:30:16 -0400 Subject: [PATCH] SWATCH-2300: Add a metric for usage covered by a contract SWATCH-2301: Add a metric for usage considered billable --- .../usage/services/BillableUsageService.java | 40 ++++++++++++- .../services/BillableUsageServiceTest.java | 60 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/services/BillableUsageService.java b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/services/BillableUsageService.java index df9ad278c1..8a15d93043 100644 --- a/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/services/BillableUsageService.java +++ b/swatch-billable-usage/src/main/java/com/redhat/swatch/billable/usage/services/BillableUsageService.java @@ -35,10 +35,13 @@ import com.redhat.swatch.billable.usage.services.model.Quantity; import com.redhat.swatch.configuration.registry.MetricId; import com.redhat.swatch.configuration.registry.SubscriptionDefinition; +import io.micrometer.core.instrument.MeterRegistry; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.candlepin.clock.ApplicationClock; @@ -52,10 +55,15 @@ public class BillableUsageService { private static final ContractCoverage DEFAULT_CONTRACT_COVERAGE = ContractCoverage.builder().total(0).gratis(false).build(); + protected static final String COVERED_USAGE_METRIC = + "rhsm-subscriptions.swatch_contract_usage_total"; + protected static final String BILLABLE_USAGE_METRIC = + "rhsm-subscriptions.swatch_billable_usage_total"; private final ApplicationClock clock; private final BillingProducer billingProducer; private final BillableUsageRemittanceRepository billableUsageRemittanceRepository; private final ContractsController contractsController; + private final MeterRegistry meterRegistry; public void submitBillableUsage(BillableUsage usage) { // transaction to store the usage into database @@ -93,7 +101,7 @@ public BillableUsage produceMonthlyBillable(BillableUsage usage) Quantity contractAmount = Quantity.fromContractCoverage(usage, contractCoverage.getTotal()); double applicableUsage = - Quantity.of(usage.getCurrentTotal()) + Quantity.of(usage.getCurrentTotal() != null ? usage.getCurrentTotal() : 0.0) .subtract(contractAmount) .positiveOrZero() // ignore usage less than the contract amount .getValue(); @@ -116,9 +124,15 @@ public BillableUsage produceMonthlyBillable(BillableUsage usage) if (usageCalc.getRemittedValue() > 0) { createRemittance(usage, usageCalc, contractCoverage); + // maybe here, maybe test for condition here } else { log.debug("Nothing to remit. Remittance record will not be created."); } + updateUsageMeter( + usage.getCurrentTotal() != null ? usage.getCurrentTotal() : 0.0, + totalRemitted, + usageCalc.getBillableValue(), + usage); // There were issues with transmitting usage to AWS since the cost event timestamps were in the // past. This modification allows us to send usage to AWS if we get it during the current hour @@ -243,4 +257,28 @@ private void createRemittance( contractCoverage.isGratis() ? BillableUsage.Status.GRATIS : BillableUsage.Status.PENDING); usage.setUuid(newRemittance.getUuid()); } + + private void updateUsageMeter( + double current, double totalRemitted, double billable, BillableUsage usage) { + if (usage.getProductId() == null + || usage.getMetricId() == null + || usage.getBillingProvider() == null + || usage.getStatus() == null) { + return; + } + List tags = + new ArrayList<>( + List.of( + "product_tag", usage.getProductId(), + "metric_id", usage.getMetricId(), + "billing_provider", usage.getBillingProvider().value(), + "status", usage.getStatus().value())); + double covered = current - totalRemitted - billable; + if (covered > 0) { + meterRegistry.counter(COVERED_USAGE_METRIC, tags.toArray(new String[0])).increment(covered); + } + if (billable > 0) { + meterRegistry.counter(BILLABLE_USAGE_METRIC, tags.toArray(new String[0])).increment(billable); + } + } } diff --git a/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/services/BillableUsageServiceTest.java b/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/services/BillableUsageServiceTest.java index d97d51ea7e..b6770492d1 100644 --- a/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/services/BillableUsageServiceTest.java +++ b/swatch-billable-usage/src/test/java/com/redhat/swatch/billable/usage/services/BillableUsageServiceTest.java @@ -20,7 +20,10 @@ */ package com.redhat.swatch.billable.usage.services; +import static com.redhat.swatch.billable.usage.services.BillableUsageService.BILLABLE_USAGE_METRIC; +import static com.redhat.swatch.billable.usage.services.BillableUsageService.COVERED_USAGE_METRIC; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; @@ -39,6 +42,8 @@ import com.redhat.swatch.configuration.registry.SubscriptionDefinitionRegistry; import com.redhat.swatch.configuration.registry.Variant; import com.redhat.swatch.configuration.util.MetricIdUtils; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectSpy; @@ -50,6 +55,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -87,6 +93,7 @@ class BillableUsageServiceTest { @Inject ApplicationClock clock; @Inject BillableUsageService service; + @Inject private MeterRegistry meterRegistry; private final SubscriptionDefinitionRegistry mockSubscriptionDefinitionRegistry = mock(SubscriptionDefinitionRegistry.class); @@ -102,6 +109,7 @@ void setup() { remittanceRepo.deleteAll(); // reset original subscription definition registry setSubscriptionDefinitionRegistry(originalReference); + meterRegistry.clear(); } @AfterEach @@ -121,6 +129,7 @@ void monthlyWindowNoCurrentRemittance() { thenRemittanceIsUpdated(usage, 1.0); thenUsageIsSent(usage, 1.0); + thenBillableMeterMatches(usage, 1.0); } @Test @@ -134,6 +143,7 @@ void monthlyWindowWithRemittanceUpdate() { thenRemittanceIsUpdated(usage, 2.0); thenUsageIsSent(usage, 2.0); + thenBillableMeterMatches(usage, 2.0); } @Test @@ -148,6 +158,7 @@ void monthlyWindowRemittanceMultipleOfBillingFactor() { // 4(Billing_factor) = 72 thenRemittanceIsUpdated(usage, 72.0); thenUsageIsSent(usage, 18.0); + thenBillableMeterMatches(usage, 18.0); } @Test @@ -160,6 +171,7 @@ void monthlyWindowWithNoRemittanceUpdate() { service.submitBillableUsage(usage); thenUsageIsSent(usage, 0.0); + thenBillableMeterMatches(usage, 0.0); } @Test @@ -190,6 +202,7 @@ void billingFactorAppliedInRecalculationEvenNumber() { thenRemittanceIsUpdated(usage, 12.0); thenUsageIsSent(usage, usage.getValue()); + thenBillableMeterMatches(usage, usage.getValue()); } @Test @@ -202,6 +215,7 @@ void billingFactorAppliedInRecalculation() { thenRemittanceIsUpdated(usage, 28.0); thenUsageIsSent(usage, usage.getValue()); + thenBillableMeterMatches(usage, usage.getValue()); } // Simulates progression through contract billing. @@ -630,6 +644,7 @@ private void performRemittanceTesting( boolean isContractEnabledTest) throws Exception { BillableUsage usage = givenInstanceHoursUsageForRosa(usageDate, currentUsage, currentUsage); + double expectedCoveredValue = currentUsage - currentRemittance - expectedBilledValue; givenExistingContractForUsage(usage); // Enable contracts for the current product. @@ -660,6 +675,8 @@ private void performRemittanceTesting( } thenUsageIsSent(usage, expectedBilledValue); + thenBillableMeterMatches(usage, expectedBilledValue); + thenCoveredMeterMatches(usage, expectedCoveredValue); } private void thenUsageIsSent(BillableUsage usage, double expectedValue) { @@ -675,6 +692,37 @@ private void thenUsageIsSent(BillableUsage usage, double expectedValue) { })); } + private void thenBillableMeterMatches(BillableUsage usage, double expectedBillableValue) { + + var billableMeter = + getUsageMetric( + BILLABLE_USAGE_METRIC, + usage.getProductId(), + usage.getMetricId(), + usage.getBillingProvider().value()); + if (expectedBillableValue > 0) { + assertEquals( + expectedBillableValue, billableMeter.get().measure().iterator().next().getValue()); + assertEquals(usage.getStatus().value(), billableMeter.get().getId().getTag("status")); + } else { + assertFalse(billableMeter.isPresent()); + } + } + + private void thenCoveredMeterMatches(BillableUsage usage, Double expectedCoveredValue) { + var coveredMeter = + getUsageMetric( + COVERED_USAGE_METRIC, + usage.getProductId(), + usage.getMetricId(), + usage.getBillingProvider().value()); + if (expectedCoveredValue > 0) { + assertEquals(expectedCoveredValue, coveredMeter.get().measure().iterator().next().getValue()); + } else { + assertFalse(coveredMeter.isPresent()); + } + } + private void thenUsageIsNotSent() { verify(producer, times(0)).produce(any()); } @@ -745,4 +793,16 @@ private static void setSubscriptionDefinitionRegistry(SubscriptionDefinitionRegi fail(e); } } + + private Optional getUsageMetric( + String metric, String productTag, String metricId, String billingProvider) { + return meterRegistry.getMeters().stream() + .filter( + m -> + metric.equals(m.getId().getName()) + && productTag.equals(m.getId().getTag("product_tag")) + && metricId.equals(m.getId().getTag("metric_id")) + && billingProvider.equals(m.getId().getTag("billing_provider"))) + .findFirst(); + } }