diff --git a/README.md b/README.md index 0238a00a..b1131b76 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ When representing the cost profile for individual resources, Ice will factor the 5. Breakdown page of Application Groups ![Breakdown page of Application Groups](https://github.com/Netflix/ice/blob/master/screenshots/ss_breakdown_appgroup.png?raw=true) +6. Estimate page with cumulative values +![Estimate page using cumulative values](./screenshots/ss_estimate.png?raw=true) + ##Prerequisite: 1. First sign up for Amazon's programmatic billing access [here](http://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/detailed-billing-reports.html) to receive detailed billing(hourly) reports. Verify you receive monthly billing file in the following format: `-aws-billing-detailed-line-items--.csv.zip`. @@ -169,7 +172,7 @@ Options with * require writing your own code. ice.reservationPeriod=threeyear # reservation utilization, possible values are LIGHT, HEAVY ice.reservationUtilization=HEAVY - + 2. Reservation capacity poller To use BasicReservationService, you should also run reservation capacity poller, which will call ec2 API (describeReservedInstances) to poll reservation capacities for each reservation owner account defined in ice.properties. The reservation capacities history is stored in a file in s3 bucket. To run reservation capacity poller, following steps below: @@ -240,6 +243,25 @@ Options with * require writing your own code. You may also want to show your organization's throughput metric alongside usage and cost. You can choose to implement interface ThroughputMetricService, or you can simply use the existing BasicThroughputMetricService. Using BasicThroughputMetricService requires the throughput metric data to be stores monthly in files with names like _2013_04, _2013_05. Data in files should be delimited by new lines. is specified when you create BasicThroughputMetricService instance. +9. Estimate Page + + You may want to use the estimate page to visualize estimated velocity against actual spending velocity. To enable: + + ice.report.estimate=true + + For each account, you can set the estimate in the ice.properties: + + ice.account.dailyestimate.account1=500 + ice.account.dailyestimate.account2=505 + + Also supports historical estimates: + + ice.account.dailyestimate.accountname.2015-01-26=500 + ice.account.dailyestimate.accountname.2015-01-30=550 + ice.account.dailyestimate.accountname.2015-02-15=650 + + + ##Support Please use the [Ice Google Group](https://groups.google.com/d/forum/iceusers) for general questions and discussion. diff --git a/grails-app/conf/BootStrap.groovy b/grails-app/conf/BootStrap.groovy index 44e978bf..8fe296ca 100644 --- a/grails-app/conf/BootStrap.groovy +++ b/grails-app/conf/BootStrap.groovy @@ -45,6 +45,9 @@ import com.netflix.ice.common.ProductService import com.netflix.ice.basic.BasicResourceService import com.netflix.ice.basic.BasicWeeklyCostEmailService import com.netflix.ice.reader.ApplicationGroupService +import org.joda.time.format.ISODateTimeFormat +import org.joda.time.format.DateTimeFormatter + class BootStrap { private static boolean initialized = false; @@ -102,11 +105,43 @@ class BootStrap { Map accounts = Maps.newHashMap(); for (String name: prop.stringPropertyNames()) { - if (name.startsWith("ice.account.")) { + if (name.startsWith("ice.account.dailyestimate.")) { + String propertyValue = prop.getProperty(name); + Double propertyDoubleValue = Double.parseDouble(propertyValue); + + String propertyNameNoPrefix = name.substring("ice.account.dailyestimate.".length()); + String accountName=propertyNameNoPrefix; + String[] propertySections = propertyNameNoPrefix.split("\\."); + DateTime estimateDate = new DateTime(0); + if (propertySections.length == 2) { + DateTimeFormatter dtf = ISODateTimeFormat.date(); + accountName = propertySections[0]; + estimateDate = dtf.parseDateTime(propertySections[1]); + } + + Account account = accounts.get(accountName); + if (account == null) { + String accountId = prop.getProperty("ice.account." + accountName); + if (accountId == null) { + System.err.println(accountName + " does not have an ice.account entry"); + continue; + } + account = new Account(accountId, accountName); + accounts.put(accountName, account); + } + System.out.println("Set Daily Estimate for " + account + " - " + propertyDoubleValue.toString()); + account.dailyEstimates.put(estimateDate, propertyDoubleValue); + } else if (name.startsWith("ice.account.") ) { String accountName = name.substring("ice.account.".length()); - accounts.put(accountName, new Account(prop.getProperty(name), accountName)); + Account account = accounts.get(accountName); + if (account == null) { + accounts.put(accountName, new Account(prop.getProperty(name), accountName)); + } else { + //loaded above + } } } + Map> reservationAccounts = Maps.newHashMap(); Map reservationAccessRoles = Maps.newHashMap(); Map reservationAccessExternalIds = Maps.newHashMap(); @@ -198,6 +233,8 @@ class BootStrap { properties.setProperty(IceOptions.CURRENCY_SIGN, prop.getProperty(IceOptions.CURRENCY_SIGN)); if (prop.getProperty(IceOptions.HIGHSTOCK_URL) != null) properties.setProperty(IceOptions.HIGHSTOCK_URL, prop.getProperty(IceOptions.HIGHSTOCK_URL)); + if (prop.getProperty(IceOptions.ESTIMATE_REPORT) != null) + properties.setProperty(IceOptions.ESTIMATE_REPORT, prop.getProperty(IceOptions.ESTIMATE_REPORT)); ResourceService resourceService = StringUtils.isEmpty(properties.getProperty(IceOptions.CUSTOM_TAGS)) ? null : new BasicResourceService(); ApplicationGroupService applicationGroupService = new BasicS3ApplicationGroupService(); diff --git a/grails-app/controllers/com/netflix/ice/DashboardController.groovy b/grails-app/controllers/com/netflix/ice/DashboardController.groovy index 39488d8b..3ce6e69e 100644 --- a/grails-app/controllers/com/netflix/ice/DashboardController.groovy +++ b/grails-app/controllers/com/netflix/ice/DashboardController.groovy @@ -377,6 +377,8 @@ class DashboardController { def detail = {} + def estimates={} + def reservation = {} def breakdown = {} @@ -393,6 +395,10 @@ class DashboardController { TagType groupBy = query.getString("groupBy").equals("None") ? null : TagType.valueOf(query.getString("groupBy")); boolean isCost = query.getBoolean("isCost"); + boolean includeEstimates = false; + if (query.has("includeEstimates")) { + includeEstimates = query.getBoolean("includeEstimates"); + } boolean breakdown = query.getBoolean("breakdown"); boolean showsps = query.getBoolean("showsps"); boolean factorsps = query.getBoolean("factorsps"); @@ -438,6 +444,11 @@ class DashboardController { } interval = roundInterval(interval, consolidateType); + //we will have an estimate for each datapoint + Map estimates; + + + Map data; if (groupBy == TagType.ApplicationGroup) { data = Maps.newTreeMap(); @@ -550,12 +561,22 @@ class DashboardController { aggregate, forReservation ); + //groupBy Account can have estimates + if (includeEstimates) { + DataManager estimateManager = getManagers().getEstimateManager(consolidateType); + estimates = estimateManager.getData(interval, new TagLists(accounts, regions, zones, products, operations, usageTypes, resourceGroups), groupBy, aggregate, forReservation); + } } - def stats = getStats(data); + def stats = getStats(data, estimates); if (aggregate == AggregateType.stats && data.size() > 1) data.remove(Tag.aggregated); def result = [status: 200, start: interval.getStartMillis(), data: data, stats: stats, groupBy: groupBy == null ? "None" : groupBy.name()] + if (estimates != null) + { + result.put("estimates", estimates); + } + if (breakdown && data.size() > 0 && data.values().iterator().next().length > 0) { result.time = new IntRange(0, data.values().iterator().next().length - 1).collect { if (consolidateType == ConsolidateType.daily) @@ -657,7 +678,7 @@ class DashboardController { } } - private Map getStats(Map data) { + private Map getStats(Map data, Map estimates) { def result = [:]; for (Map.Entry entry: data.entrySet()) { @@ -675,6 +696,15 @@ class DashboardController { total += v; } result[tag] = [max: max, total: total, average: total / values.length]; + double totalEstimate = 0; + if (estimates) { + for (double v: estimates[tag]) { + totalEstimate += v; + } + result[tag]["averageEstimate"]=totalEstimate/values.length; + result[tag]["totalEstimate"]=totalEstimate; + } + } return result; diff --git a/grails-app/views/dashboard/estimates.gsp b/grails-app/views/dashboard/estimates.gsp new file mode 100644 index 00000000..1ab63080 --- /dev/null +++ b/grails-app/views/dashboard/estimates.gsp @@ -0,0 +1,117 @@ +<%-- + + Copyright 2013 Netflix, Inc. + + Licensed 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. + +--%> + +<%@ page contentType="text/html;charset=UTF-8" %> + + + + Aws Cumulative Usage + + +
+ + + + + + + + + + + +
StartOptionsAccount
+ +
End
+
+
+
Aggregate + +
+
Cumulative + +
+
+ +
+
+ +
+ + Submit + +
+ + + + + + + + +
+
+
+
+
+
+ SHOW ALL + HIDE ALL + +
+ + + + + + + + + + + + + + + + + + + +
{{legendName}}
TotalTotal EstimateAverageAverage Estimate
+
+ {{legend.name}} +
{{currencySign}} {{legend.stats.total | number:2}}{{currencySign}} {{legend.stats.totalEstimate | number:2}}{{currencySign}} {{legend.stats.average | number:2}}{{currencySign}} {{legend.stats.averageEstimate | number:2}}
+
+
+ +
+ + diff --git a/grails-app/views/layouts/main.gsp b/grails-app/views/layouts/main.gsp index 7e1e094d..65bcea5a 100644 --- a/grails-app/views/layouts/main.gsp +++ b/grails-app/views/layouts/main.gsp @@ -46,6 +46,11 @@ AWS Details
    + + + + + diff --git a/screenshots/ss_estimate.png b/screenshots/ss_estimate.png new file mode 100644 index 00000000..eda86c01 Binary files /dev/null and b/screenshots/ss_estimate.png differ diff --git a/src/java/com/netflix/ice/basic/BasicEstimateManager.java b/src/java/com/netflix/ice/basic/BasicEstimateManager.java new file mode 100644 index 00000000..7d6119f6 --- /dev/null +++ b/src/java/com/netflix/ice/basic/BasicEstimateManager.java @@ -0,0 +1,238 @@ +/* + * + * Copyright 2013 Netflix, Inc. + * + * Licensed 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.netflix.ice.basic; + +import com.google.common.cache.*; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.netflix.ice.common.*; +import com.netflix.ice.reader.*; +import com.netflix.ice.tag.Product; +import com.netflix.ice.tag.Tag; +import com.netflix.ice.tag.Account; +import com.netflix.ice.tag.TagType; +import org.joda.time.*; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.ExecutionException; + +/** + * This class reads data from s3 bucket and feeds the data to UI + */ +public class BasicEstimateManager extends Poller implements DataManager { + + protected ReaderConfig config = ReaderConfig.getInstance(); + protected ConsolidateType consolidateType; + + + + public BasicEstimateManager(ConsolidateType consolidateType) { + this.consolidateType = consolidateType; + + } + + /** + * We check if new data is available periodically + * @throws Exception + */ + @Override + protected void poll() throws Exception { + // no-op + } + + @Override + protected String getThreadName() { + return "BasicEstimateManager"; + } + + /** + * + * @param interval + * @param tagLists + * @param groupBy + * @param aggregate + * @param forReservation + * @return + */ + public double[] getEstimatesData(Interval interval, Account account, TagType groupBy, AggregateType aggregate, boolean forReservation) { + + DateTime start = interval.getStart(); + DateTime end = interval.getEnd(); + Map accountEstimates = account.dailyEstimates; + + if (accountEstimates == null) { + return new double[0]; + } + + if (consolidateType == ConsolidateType.hourly) { + start = interval.getStart().withDayOfMonth(1).withMillisOfDay(0); + end = interval.getEnd(); + } + else if (consolidateType == ConsolidateType.daily) { + start = interval.getStart().withDayOfYear(1).withMillisOfDay(0); + end = interval.getEnd(); + } + + int num = 0; + if (consolidateType == ConsolidateType.hourly) { + num = interval.toPeriod(PeriodType.hours()).getHours(); + if (interval.getStart().plusHours(num).isBefore(interval.getEnd())) + num++; + } + else if (consolidateType == ConsolidateType.daily) { + num = interval.toPeriod(PeriodType.days()).getDays(); + if (interval.getStart().plusDays(num).isBefore(interval.getEnd())) + num++; + } + else if (consolidateType == ConsolidateType.weekly) { + num = interval.toPeriod(PeriodType.weeks()).getWeeks(); + if (interval.getStart().plusWeeks(num).isBefore(interval.getEnd())) + num++; + } + else if (consolidateType == ConsolidateType.monthly) { + num = interval.toPeriod(PeriodType.months()).getMonths(); + if (interval.getStart().plusMonths(num).isBefore(interval.getEnd())) + num++; + } + + double[] accountResults = new double[num]; + DateTime currentEstimateDate=start; + DateTime estimateChangeAt=null; + double currentEstimate=0; + + + //we have start, num of items and a ConsolidateType + for(int i=0;i 0 && currentEstimate == 0) || + (accountEstimates.size() > 0 && currentEstimateDate.isAfter(estimateChangeAt)) + ) { + + boolean foundCurrent = false; + boolean foundNext = false; + + // accountEstimates is a sorted TreeMap + for (Map.Entry accountEstimate : accountEstimates.entrySet()) { + + // This might be our only option + if (accountEstimates.size() == 1) { + currentEstimate = accountEstimate.getValue().doubleValue(); + estimateChangeAt = new DateTime(2042); // No further estimates + break; + } + + if (foundCurrent && foundNext) { + estimateChangeAt = accountEstimate.getKey(); + break; + } + + if ((accountEstimate.getKey().equals(currentEstimateDate) || accountEstimate.getKey().isBefore(currentEstimateDate))) { + foundCurrent = true; + continue; + } + + if (foundCurrent) { + foundNext = true; + currentEstimate = accountEstimate.getValue().doubleValue(); + estimateChangeAt = null; + continue; + } + } + if (estimateChangeAt == null) { + estimateChangeAt = new DateTime(2042); // No further estimates + } + + } + + DateTime newEstimateDate = null; + if (consolidateType == ConsolidateType.hourly) { + accountResults[i] = currentEstimate / 24.0; + newEstimateDate = currentEstimateDate.plusHours(1); + nextIndex = true; + } else if (consolidateType == ConsolidateType.daily) { + accountResults[i] = currentEstimate; + newEstimateDate = currentEstimateDate.plusDays(1); + nextIndex = true; + } else if (consolidateType == ConsolidateType.weekly) { + accountResults[i] += currentEstimate; + newEstimateDate = currentEstimateDate.plusDays(1); + if (newEstimateDate.getDayOfWeek() == 1) + nextIndex = true; + } else if (consolidateType == ConsolidateType.monthly) { + accountResults[i] += currentEstimate; + newEstimateDate = currentEstimateDate.plusDays(1); + if (newEstimateDate.getDayOfMonth() == 1) + nextIndex = true; + } + + if (newEstimateDate.isBefore(currentEstimateDate)) { + newEstimateDate = newEstimateDate.plusYears(1); + } + + currentEstimateDate = newEstimateDate; + } + } + + return accountResults; + } + + private void addData(double[] from, double[] to) { + for (int i = 0; i < from.length; i++) + to[i] += from[i]; + } + + /** + * Get Estimates for given interval + */ + public Map getData(Interval interval, TagLists tagLists, TagType groupBy, AggregateType aggregate, boolean forReservation) { + + DateTime start = config.startDate; + DateTime end = config.startDate; + Map results = new HashMap(); + + double[] aggregated = null; + for (Account account : tagLists.accounts) { + logger.info("Get Estimates for " + account.name); + double[] accountResults = getEstimatesData(interval, account, groupBy, aggregate, forReservation); + if (aggregated == null) + aggregated = new double[accountResults.length]; + addData(accountResults, aggregated); + if (aggregated != null) + results.put(Tag.aggregated, aggregated); + results.put(account, accountResults); + } + + return results; + + } + + public int getDataLength(DateTime start) { + return 0; + } +} diff --git a/src/java/com/netflix/ice/basic/BasicManagers.java b/src/java/com/netflix/ice/basic/BasicManagers.java index c74b6728..845551a7 100644 --- a/src/java/com/netflix/ice/basic/BasicManagers.java +++ b/src/java/com/netflix/ice/basic/BasicManagers.java @@ -31,6 +31,7 @@ import java.util.Set; import java.util.TreeMap; + /** * This class manages all BasicTagGroupManager and BasicDataManager instances. */ @@ -41,8 +42,10 @@ public class BasicManagers extends Poller implements Managers { private Map tagGroupManagers = Maps.newHashMap(); private TreeMap costManagers = Maps.newTreeMap(); private TreeMap usageManagers = Maps.newTreeMap(); + private Map estimateManagers = Maps.newHashMap(); public void shutdown() { + for (BasicTagGroupManager tagGroupManager: tagGroupManagers.values()) { tagGroupManager.shutdown(); } @@ -76,6 +79,10 @@ public DataManager getUsageManager(Product product, ConsolidateType consolidateT return usageManagers.get(new Key(product, consolidateType)); } + public DataManager getEstimateManager(ConsolidateType consolidateType) { + return estimateManagers.get(consolidateType); + } + @Override protected void poll() throws Exception { doWork(); @@ -87,7 +94,12 @@ private void doWork() { Set products = Sets.newHashSet(this.products); Map tagGroupManagers = Maps.newHashMap(this.tagGroupManagers); TreeMap costManagers = Maps.newTreeMap(this.costManagers); - TreeMap usageManagers = Maps.newTreeMap(this.usageManagers); + + for (ConsolidateType consolidateType: ConsolidateType.values()) { + estimateManagers.put(consolidateType, new BasicEstimateManager(consolidateType)); + } + + Set newProducts = Sets.newHashSet(); AmazonS3Client s3Client = AwsUtils.getAmazonS3Client(); diff --git a/src/java/com/netflix/ice/common/IceOptions.java b/src/java/com/netflix/ice/common/IceOptions.java index c7d9bca9..a793e51a 100644 --- a/src/java/com/netflix/ice/common/IceOptions.java +++ b/src/java/com/netflix/ice/common/IceOptions.java @@ -39,6 +39,11 @@ public class IceOptions { */ public static final String CURRENCY_RATE = "ice.currencyRate"; + /** + * Property for enabling the Estimate Cost Report + */ + public static final String ESTIMATE_REPORT = "ice.report.estimate"; + /** * The URL of highstock.js. The default value is the Highcharts CDN; change this if you need to * serve it from somewhere else (for example, if you need HTTPS). @@ -154,4 +159,16 @@ public class IceOptions { * from email to use when test flag is enabled. */ public static final String NUM_WEEKS_FOR_WEEKLYEMAILS = "ice.weeklyCostEmails_numWeeks"; + + /** + * Daily Estimates per each account. Can be appended with a ISO8601 Date stamp to + * keep a history of estimates. The no datetime is the default + * ice.account.dailyestimate.accountname=100 + * ice.account.dailyestimate.accountname.2015-01-26=500 + * ice.account.dailyestimate.accountname.2015-01-30=550 + * ice.account.dailyestimate.accountname.2015-02-15=650 + * + */ + public static final String ACCOUNT_DAILY_ESTIMATES = "ice.account.dailyestimate"; + } diff --git a/src/java/com/netflix/ice/reader/Managers.java b/src/java/com/netflix/ice/reader/Managers.java index 26d9486e..39e83aac 100644 --- a/src/java/com/netflix/ice/reader/Managers.java +++ b/src/java/com/netflix/ice/reader/Managers.java @@ -58,6 +58,13 @@ public interface Managers { */ DataManager getUsageManager(Product product, ConsolidateType consolidateType); + /** + * + * @param consolidateType + * @return estimates DataManager instance for specified consolidateType + */ + DataManager getEstimateManager(ConsolidateType consolidateType); + /** * shutdown all manager instances */ diff --git a/src/java/com/netflix/ice/reader/ReaderConfig.java b/src/java/com/netflix/ice/reader/ReaderConfig.java index 4c08af76..97de69f1 100644 --- a/src/java/com/netflix/ice/reader/ReaderConfig.java +++ b/src/java/com/netflix/ice/reader/ReaderConfig.java @@ -25,11 +25,17 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.Interval; +import org.joda.time.format.ISODateTimeFormat; +import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.Exception; import java.util.Collection; import java.util.Properties; +import java.util.Map; +import java.util.HashMap; +import java.util.Date; /** * COnfiguration class for reader/UI. @@ -47,6 +53,8 @@ public class ReaderConfig extends Config { public final BasicWeeklyCostEmailService costEmailService; public final Managers managers; public final int monthlyCacheSize; + public final Map> accountDailyEstimates = new HashMap>(); + public final boolean estimateReport; /** * @@ -74,7 +82,7 @@ public ReaderConfig( currencySign = properties.getProperty(IceOptions.CURRENCY_SIGN, "$"); currencyRate = Double.parseDouble(properties.getProperty(IceOptions.CURRENCY_RATE, "1")); highstockUrl = properties.getProperty(IceOptions.HIGHSTOCK_URL, "http://code.highcharts.com/stock/highstock.js"); - + estimateReport = Boolean.parseBoolean(properties.getProperty(IceOptions.ESTIMATE_REPORT, "false")); this.managers = managers; this.applicationGroupService = applicationGroupService; this.throughputMetricService = throughputMetricService; diff --git a/src/java/com/netflix/ice/tag/Account.java b/src/java/com/netflix/ice/tag/Account.java index b3c8f66a..7cc7feb3 100644 --- a/src/java/com/netflix/ice/tag/Account.java +++ b/src/java/com/netflix/ice/tag/Account.java @@ -17,11 +17,24 @@ */ package com.netflix.ice.tag; +import org.joda.time.DateTime; +import java.util.TreeMap; +import java.util.Map; + public class Account extends Tag { public final String id; + public Map dailyEstimates=new TreeMap(); public Account(String accountId, String accountName) { super(accountName); this.id = accountId; } + + public Account(String accountId, String accountName, Map dailyEstimates) { + super(accountName); + this.id = accountId; + this.dailyEstimates=dailyEstimates; + } + + } diff --git a/web-app/css/main.css b/web-app/css/main.css index ec62d3e5..70b9817c 100644 --- a/web-app/css/main.css +++ b/web-app/css/main.css @@ -232,7 +232,7 @@ button.resize div, a.resize { background-image: url(../images/ .buttons a.userData { background-image: url(../images/tango/24/actions/document-properties.png); } .buttons button.takeSnapshot div { background-image: url(../images/tango/24/devices/camera-photo.png); } .buttons button.shutdown div { background-image: url(../images/tango/24/actions/system-shutdown.png); } -.buttons button.restore div { background-image: url(../images/tango/24/actions/edit-undo.png); } +.buttons button.restore div { background-image: url(../images/tango/24/action5s/edit-undo.png); } .buttons button.elastic div { background-image: url(../images/tango/24/tools/select-lasso.png); } .buttons button.save div { background-image: url(../images/tango/24/actions/document-save.png); } .buttons button.blueOut div { background-image: url(../images/tango/24/tools/draw-ink.png); padding-right: 0; } @@ -329,7 +329,7 @@ div.grippie { background: #EEEEEE url(../images/grippie.png) no-repeat scroll ce .messageForIeUsers { padding: 10px; color: blue; font-size: 13px; font-weight: bold; background-color: white; } .chromeFrameInstallDefaultStyle { width: 96%; height: 90%; border: 5px solid blue; z-index: 1000; left: 410px; top: 360px; } -.metaAccounts {width: 115px} +.metaAccounts {width: 165px} .metaRegions {width: 115px} .metaProducts {width: 110px} .metaOperations {width: 230px} diff --git a/web-app/js/ice.js b/web-app/js/ice.js index f76e6011..728e79b7 100644 --- a/web-app/js/ice.js +++ b/web-app/js/ice.js @@ -59,6 +59,7 @@ ice.factory('highchart', function() { plotOptions: { area: {lineWidth: 1, stacking: 'normal'}, column: {lineWidth: 1, stacking: 'normal'}, + line: {lineWidth: 1, stacking: null}, series: { states: { hover: { @@ -92,11 +93,11 @@ ice.factory('highchart', function() { var precision = currencySign === "" ? 0 : (currencySign === "ยข" ? 4 : 2); for (var i = 0; i < this.points.length - (showsps ? 1 : 0); i++) { var point = this.points[i]; - if (i == 0) { + if (i == 0 && point.total!=undefined) { s += '
    aggregated : ' + currencySign + Highcharts.numberFormat(showsps ? total : point.total, precision, '.') + ' / ' + (factorsps ? metricunitname : consolidate); } var perc = showsps ? point.y * 100 / total : point.percentage; - s += '
    ' + point.series.name + ' : ' + currencySign + Highcharts.numberFormat(point.y, precision, '.') + ' / ' + (factorsps ? metricunitname : consolidate) + ' (' + Highcharts.numberFormat(perc, 1) + '%)'; + s += '
    ' + point.series.name + ' : ' + currencySign + Highcharts.numberFormat(point.y, precision, '.') + ' / ' + (factorsps ? metricunitname : consolidate) + ' (' + Highcharts.numberFormat(perc, 1) + '%)'; if (i > 40 && point) break; } @@ -106,7 +107,55 @@ ice.factory('highchart', function() { } }; - var setupHcData = function(result, plotType, showsps) { + var setupHcEstimate = function(result, plotType, cumulative) { + plotType = "line"; + Highcharts.setOptions({ + global: { + useUTC: true + } + }); + + var hadEstimate = false; + + for (i in result.estimates) { + var estimates = result.estimates[i].data; + var hasEstimate = false; + for (j in estimates) { + if (cumulative && cumulative == "true" && j > 0) { + estimates[j] = estimates[j-1] + parseFloat(estimates[j].toFixed(2)); + //aggregateEstimates[j] += estimates[j]; + } else { + estimates[j] = parseFloat(estimates[j].toFixed(2)); + //aggregateEstimates[j] += estimates[j]; + } + if (estimates[j] !== 0) + hasEstimate = true; + } + + if (hasEstimate) { + hadEstimate = true; + if (!result.interval && result.time) { + for (j in estimates) { + estimates[j] = [result.time[j], estimates[j]]; + } + } + + var serie = { + name: result.estimates[i].name + " Estimate", + data: estimates, + pointStart: result.start, + pointInterval: result.interval, + //step: true, + type: "line", + Index: -1 + }; + + hc_options.series.push(serie); + } + } + } + + var setupHcData = function(result, plotType, showsps, cumulative) { Highcharts.setOptions({ global: { @@ -120,7 +169,10 @@ ice.factory('highchart', function() { var data = result.data[i].data; var hasData = false; for (j in data) { - data[j] = parseFloat(data[j].toFixed(2)); + if (cumulative && cumulative == "true" && j > 0) + data[j] = data[j-1] + parseFloat(data[j].toFixed(2)); + else + data[j] = parseFloat(data[j].toFixed(2)); if (data[j] !== 0) hasData = true; } @@ -195,7 +247,8 @@ ice.factory('highchart', function() { currencySign = $scope.usage_cost === 'cost' ? ($scope.factorsps ? factoredCostCurrencySign : global_currencySign) : ""; hc_options.legend.enabled = legendEnabled; - setupHcData(result, $scope.plotType, $scope.showsps); + setupHcData(result, $scope.plotType, $scope.showsps, $scope.cumulative); + setupHcEstimate(result, $scope.plotType, $scope.cumulative); setupYAxis($scope.usage_cost === 'cost', $scope.showsps, $scope.factorsps); showsps = $scope.showsps; factorsps = $scope.factorsps; @@ -210,7 +263,7 @@ ice.factory('highchart', function() { var i = 0; for (i = 0; i < chart.series.length - ($scope.showsps ? 2 : 1); i++) { if ($scope && $scope.legends) { - var legend = { + var legend = { name: chart.series[i].name, style: "color: " + chart.series[i].color, iconStyle: "background-color: " + chart.series[i].color, @@ -365,7 +418,7 @@ ice.factory('usage_db', function($window, $http, $filter) { } $location.hash(result); - + if (time) { timeParams = time; } @@ -380,9 +433,9 @@ ice.factory('usage_db', function($window, $http, $filter) { if (hash) { var params = hash.split("&"); for (i = 0; i < params.length; i++) { - if (params[i].indexOf("=") < 0 && i > 0 && (params[i-1].indexOf("appgroup=") == 0 || params[i-1].indexOf("resourceGroup=") == 0)) - params[i-1] = params[i-1] + "&" + params[i]; - } + if (params[i].indexOf("=") < 0 && i > 0 && (params[i-1].indexOf("appgroup=") == 0 || params[i-1].indexOf("resourceGroup=") == 0)) + params[i-1] = params[i-1] + "&" + params[i]; + } var i, j, time = ""; for (i = 0; i < params.length; i++) { @@ -458,6 +511,9 @@ ice.factory('usage_db', function($window, $http, $filter) { else if (params[i].indexOf("resourceGroup=") === 0) { $scope.selected__resourceGroups = params[i].substr(14).split(","); } + else if (params[i].indexOf("includeEstimates=") === 0) { + $scope.includeEstimates = "true" === params[i].substr(17); + } } } if (!$scope.showResourceGroups) { @@ -1170,6 +1226,135 @@ function detailCtrl($scope, $location, $http, usage_db, highchart) { fn(); } +function estimateCtrl($scope, $location, $http, usage_db, highchart) { + + $scope.graphType = "aggregate"; + $scope.legends = []; + $scope.usage_cost = "cost"; + $scope.includeEstimates=true; + $scope.cumulative="true"; + $scope.dailyEstimate=500; + $scope.groupBy={ name: "Account" } + $scope.consolidate = "daily"; + $scope.end = new Date(); + $scope.start = new Date(); + $scope.plotType = "area"; + + var startMonth = $scope.end.getUTCMonth() - 1; + var startYear = $scope.end.getUTCFullYear(); + if (startMonth < 0) { + startMonth += 12; + startYear -= 1; + } + $scope.start.setUTCFullYear(startYear); + $scope.start.setUTCMonth(startMonth); + $scope.start.setUTCDate(1); + $scope.start.setUTCHours(0); + + $scope.end = highchart.dateFormat($scope.end); //$filter('date')($scope.end, "y-MM-dd hha"); + $scope.start = highchart.dateFormat($scope.start); //$filter('date')($scope.start, "y-MM-dd hha"); + + + $scope.updateUrl = function() { + $scope.end = jQuery('#end').datetimepicker().val(); + $scope.start = jQuery('#start').datetimepicker().val(); + + var params = { + account: {selected: $scope.selected_accounts, from: $scope.accounts}, + usage_cost: $scope.usage_cost, + start: $scope.start, + end: $scope.end, + groupBy: "Account", + plotType: "area", + aggregate: "none", + consolidate: $scope.consolidate, + graphType: $scope.graphType, + includeEstimates: "" + $scope.includeEstimates + }; + usage_db.updateUrl($location, params); + } + + $scope.download = function() { + usage_db.getData($scope, null, null, true); + } + + $scope.getData = function() { + $scope.loading = true; + usage_db.getData($scope, function(result){ + var hourlydata = []; + var estimatedata = []; + for (var key in result.data) { + hourlydata.push({name: key, data: result.data[key]}); + } + for (var key in result.estimates) { + estimatedata.push({name: key, data: result.estimates[key]}); + } + result.data = hourlydata; + result.estimates = estimatedata; + $scope.legends = []; + $scope.stats = result.stats; + highchart.drawGraph(result, $scope); + + $scope.legendName = $scope.groupBy.name; + $scope.legend_usage_cost = $scope.usage_cost; + }, { includeEstimates: "" + $scope.includeEstimates}); + } + + $scope.accountsChanged = function() { + $scope.updateEstimates(); + } + + $scope.updateEstimates = function() { + usage_db.getDailyEstimate($scope); + } + + $scope.updateUsageTypes = function() { + usage_db.getUsageTypes($scope, function(data){ + }); + } + + $scope.removeEstimatesFilter = function(legend) { + return legend.name.indexOf("Estimate") == -1; + }; + + + var fn = function() { + usage_db.getAccounts($scope, function(data){ + //while (! $scope.selected_account) { + //we need to have these! + // } + }); + $scope.getData(); + + jQuery("#start, #end" ).datetimepicker({ + showTime: false, + showMinute: false, + ampm: true, + timeFormat: 'hhTT', + dateFormat: 'yy-mm-dd' + }); + jQuery('#end').datetimepicker().val($scope.end); + jQuery('#start').datetimepicker().val($scope.start); + } + + usage_db.getParams($location.hash(), $scope); + + if ($scope.spans) { + $http({ + method: "GET", + url: "getTimeSpan", + params: {spans: $scope.spans, end: $scope.end, consolidate: $scope.consolidate} + }).success(function(result) { + $scope.end = result.end; + $scope.start = result.start; + fn(); + }); + } else { + fn(); + } +} + + function appgroupCtrl($scope, $location, $http, usage_db, highchart) { // var predefinedQuery = {product: "ebs,ec2,ec2_instance,monitor,rds,s3"}; @@ -1496,7 +1681,7 @@ function summaryCtrl($scope, $location, usage_db, highchart) { {name: "UsageType"} ], $scope.groupBy = $scope.groupBys[2]; - $scope.consolidate = "hourly"; + $scope.consolidate = "monthly"; $scope.plotType = "area"; $scope.end = new Date(); $scope.start = new Date(); @@ -1526,7 +1711,7 @@ function summaryCtrl($scope, $location, usage_db, highchart) { $scope.order = function(index) { if ($scope.predicate != index) { - $scope.reservse = index === 'name'; + $scope.reserve = index === 'name'; $scope.predicate = index; } else { @@ -1804,4 +1989,3 @@ function editCtrl($scope, $location, $http) { } }); } -