| /* |
| * 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.MongoKeyWords; |
| import com.epam.dlab.billing.azure.config.AzureAuthFile; |
| import com.epam.dlab.billing.azure.config.BillingConfigurationAzure; |
| import com.epam.dlab.billing.azure.model.AzureDailyResourceInvoice; |
| import com.epam.dlab.billing.azure.model.BillingPeriod; |
| import com.epam.dlab.exceptions.DlabException; |
| import com.epam.dlab.util.mongo.modules.IsoDateModule; |
| import com.fasterxml.jackson.core.JsonProcessingException; |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import com.mongodb.BasicDBObject; |
| import com.mongodb.client.model.Filters; |
| import com.mongodb.client.model.UpdateOptions; |
| import com.mongodb.client.result.UpdateResult; |
| import lombok.extern.slf4j.Slf4j; |
| import org.bson.Document; |
| import org.joda.time.DateTime; |
| import org.joda.time.DateTimeZone; |
| import org.joda.time.format.DateTimeFormat; |
| import org.joda.time.format.DateTimeFormatter; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.stereotype.Component; |
| |
| import javax.annotation.PostConstruct; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.List; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.TimeUnit; |
| import java.util.stream.Collectors; |
| |
| @Slf4j |
| @Component |
| public class BillingSchedulerAzure { |
| private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); |
| private BillingConfigurationAzure billingConfigurationAzure; |
| private MongoDbBillingClient mongoDbBillingClient; |
| |
| @Autowired |
| public BillingSchedulerAzure(BillingConfigurationAzure configuration) throws IOException { |
| billingConfigurationAzure = configuration; |
| Path path = Paths.get(billingConfigurationAzure.getAuthenticationFile()); |
| |
| if (path.toFile().exists()) { |
| log.info("Read and override configs using auth file"); |
| try { |
| AzureAuthFile azureAuthFile = new ObjectMapper().readValue(path.toFile(), AzureAuthFile.class); |
| this.billingConfigurationAzure.setClientId(azureAuthFile.getClientId()); |
| this.billingConfigurationAzure.setClientSecret(azureAuthFile.getClientSecret()); |
| this.billingConfigurationAzure.setTenantId(azureAuthFile.getTenantId()); |
| this.billingConfigurationAzure.setSubscriptionId(azureAuthFile.getSubscriptionId()); |
| } catch (IOException e) { |
| log.error("Cannot read configuration file", e); |
| throw e; |
| } |
| log.info("Configs from auth file are used"); |
| } else { |
| log.info("Configs from yml file are used"); |
| } |
| |
| this.mongoDbBillingClient = new MongoDbBillingClient |
| (billingConfigurationAzure.getAggregationOutputMongoDataSource().getHost(), |
| billingConfigurationAzure.getAggregationOutputMongoDataSource().getPort(), |
| billingConfigurationAzure.getAggregationOutputMongoDataSource().getDatabase(), |
| billingConfigurationAzure.getAggregationOutputMongoDataSource().getUsername(), |
| billingConfigurationAzure.getAggregationOutputMongoDataSource().getPassword()); |
| } |
| |
| @PostConstruct |
| public void start() { |
| if (billingConfigurationAzure.isBillingEnabled()) { |
| executorService.scheduleWithFixedDelay(new CalculateBilling(billingConfigurationAzure, |
| mongoDbBillingClient), billingConfigurationAzure.getInitialDelay(), |
| billingConfigurationAzure.getPeriod(), TimeUnit.MINUTES); |
| } else { |
| log.info("======Billing is disabled======"); |
| } |
| } |
| |
| public void stop() { |
| try { |
| log.info("Stopping Azure billing scheduler"); |
| if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { |
| log.error("Force shut down"); |
| executorService.shutdownNow(); |
| } |
| mongoDbBillingClient.getClient().close(); |
| } catch (InterruptedException e) { |
| executorService.shutdownNow(); |
| mongoDbBillingClient.getClient().close(); |
| Thread.currentThread().interrupt(); |
| } |
| } |
| |
| |
| @Slf4j |
| private static class CalculateBilling implements Runnable { |
| private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern |
| ("yyyy-MM-dd'T'HH:mm:ss.SSS'Z"); |
| private static final String SCHEDULER_ID = "azureBillingScheduler"; |
| private BillingConfigurationAzure billingConfigurationAzure; |
| private MongoDbBillingClient client; |
| private ObjectMapper objectMapper = new ObjectMapper().registerModule(new IsoDateModule()); |
| |
| |
| public CalculateBilling(BillingConfigurationAzure billingConfigurationAzure, MongoDbBillingClient client) { |
| this.billingConfigurationAzure = billingConfigurationAzure; |
| this.client = client; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| BillingPeriod billingPeriod = getBillingPeriod(); |
| DateTime currentTime = new DateTime().withZone(DateTimeZone.UTC); |
| if (billingPeriod == null) { |
| saveBillingPeriod(initialSchedulerInfo(currentTime)); |
| } else { |
| log.info("Billing period from db is {}", billingPeriod); |
| |
| if (shouldTriggerJobByTime(currentTime, billingPeriod)) { |
| boolean hasNew = run(billingPeriod); |
| updateBillingPeriod(billingPeriod, currentTime, hasNew); |
| } |
| } |
| } catch (RuntimeException e) { |
| log.error("Cannot update billing information", e); |
| } |
| } |
| |
| private BillingPeriod initialSchedulerInfo(DateTime currentTime) { |
| |
| BillingPeriod initialBillingPeriod = new BillingPeriod(); |
| initialBillingPeriod.setFrom(currentTime.minusDays(2).toDateMidnight().toDate()); |
| initialBillingPeriod.setTo(currentTime.toDateMidnight().toDate()); |
| |
| log.info("Initial scheduler info {}", initialBillingPeriod); |
| |
| return initialBillingPeriod; |
| |
| } |
| |
| private boolean shouldTriggerJobByTime(DateTime currentTime, BillingPeriod billingPeriod) { |
| |
| DateTime dateTimeToFromBillingPeriod = new DateTime(billingPeriod.getTo()).withZone(DateTimeZone.UTC); |
| |
| log.info("Comparing current time[{}, {}] and from scheduler info [{}, {}]", currentTime, |
| currentTime.toDateMidnight(), |
| dateTimeToFromBillingPeriod, dateTimeToFromBillingPeriod.toDateMidnight()); |
| |
| if (currentTime.toDateMidnight().isAfter(dateTimeToFromBillingPeriod.toDateMidnight()) |
| || currentTime.toDateMidnight().isEqual(dateTimeToFromBillingPeriod.toDateMidnight())) { |
| log.info("Should trigger the job by time"); |
| return true; |
| } |
| |
| log.info("Should not trigger the job by time"); |
| return false; |
| } |
| |
| private boolean run(BillingPeriod billingPeriod) { |
| AzureInvoiceCalculationService azureInvoiceCalculationService |
| = new AzureInvoiceCalculationService(billingConfigurationAzure); |
| |
| List<AzureDailyResourceInvoice> dailyInvoices = azureInvoiceCalculationService.generateInvoiceData( |
| DATE_TIME_FORMATTER.print(new DateTime(billingPeriod.getFrom()).withZone(DateTimeZone.UTC)), |
| DATE_TIME_FORMATTER.print(new DateTime(billingPeriod.getTo()).withZone(DateTimeZone.UTC))); |
| |
| if (!dailyInvoices.isEmpty()) { |
| client.getDatabase().getCollection(MongoKeyWords.BILLING_DETAILS) |
| .insertMany(dailyInvoices.stream().map(AzureDailyResourceInvoice::to) |
| .collect(Collectors.toList())); |
| return true; |
| } else { |
| log.warn("Daily invoices is empty for period {}", billingPeriod); |
| return false; |
| } |
| } |
| |
| private void updateBillingPeriod(BillingPeriod billingPeriod, DateTime currentTime, boolean updates) { |
| |
| try { |
| client.getDatabase().getCollection(MongoKeyWords.AZURE_BILLING_SCHEDULER_HISTORY).insertOne( |
| Document.parse(objectMapper.writeValueAsString(billingPeriod)).append("updates", updates)); |
| log.debug("History of billing periods is updated with {}", |
| objectMapper.writeValueAsString(billingPeriod)); |
| } catch (JsonProcessingException e) { |
| log.error("Cannot update history of billing periods", e); |
| |
| } |
| |
| billingPeriod.setFrom(billingPeriod.getTo()); |
| |
| if (new DateTime(billingPeriod.getFrom()).withZone(DateTimeZone.UTC).toDateMidnight() |
| .isEqual(currentTime.toDateMidnight())) { |
| |
| log.info("Setting billing to one day later"); |
| billingPeriod.setTo(currentTime.plusDays(1).toDateMidnight().toDate()); |
| |
| } else { |
| billingPeriod.setTo(currentTime.toDateMidnight().toDate()); |
| } |
| |
| saveBillingPeriod(billingPeriod); |
| } |
| |
| private boolean saveBillingPeriod(BillingPeriod billingPeriod) { |
| log.debug("Saving billing period {}", billingPeriod); |
| |
| try { |
| UpdateResult updateResult = client.getDatabase().getCollection(MongoKeyWords.AZURE_BILLING_SCHEDULER) |
| .updateMany(Filters.eq(MongoKeyWords.MONGO_ID, SCHEDULER_ID), |
| new BasicDBObject("$set", |
| Document.parse(objectMapper.writeValueAsString(billingPeriod)) |
| .append(MongoKeyWords.MONGO_ID, SCHEDULER_ID)) |
| , new UpdateOptions().upsert(true) |
| ); |
| |
| log.debug("Billing period save operation result is {}", updateResult); |
| return true; |
| } catch (JsonProcessingException e) { |
| log.error("Cannot save billing period", e); |
| } |
| |
| return false; |
| } |
| |
| private BillingPeriod getBillingPeriod() { |
| log.debug("Get billing period"); |
| |
| try { |
| Document document = client.getDatabase().getCollection(MongoKeyWords.AZURE_BILLING_SCHEDULER) |
| .find(Filters.eq(MongoKeyWords.MONGO_ID, SCHEDULER_ID)).first(); |
| |
| log.debug("Retrieved billing period document {}", document); |
| if (document != null) { |
| return objectMapper.readValue(document.toJson(), BillingPeriod.class); |
| } |
| |
| return null; |
| |
| } catch (IOException e) { |
| log.error("Cannot save billing period", e); |
| throw new DlabException("Cannot parse string", e); |
| } |
| } |
| } |
| } |