blob: b3eec4feee15e1140503f099c2ce90a5cb0adb46 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.epam.dlab.billing.azure;
import com.epam.dlab.billing.BillingCalculationUtils;
import com.epam.dlab.billing.azure.config.BillingConfigurationAzure;
import com.epam.dlab.billing.azure.model.AzureDailyResourceInvoice;
import com.epam.dlab.billing.azure.model.AzureDlabBillableResource;
import com.epam.dlab.billing.azure.rate.AzureRateCardClient;
import com.epam.dlab.billing.azure.rate.Meter;
import com.epam.dlab.billing.azure.rate.RateCardResponse;
import com.epam.dlab.billing.azure.usage.AzureUsageAggregateClient;
import com.epam.dlab.billing.azure.usage.UsageAggregateRecord;
import com.epam.dlab.billing.azure.usage.UsageAggregateResponse;
import com.epam.dlab.exceptions.DlabException;
import com.microsoft.azure.AzureEnvironment;
import com.microsoft.azure.credentials.ApplicationTokenCredentials;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Filters billing data and calculate prices for each
* resource using combination of Microsoft Azure RateCard API and Usage API
*/
@Slf4j
public class AzureInvoiceCalculationService {
/**
* According to Microsoft Azure documentation
* https://docs.microsoft.com/en-us/azure/active-directory/active-directory-configurable-token-lifetimes
* min TTL time of token 10 minutes
*/
private static final long MAX_AUTH_TOKEN_TTL_MILLIS = 9L * 60L * 1000L;
private BillingConfigurationAzure billingConfigurationAzure;
private Map<String, AzureDlabBillableResource> billableResources;
/**
* Constructs service class
*
* @param billingConfigurationAzure contains <code>billing-azure</code> module configuration
*/
public AzureInvoiceCalculationService(BillingConfigurationAzure billingConfigurationAzure) {
this.billingConfigurationAzure = billingConfigurationAzure;
}
/**
* Prepares invoice records aggregated by day
*
* @param from start usage period
* @param to end usage period
* @return list of invoice records for each meter category aggregated by day
*/
public List<AzureDailyResourceInvoice> generateInvoiceData(String from, String to) {
long refreshTokenTime = System.currentTimeMillis() + MAX_AUTH_TOKEN_TTL_MILLIS;
String authenticationToken = getNewToken();
AzureRateCardClient azureRateCardClient = new AzureRateCardClient(billingConfigurationAzure,
authenticationToken);
AzureUsageAggregateClient azureUsageAggregateClient = new AzureUsageAggregateClient(billingConfigurationAzure,
authenticationToken);
List<AzureDailyResourceInvoice> invoiceData = new ArrayList<>();
try {
UsageAggregateResponse usageAggregateResponse = null;
Map<String, Meter> rates = transformRates(azureRateCardClient.getRateCard());
do {
if (usageAggregateResponse != null && StringUtils.isNotEmpty(usageAggregateResponse.getNextLink())) {
log.info("Get usage of resources using link {}", usageAggregateResponse.getNextLink());
usageAggregateResponse = azureUsageAggregateClient.getUsageAggregateResponse
(usageAggregateResponse.getNextLink());
log.info("Received usage of resources. Items {} ", usageAggregateResponse.getValue() != null ?
usageAggregateResponse.getValue().size() : 0);
log.info("Next link is {}", usageAggregateResponse.getNextLink());
} else if (usageAggregateResponse == null) {
log.info("Get usage of resources from {} to {}", from, to);
usageAggregateResponse = azureUsageAggregateClient.getUsageAggregateResponse(from, to);
log.info("Received usage of resources. Items {} ", usageAggregateResponse.getValue() != null ?
usageAggregateResponse.getValue().size() : 0);
log.info("Next link is {}", usageAggregateResponse.getNextLink());
}
invoiceData.addAll(generateBillingInfo(rates, usageAggregateResponse));
if (System.currentTimeMillis() > refreshTokenTime) {
authenticationToken = getNewToken();
azureUsageAggregateClient.setAuthToken(authenticationToken);
}
} while (StringUtils.isNotEmpty(usageAggregateResponse.getNextLink()));
} catch (IOException | RuntimeException | URISyntaxException e) {
log.error("Cannot calculate billing information", e);
throw new DlabException("Cannot prepare invoice data", e);
}
return invoiceData;
}
private List<AzureDailyResourceInvoice> generateBillingInfo(Map<String, Meter> rates, UsageAggregateResponse
usageAggregateResponse) {
List<UsageAggregateRecord> usageAggregateRecordList = usageAggregateResponse.getValue();
List<AzureDailyResourceInvoice> invoices = new ArrayList<>();
if (usageAggregateRecordList != null && !usageAggregateRecordList.isEmpty()) {
log.info("Processing {} usage records", usageAggregateRecordList.size());
usageAggregateRecordList = usageAggregateRecordList.stream().filter(e ->
matchProperStructure(e) && isBillableDlabResource(e))
.collect(Collectors.toList());
log.info("Applicable records number is {}", usageAggregateRecordList.size());
for (UsageAggregateRecord record : usageAggregateRecordList) {
invoices.add(calculateInvoice(rates, record, record.getProperties().getParsedInstanceData()
.getMicrosoftResources().getTags().get("Name")));
}
} else {
log.error("No usage records in response.");
}
return invoices;
}
private Map<String, Meter> transformRates(RateCardResponse rateCardResponse) {
Map<String, Meter> rates = new HashMap<>();
for (Meter meter : rateCardResponse.getMeters()) {
rates.put(meter.getMeterId(), meter);
}
return rates;
}
private boolean matchProperStructure(UsageAggregateRecord record) {
if (record.getProperties() == null) {
return false;
}
if (record.getProperties().getMeterId() == null || record.getProperties().getMeterId().isEmpty()) {
return false;
}
return !(record.getProperties().getParsedInstanceData() == null
|| record.getProperties().getParsedInstanceData().getMicrosoftResources() == null
|| record.getProperties().getParsedInstanceData().getMicrosoftResources().getTags() == null
|| record.getProperties().getParsedInstanceData().getMicrosoftResources().getTags().isEmpty());
}
private boolean isBillableDlabResource(UsageAggregateRecord record) {
String dlabId = record.getProperties().getParsedInstanceData().getMicrosoftResources().getTags().get("Name");
return dlabId != null && !dlabId.isEmpty() && dlabId.startsWith(billingConfigurationAzure.getSbn());
}
private AzureDailyResourceInvoice calculateInvoice(Map<String, Meter> rates, UsageAggregateRecord record, String dlabId) {
String meterId = record.getProperties().getMeterId();
Meter rateCard = rates.get(meterId);
if (rateCard != null) {
Map<String, Double> meterRates = rateCard.getMeterRates();
if (meterRates != null) {
Double rate = meterRates.get(AzureRateCardClient.MAIN_RATE_KEY);
if (rate != null) {
return AzureDailyResourceInvoice.builder()
.dlabId(dlabId)
.usageStartDate(getDay(record.getProperties().getUsageStartTime()))
.usageEndDate(getDay(record.getProperties().getUsageEndTime()))
.meterCategory(record.getProperties().getMeterCategory())
.cost(BillingCalculationUtils.round(rate * record.getProperties().getQuantity(), 3))
.day(getDay(record.getProperties().getUsageStartTime()))
.currencyCode(billingConfigurationAzure.getCurrency())
.build();
} else {
log.error("Rate Card {} has no rate for meter id {} and rate id {}. Skip record {}.",
rateCard, meterId, AzureRateCardClient.MAIN_RATE_KEY, record);
}
} else {
log.error("Rate Card {} has no meter rates fro meter id {}. Skip record {}",
rateCard, meterId, record);
}
} else {
log.error("Meter rate {} form usage aggregate is not found in rate card. Skip record {}.", meterId, record);
}
return null;
}
private String getNewToken() {
try {
log.info("Requesting authentication token ... ");
ApplicationTokenCredentials applicationTokenCredentials = new ApplicationTokenCredentials(
billingConfigurationAzure.getClientId(),
billingConfigurationAzure.getTenantId(),
billingConfigurationAzure.getClientSecret(),
AzureEnvironment.AZURE);
return applicationTokenCredentials.getToken(AzureEnvironment.AZURE.resourceManagerEndpoint());
} catch (IOException e) {
log.error("Cannot retrieve authentication token due to", e);
throw new DlabException("Cannot retrieve authentication token", e);
}
}
private String getDay(String dateTime) {
if (dateTime != null) {
String[] parts = dateTime.split("T");
if (parts.length == 2) {
return parts[0];
}
}
log.error("Wrong usage date format {} ", dateTime);
return null;
}
}