Skip to content

Commit

Permalink
SWATCH-2300: Add a metric for usage covered by a contract
Browse files Browse the repository at this point in the history
SWATCH-2301: Add a metric for usage considered billable
  • Loading branch information
wottop committed Nov 25, 2024
1 parent 1fbb6a4 commit f5695ff
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -93,7 +101,7 @@ public BillableUsage produceMonthlyBillable(BillableUsage usage)
Quantity<BillingUnit> 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();
Expand All @@ -119,6 +127,7 @@ public BillableUsage produceMonthlyBillable(BillableUsage usage)
} else {
log.debug("Nothing to remit. Remittance record will not be created.");
}
updateUsageMeter(usage, contractCoverage.getTotal(), usageCalc.getBillableValue());

// 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
Expand All @@ -135,7 +144,7 @@ public BillableUsage produceMonthlyBillable(BillableUsage usage)
return usage;
}

private ContractCoverage getContractCoverage(BillableUsage usage)
protected ContractCoverage getContractCoverage(BillableUsage usage)
throws ContractCoverageException {
try {
return contractsController.getContractCoverage(usage);
Expand Down Expand Up @@ -243,4 +252,31 @@ private void createRemittance(
contractCoverage.isGratis() ? BillableUsage.Status.GRATIS : BillableUsage.Status.PENDING);
usage.setUuid(newRemittance.getUuid());
}

private void updateUsageMeter(BillableUsage usage, double contractCoverage, double billable) {
if (usage.getProductId() == null
|| usage.getMetricId() == null
|| usage.getBillingProvider() == null
|| usage.getStatus() == null) {
return;
}
List<String> tags =
new ArrayList<>(
List.of(
"product_tag", usage.getProductId(),
"metric_id", usage.getMetricId(),
"billing_provider", usage.getBillingProvider().value(),
"status", usage.getStatus().value()));
log.error("This is the code calculated coverage from the contracts: {}", contractCoverage);
if (usage.getCurrentTotal() > 0) {
double coverage =
usage.getCurrentTotal() * usage.getBillingFactor() > contractCoverage
? contractCoverage
: usage.getCurrentTotal() * usage.getBillingFactor();
meterRegistry.counter(COVERED_USAGE_METRIC, tags.toArray(new String[0])).increment(coverage);
}
if (billable > 0) {
meterRegistry.counter(BILLABLE_USAGE_METRIC, tags.toArray(new String[0])).increment(billable);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,6 +46,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;
Expand All @@ -53,7 +58,9 @@
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
Expand All @@ -79,7 +86,7 @@ class BillableUsageServiceTest {
LocalDateTime.of(2019, 5, 24, 12, 35, 0, 0).toInstant(ZoneOffset.UTC),
ZoneOffset.UTC));

private static final String AWS_METRIC_ID = "aws_metric";
private static final String AWS_METRIC_ID = "Instance-hours";
private static final String ROSA = "rosa";
private static final String ORG_ID = "org123";

Expand All @@ -91,6 +98,8 @@ class BillableUsageServiceTest {

@Inject ApplicationClock clock;
@Inject BillableUsageService service;
@Inject BillableUsageService billableUsageService;
@Inject private MeterRegistry meterRegistry;

private final SubscriptionDefinitionRegistry mockSubscriptionDefinitionRegistry =
mock(SubscriptionDefinitionRegistry.class);
Expand All @@ -106,6 +115,7 @@ void setup() {
remittanceRepo.deleteAll();
// reset original subscription definition registry
setSubscriptionDefinitionRegistry(originalReference);
meterRegistry.clear();
}

@AfterEach
Expand All @@ -125,6 +135,7 @@ void monthlyWindowNoCurrentRemittance() {

thenRemittanceIsUpdated(usage, 1.0);
thenUsageIsSent(usage, 1.0);
thenBillableMeterMatches(usage, 1.0);
}

@Test
Expand All @@ -138,6 +149,7 @@ void monthlyWindowWithRemittanceUpdate() {

thenRemittanceIsUpdated(usage, 2.0);
thenUsageIsSent(usage, 2.0);
thenBillableMeterMatches(usage, 2.0);
}

@Test
Expand All @@ -152,6 +164,7 @@ void monthlyWindowRemittanceMultipleOfBillingFactor() {
// 4(Billing_factor) = 72
thenRemittanceIsUpdated(usage, 72.0);
thenUsageIsSent(usage, 18.0);
thenBillableMeterMatches(usage, 18.0);
}

@Test
Expand All @@ -164,6 +177,7 @@ void monthlyWindowWithNoRemittanceUpdate() {
service.submitBillableUsage(usage);

thenUsageIsSent(usage, 0.0);
thenBillableMeterMatches(usage, 0.0);
}

@Test
Expand Down Expand Up @@ -194,6 +208,7 @@ void billingFactorAppliedInRecalculationEvenNumber() {

thenRemittanceIsUpdated(usage, 12.0);
thenUsageIsSent(usage, usage.getValue());
thenBillableMeterMatches(usage, usage.getValue());
}

@Test
Expand All @@ -206,6 +221,7 @@ void billingFactorAppliedInRecalculation() {

thenRemittanceIsUpdated(usage, 28.0);
thenUsageIsSent(usage, usage.getValue());
thenBillableMeterMatches(usage, usage.getValue());
}

// Simulates progression through contract billing.
Expand Down Expand Up @@ -522,7 +538,7 @@ private void givenExistingContractForUsage(Contract contract, BillableUsage usag
}
}

private void givenExistingContract(
private List<Contract> givenExistingContract(
String orgId,
String productId,
String metric,
Expand All @@ -547,6 +563,7 @@ private void givenExistingContract(
when(contractsApi.getContract(
orgId, productId, vendorProductCode, billingProvider, billingAccountId, startDate))
.thenReturn(List.of(contract1, updatedContract));
return List.of(contract1, updatedContract);
}

void givenExistingRemittanceForUsage(BillableUsage usage, double remittedPendingValue) {
Expand Down Expand Up @@ -616,7 +633,6 @@ private void performRemittanceTesting(
boolean isContractEnabledTest)
throws Exception {
BillableUsage usage = givenInstanceHoursUsageForRosa(usageDate, currentUsage, currentUsage);
givenExistingContractForUsage(usage);

// Enable contracts for the current product.
givenExistingRemittanceForUsage(usage, CLOCK.now().minusHours(1), currentRemittance);
Expand All @@ -625,16 +641,18 @@ private void performRemittanceTesting(
stubSubscriptionDefinition(
usage.getProductId(), usage.getMetricId(), billingFactor, isContractEnabledTest);

List<Contract> contracts = new ArrayList<>();
// Configure contract data, if defined
if (isContractEnabledTest) {
givenExistingContract(
usage.getOrgId(),
usage.getProductId(),
AWS_METRIC_ID,
usage.getVendorProductCode(),
usage.getBillingProvider().value(),
usage.getBillingAccountId(),
usage.getSnapshotDate());
contracts =
givenExistingContract(
usage.getOrgId(),
usage.getProductId(),
AWS_METRIC_ID,
usage.getVendorProductCode(),
usage.getBillingProvider().value(),
usage.getBillingAccountId(),
usage.getSnapshotDate());
}

service.submitBillableUsage(usage);
Expand All @@ -646,6 +664,37 @@ private void performRemittanceTesting(
}

thenUsageIsSent(usage, expectedBilledValue);
thenBillableMeterMatches(usage, expectedBilledValue);
if (!contracts.isEmpty()) {
thenCoveredMeterMatches(usage, getCoveredAmount(usage, currentUsage, contracts, usageDate));
}
}

private double getCoveredAmount(
BillableUsage usage,
Double currentUsage,
List<Contract> contracts,
OffsetDateTime usageDate) {
double coverage =
contracts.stream()
.filter(
x ->
(x.getStartDate() == null
|| x.getStartDate().isBefore(usageDate)
|| x.getStartDate().isEqual(usageDate))
&& (x.getEndDate() == null
|| x.getEndDate().isAfter(usageDate)
|| x.getEndDate().isEqual(usageDate)))
.map(Contract::getMetrics)
.flatMap(List::stream)
.filter(
x -> x.getMetricId().equals(MetricId.fromString(usage.getMetricId()).toString()))
.mapToInt(Metric::getValue)
.sum();
log.error("This is the test calculated coverage from the contracts: {}", coverage);
return currentUsage * usage.getBillingFactor() > coverage
? coverage
: currentUsage * usage.getBillingFactor();
}

private void thenUsageIsSent(BillableUsage usage, double expectedValue) {
Expand All @@ -661,6 +710,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());
}
Expand Down Expand Up @@ -731,4 +811,16 @@ private static void setSubscriptionDefinitionRegistry(SubscriptionDefinitionRegi
fail(e);
}
}

private Optional<Meter> 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();
}
}

0 comments on commit f5695ff

Please sign in to comment.