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 6, 2024
1 parent e862b4e commit 534d9c2
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 1 deletion.
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 @@ -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
Expand Down Expand Up @@ -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<String> 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);
}
}
}
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 @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -87,6 +93,7 @@ class BillableUsageServiceTest {

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

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

@AfterEach
Expand All @@ -121,6 +129,7 @@ void monthlyWindowNoCurrentRemittance() {

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

@Test
Expand All @@ -134,6 +143,7 @@ void monthlyWindowWithRemittanceUpdate() {

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

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

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

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

@Test
Expand Down Expand Up @@ -190,6 +202,7 @@ void billingFactorAppliedInRecalculationEvenNumber() {

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

@Test
Expand All @@ -202,6 +215,7 @@ void billingFactorAppliedInRecalculation() {

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

// Simulates progression through contract billing.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -660,6 +675,8 @@ private void performRemittanceTesting(
}

thenUsageIsSent(usage, expectedBilledValue);
thenBillableMeterMatches(usage, expectedBilledValue);
thenCoveredMeterMatches(usage, expectedCoveredValue);
}

private void thenUsageIsSent(BillableUsage usage, double expectedValue) {
Expand All @@ -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());
}
Expand Down Expand Up @@ -745,4 +793,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 534d9c2

Please sign in to comment.