@cloud-carbon-footprint/aws
Version:
The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.
459 lines • 25.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.tagNameToAthenaColumn = void 0;
const moment_1 = __importDefault(require("moment"));
const common_1 = require("@cloud-carbon-footprint/common");
const core_1 = require("@cloud-carbon-footprint/core");
const CostAndUsageTypes_1 = require("./CostAndUsageTypes");
const CostAndUsageReportsRow_1 = __importDefault(require("./CostAndUsageReportsRow"));
const domain_1 = require("../domain");
const AWSComputeEstimatesBuilder_1 = __importDefault(require("./AWSComputeEstimatesBuilder"));
const AWSMemoryEstimatesBuilder_1 = __importDefault(require("./AWSMemoryEstimatesBuilder"));
const AWSInstanceTypes_1 = require("./AWSInstanceTypes");
const AWSRegions_1 = require("./AWSRegions");
class CostAndUsageReports {
constructor(computeEstimator, ssdStorageEstimator, hddStorageEstimator, networkingEstimator, memoryEstimator, unknownEstimator, embodiedEmissionsEstimator, serviceWrapper) {
this.computeEstimator = computeEstimator;
this.ssdStorageEstimator = ssdStorageEstimator;
this.hddStorageEstimator = hddStorageEstimator;
this.networkingEstimator = networkingEstimator;
this.memoryEstimator = memoryEstimator;
this.unknownEstimator = unknownEstimator;
this.embodiedEmissionsEstimator = embodiedEmissionsEstimator;
this.serviceWrapper = serviceWrapper;
this.dataBaseName = (0, common_1.configLoader)().AWS.ATHENA_DB_NAME;
this.tableName = (0, common_1.configLoader)().AWS.ATHENA_DB_TABLE;
this.queryResultsLocation = (0, common_1.configLoader)().AWS.ATHENA_QUERY_RESULT_LOCATION;
this.costAndUsageReportsLogger = new common_1.Logger('CostAndUsageReports');
}
async getEstimates(start, end, grouping) {
const awsConfig = (0, common_1.configLoader)().AWS;
const tagNames = awsConfig.RESOURCE_TAG_NAMES;
const accountList = awsConfig.accounts;
const usageRows = await this.getUsage(start, end, grouping, tagNames, accountList);
usageRows.shift();
const results = [];
const unknownRows = [];
this.costAndUsageReportsLogger.info('Mapping over Usage Rows');
const accounts = {};
if (Array.isArray(accountList)) {
accountList.forEach((account) => {
if (account.name)
accounts[account.id] = account;
});
}
for (const rowData of usageRows) {
const costAndUsageReportRow = this.convertAthenaRowToCostAndUsageReportsRow(rowData.Data, tagNames, accounts);
const emissionsFactors = await (0, common_1.getEmissionsFactors)(costAndUsageReportRow.region, costAndUsageReportRow.timestamp.toISOString(), domain_1.AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH, AWSRegions_1.AWS_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES, this.costAndUsageReportsLogger);
const footprintEstimate = this.getFootprintEstimateFromUsageRow(costAndUsageReportRow, unknownRows, emissionsFactors);
if (footprintEstimate)
(0, core_1.appendOrAccumulateEstimatesByDay)(results, costAndUsageReportRow, footprintEstimate, grouping, tagNames);
}
if (results.length > 0) {
unknownRows.map((rowData) => {
const footprintEstimate = this.getEstimateForUnknownUsage(rowData);
if (footprintEstimate)
(0, core_1.appendOrAccumulateEstimatesByDay)(results, rowData, footprintEstimate, grouping, tagNames);
});
}
return results;
}
async getEstimatesFromInputData(inputData) {
const result = [];
const unknownRows = [];
for (const inputDataRow of inputData) {
const costAndUsageReportRow = new CostAndUsageReportsRow_1.default(null, '', '', inputDataRow.region, inputDataRow.serviceName, inputDataRow.usageType, inputDataRow.usageUnit, inputDataRow.vCpus != '' ? parseFloat(inputDataRow.vCpus) : null, 1, 1, {});
const dateTime = new Date().toISOString();
const emissionsFactors = await (0, common_1.getEmissionsFactors)(costAndUsageReportRow.region, dateTime, domain_1.AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH, AWSRegions_1.AWS_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES, this.costAndUsageReportsLogger);
const footprintEstimate = this.getFootprintEstimateFromUsageRow(costAndUsageReportRow, unknownRows, emissionsFactors);
if (footprintEstimate) {
result.push({
serviceName: inputDataRow.serviceName,
region: inputDataRow.region,
usageType: inputDataRow.usageType,
vCpus: inputDataRow.vCpus,
kilowattHours: footprintEstimate.kilowattHours,
co2e: footprintEstimate.co2e,
});
}
}
if (result.length > 0) {
unknownRows.map((inputDataRow) => {
const footprintEstimate = this.getEstimateForUnknownUsage(inputDataRow);
if (footprintEstimate)
result.push({
serviceName: inputDataRow.serviceName,
region: inputDataRow.region,
usageType: inputDataRow.usageType,
vCpus: inputDataRow.vCpus,
kilowattHours: footprintEstimate.kilowattHours,
co2e: footprintEstimate.co2e,
});
});
}
return result;
}
getFootprintEstimateFromUsageRow(costAndUsageReportRow, unknownRows, emissionsFactors) {
if (this.usageTypeIsUnsupported(costAndUsageReportRow.usageType))
return;
if (this.usageTypeIsUnknown(costAndUsageReportRow.usageType) ||
this.usageUnitIsUnknown(costAndUsageReportRow.usageUnit)) {
unknownRows.push(costAndUsageReportRow);
return;
}
return this.getEstimateByUsageUnit(costAndUsageReportRow, emissionsFactors);
}
getEstimateByUsageUnit(costAndUsageReportRow, emissionsFactors) {
const powerUsageEffectiveness = domain_1.AWS_CLOUD_CONSTANTS.getPUE(costAndUsageReportRow.region);
switch (costAndUsageReportRow.usageUnit) {
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.HOURS_1:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.HOURS_2:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.HOURS_3:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.VCPU_HOURS:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.DPU_HOUR:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.ACU_HOUR:
const computeFootprint = new AWSComputeEstimatesBuilder_1.default(costAndUsageReportRow, this.computeEstimator, emissionsFactors).computeFootprint;
const memoryFootprint = new AWSMemoryEstimatesBuilder_1.default(costAndUsageReportRow, this.memoryEstimator, emissionsFactors).memoryFootprint;
const embodiedEmissions = this.getEmbodiedEmissions(costAndUsageReportRow, emissionsFactors);
if (isNaN(computeFootprint.kilowattHours)) {
this.costAndUsageReportsLogger.warn(`Could not estimate compute usage for usage type: ${costAndUsageReportRow.usageType}`);
return {
timestamp: computeFootprint.timestamp,
kilowattHours: 0,
co2e: computeFootprint.co2e,
usesAverageCPUConstant: computeFootprint.usesAverageCPUConstant,
};
}
if (memoryFootprint.co2e || embodiedEmissions.co2e) {
const kilowattHours = computeFootprint.kilowattHours +
memoryFootprint.kilowattHours +
embodiedEmissions.kilowattHours;
(0, core_1.accumulateKilowattHours)(domain_1.AWS_CLOUD_CONSTANTS.KILOWATT_HOURS_BY_SERVICE_AND_USAGE_UNIT, costAndUsageReportRow, kilowattHours, core_1.AccumulateKilowattHoursBy.COST);
return {
timestamp: computeFootprint.timestamp,
kilowattHours: kilowattHours,
co2e: computeFootprint.co2e +
memoryFootprint.co2e +
embodiedEmissions.co2e,
usesAverageCPUConstant: computeFootprint.usesAverageCPUConstant,
};
}
if (computeFootprint)
(0, core_1.accumulateKilowattHours)(domain_1.AWS_CLOUD_CONSTANTS.KILOWATT_HOURS_BY_SERVICE_AND_USAGE_UNIT, costAndUsageReportRow, computeFootprint.kilowattHours, core_1.AccumulateKilowattHoursBy.COST);
return computeFootprint;
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_MONTH_1:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_MONTH_2:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_MONTH_3:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_MONTH_4:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_HOURS:
return this.getStorageFootprintEstimate(costAndUsageReportRow, powerUsageEffectiveness, emissionsFactors);
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.SECONDS_1:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.SECONDS_2:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.LAMBDA_SECONDS:
costAndUsageReportRow.vCpuHours =
costAndUsageReportRow.usageAmount / 3600;
return new AWSComputeEstimatesBuilder_1.default(costAndUsageReportRow, this.computeEstimator, emissionsFactors).computeFootprint;
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_1:
case CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_2:
return this.getNetworkingFootprintEstimate(costAndUsageReportRow, powerUsageEffectiveness, emissionsFactors);
default:
this.costAndUsageReportsLogger.warn(`Unexpected pricing unit: ${costAndUsageReportRow.usageUnit}`);
return {
timestamp: new Date(),
kilowattHours: 0,
co2e: 0,
usesAverageCPUConstant: false,
};
}
}
getEstimateForUnknownUsage(rowData) {
const unknownUsage = {
timestamp: rowData.timestamp,
cost: rowData.cost,
usageUnit: rowData.usageUnit,
};
const unknownConstants = {
kilowattHoursByServiceAndUsageUnit: domain_1.AWS_CLOUD_CONSTANTS.KILOWATT_HOURS_BY_SERVICE_AND_USAGE_UNIT,
};
return this.unknownEstimator.estimate([unknownUsage], rowData.region, domain_1.AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH, unknownConstants)[0];
}
getNetworkingFootprintEstimate(costAndUsageReportRow, powerUsageEffectiveness, emissionsFactors) {
let networkingEstimate;
if (this.usageTypeIsNetworking(costAndUsageReportRow)) {
const networkingUsage = {
timestamp: costAndUsageReportRow.timestamp,
gigabytes: costAndUsageReportRow.usageAmount,
};
const networkingConstants = {
powerUsageEffectiveness: powerUsageEffectiveness,
};
networkingEstimate = this.networkingEstimator.estimate([networkingUsage], costAndUsageReportRow.region, emissionsFactors, networkingConstants)[0];
}
if (networkingEstimate) {
networkingEstimate.usesAverageCPUConstant = false;
(0, core_1.accumulateKilowattHours)(domain_1.AWS_CLOUD_CONSTANTS.KILOWATT_HOURS_BY_SERVICE_AND_USAGE_UNIT, costAndUsageReportRow, networkingEstimate.kilowattHours, core_1.AccumulateKilowattHoursBy.COST);
}
return networkingEstimate;
}
getStorageFootprintEstimate(costAndUsageReportRow, powerUsageEffectiveness, emissionsFactors) {
const usageAmountTerabyteHours = this.getUsageAmountInTerabyteHours(costAndUsageReportRow);
const storageUsage = {
timestamp: costAndUsageReportRow.timestamp,
terabyteHours: usageAmountTerabyteHours,
};
const storageConstants = {
powerUsageEffectiveness: powerUsageEffectiveness,
replicationFactor: costAndUsageReportRow.replicationFactor,
};
let estimate;
if (this.usageTypeIsSSD(costAndUsageReportRow))
estimate = this.ssdStorageEstimator.estimate([storageUsage], costAndUsageReportRow.region, emissionsFactors, storageConstants)[0];
else if (this.usageTypeIsHDD(costAndUsageReportRow.usageType))
estimate = this.hddStorageEstimator.estimate([storageUsage], costAndUsageReportRow.region, emissionsFactors, storageConstants)[0];
else
this.costAndUsageReportsLogger.warn(`Unexpected usage type for storage estimation. Usage type: ${costAndUsageReportRow.usageType}`);
if (estimate) {
estimate.usesAverageCPUConstant = false;
(0, core_1.accumulateKilowattHours)(domain_1.AWS_CLOUD_CONSTANTS.KILOWATT_HOURS_BY_SERVICE_AND_USAGE_UNIT, costAndUsageReportRow, estimate.kilowattHours, core_1.AccumulateKilowattHoursBy.COST);
}
return estimate;
}
getUsageAmountInTerabyteHours(costAndUsageReportRow) {
if (this.usageTypeisByteHours(costAndUsageReportRow.usageType)) {
return (0, common_1.convertBytesToTerabytes)(costAndUsageReportRow.usageAmount);
}
if (costAndUsageReportRow.usageUnit === CostAndUsageTypes_1.KNOWN_USAGE_UNITS.GB_HOURS) {
return (0, common_1.convertGigabyteHoursToTerabyteHours)(costAndUsageReportRow.usageAmount);
}
return (0, common_1.convertGigabyteMonthsToTerabyteHours)(costAndUsageReportRow.usageAmount, costAndUsageReportRow.timestamp);
}
usageTypeIsSSD(costAndUsageRow) {
return ((0, common_1.endsWithAny)(CostAndUsageTypes_1.SSD_USAGE_TYPES, costAndUsageRow.usageType) ||
((0, common_1.endsWithAny)(CostAndUsageTypes_1.SSD_SERVICES, costAndUsageRow.serviceName) &&
!costAndUsageRow.usageType.includes('Backup')));
}
usageTypeIsHDD(usageType) {
return (0, common_1.endsWithAny)(CostAndUsageTypes_1.HDD_USAGE_TYPES, usageType);
}
usageTypeisByteHours(usageType) {
return (0, common_1.endsWithAny)(CostAndUsageTypes_1.BYTE_HOURS_USAGE_TYPES, usageType);
}
usageTypeIsNetworking(costAndUsageRow) {
return ((0, common_1.endsWithAny)(CostAndUsageTypes_1.NETWORKING_USAGE_TYPES, costAndUsageRow.usageType) &&
costAndUsageRow.serviceName !== 'AmazonCloudFront');
}
usageTypeIsUnsupported(usageType) {
return ((0, common_1.endsWithAny)(CostAndUsageTypes_1.UNSUPPORTED_USAGE_TYPES, usageType) ||
CostAndUsageTypes_1.UNSUPPORTED_USAGE_TYPES.some((unsupportedUsageType) => usageType.includes(unsupportedUsageType)));
}
usageTypeIsUnknown(usageType) {
return ((0, common_1.endsWithAny)(CostAndUsageTypes_1.UNKNOWN_USAGE_TYPES, usageType) ||
CostAndUsageTypes_1.UNKNOWN_USAGE_TYPES.some((unknownUsageType) => usageType.includes(unknownUsageType)));
}
usageUnitIsUnknown(usageUnit) {
return !Object.values(CostAndUsageTypes_1.KNOWN_USAGE_UNITS).some((knownUsageUnit) => knownUsageUnit === usageUnit);
}
async getUsage(start, end, grouping, tagNames, accounts) {
const dateGranularity = CostAndUsageTypes_1.AWS_QUERY_GROUP_BY[grouping];
const dateExpression = `DATE(DATE_TRUNC('${dateGranularity}', line_item_usage_start_date))`;
const lineItemTypes = CostAndUsageTypes_1.LINE_ITEM_TYPES.join(`', '`);
const startDate = new Date(moment_1.default.utc(start).startOf('day'));
const endDate = new Date(moment_1.default.utc(end).endOf('day'));
const hasCpuColumn = await this.checkIfColumnExists('product_vcpu');
const optionalColumns = [];
let optionalColumnSelects = '';
if (hasCpuColumn) {
optionalColumns.push('product_vcpu');
optionalColumnSelects += 'product_vcpu as vCpus,\n';
}
else {
this.costAndUsageReportsLogger.warn(`'product_vcpu' column could not be verified in Athena table schema. This may occur if there was an error fetching the schema or when there is no historical CPU usage (i.e. EC2) for the configured account. The CPU column will be excluded from Athena Query`);
}
const tagColumnNames = tagNames.map(exports.tagNameToAthenaColumn);
const tagSelectionExpression = tagColumnNames
.map((column) => `, ${column} as ${column}`)
.join('\n');
const groupByColumnNames = [
dateExpression,
'line_item_usage_account_id',
'product_region',
'line_item_product_code',
'line_item_usage_type',
'pricing_unit',
...optionalColumns,
...tagColumnNames,
].join(', ');
const accountFilter = this.buildAccountFilter(accounts);
const queryString = `SELECT ${dateExpression} AS timestamp,
line_item_usage_account_id as accountName,
product_region as region,
line_item_product_code as serviceName,
line_item_usage_type as usageType,
pricing_unit as usageUnit,
${optionalColumnSelects}
SUM(line_item_usage_amount) as usageAmount,
SUM(line_item_blended_cost) as cost
${tagSelectionExpression}
FROM ${this.tableName}
WHERE line_item_line_item_type IN ('${lineItemTypes}')
AND line_item_usage_start_date BETWEEN from_iso8601_timestamp('${moment_1.default
.utc(startDate)
.toISOString()}') AND from_iso8601_timestamp('${moment_1.default
.utc(endDate)
.toISOString()}')
${accountFilter}
GROUP BY ${groupByColumnNames}`;
const params = {
QueryString: queryString,
QueryExecutionContext: {
Database: this.dataBaseName,
},
ResultConfiguration: {
EncryptionConfiguration: {
EncryptionOption: 'SSE_S3',
},
OutputLocation: this.queryResultsLocation,
},
};
const response = await this.startQuery(params);
const queryExecutionInput = {
QueryExecutionId: response.QueryExecutionId,
};
return await this.getQueryResultSetRows(queryExecutionInput);
}
async startQuery(queryParams) {
let response;
try {
response = await this.serviceWrapper.startAthenaQueryExecution(queryParams);
this.costAndUsageReportsLogger.info('Started Athena Query Execution');
}
catch (e) {
throw new Error(`Athena start query failed. Reason ${e.message}.`);
}
return response;
}
async getQueryResultSetRows(queryExecutionInput) {
this.costAndUsageReportsLogger.info('Getting Athena Query Execution');
while (true) {
const queryExecutionResults = await this.serviceWrapper.getAthenaQueryExecution(queryExecutionInput);
const queryStatus = queryExecutionResults.QueryExecution.Status;
if (queryStatus.State === ('FAILED' || 'CANCELLED'))
throw new Error(`Athena query failed. Reason ${queryStatus.StateChangeReason}. Query ID: ${queryExecutionInput.QueryExecutionId}`);
if (queryStatus.State === 'SUCCEEDED')
break;
await (0, common_1.wait)(1000);
}
this.costAndUsageReportsLogger.info('Getting Athena Query Result Sets');
const results = await this.serviceWrapper.getAthenaQueryResultSets(queryExecutionInput);
return results.flatMap((result) => result.ResultSet.Rows);
}
getEmbodiedEmissions(costAndUsageReportRow, emissionsFactors) {
const { instancevCpu, scopeThreeEmissions, largestInstancevCpu } = this.getDataFromInstanceType(costAndUsageReportRow.instanceType);
if (!instancevCpu || !scopeThreeEmissions || !largestInstancevCpu)
return {
timestamp: undefined,
kilowattHours: 0,
co2e: 0,
};
const embodiedEmissionsUsage = {
instancevCpu,
largestInstancevCpu,
usageTimePeriod: costAndUsageReportRow.usageAmount / instancevCpu,
scopeThreeEmissions,
};
return this.embodiedEmissionsEstimator.estimate([embodiedEmissionsUsage], costAndUsageReportRow.region, emissionsFactors)[0];
}
getDataFromInstanceType(instanceType) {
const instanceTypeDetails = instanceType.split('.');
const instanceSize = instanceTypeDetails[instanceTypeDetails.length - 1];
const instanceFamily = instanceTypeDetails[instanceTypeDetails.length - 2];
if (!instanceSize || !instanceFamily) {
return {
instancevCpu: 0,
scopeThreeEmissions: 0,
largestInstancevCpu: 0,
};
}
const instancevCpu = AWSInstanceTypes_1.EC2_INSTANCE_TYPES[instanceFamily]?.[instanceSize]?.[0] ||
AWSInstanceTypes_1.INSTANCE_FAMILY_TO_INSTANCE_TYPE_MAPPING[instanceFamily]?.[instanceSize]?.[0];
const scopeThreeEmissions = AWSInstanceTypes_1.EC2_INSTANCE_TYPES[instanceFamily]?.[instanceSize]?.[2] ||
AWSInstanceTypes_1.INSTANCE_FAMILY_TO_INSTANCE_TYPE_MAPPING[instanceFamily]?.[instanceSize]?.[1];
const familyInstanceTypes = Object.values(AWSInstanceTypes_1.EC2_INSTANCE_TYPES[instanceFamily] ||
AWSInstanceTypes_1.INSTANCE_FAMILY_TO_INSTANCE_TYPE_MAPPING[instanceFamily] ||
{});
const [largestInstancevCpu] = familyInstanceTypes[familyInstanceTypes.length - 1] || [];
return {
instancevCpu,
scopeThreeEmissions,
largestInstancevCpu,
};
}
convertAthenaRowToCostAndUsageReportsRow(rowData, tagNames, accounts) {
const timestamp = new Date(rowData[0].VarCharValue);
const accountId = rowData[1].VarCharValue;
const accountName = accounts[accountId]?.name || accountId;
const region = rowData[2].VarCharValue;
const serviceName = rowData[3].VarCharValue;
const usageType = rowData[4].VarCharValue;
const usageUnit = rowData[5].VarCharValue;
const vCpus = rowData[6].VarCharValue != '' ? parseFloat(rowData[6].VarCharValue) : null;
const usageAmount = parseFloat(rowData[7].VarCharValue);
const cost = parseFloat(rowData[8].VarCharValue);
const tags = Object.fromEntries(tagNames.map((name, i) => [name, rowData[i + 9].VarCharValue]));
return new CostAndUsageReportsRow_1.default(timestamp, accountId, accountName, region, serviceName, usageType, usageUnit, vCpus, usageAmount, cost, tags);
}
async checkIfColumnExists(columnName) {
try {
const athenaTableDescription = await this.serviceWrapper.getAthenaTableDescription({
DatabaseName: this.dataBaseName,
Name: this.tableName,
});
const columns = athenaTableDescription.Table?.StorageDescriptor?.Columns;
return columns?.some((column) => column.Name === columnName);
}
catch (error) {
this.costAndUsageReportsLogger.error(`Error verifying schema for Athena table: "${this.tableName}"`, error);
return false;
}
}
buildAccountFilter(accounts) {
let accountFilter = '';
try {
accountFilter = (0, common_1.buildAccountFilter)(accounts, 'line_item_usage_account_id');
}
catch (e) {
this.costAndUsageReportsLogger.warn('Configured list of AWS accounts is invalid. AWS Accounts must be a list of objects containing account details or a list of account IDs. Ignoring account filter...');
}
return accountFilter;
}
}
exports.default = CostAndUsageReports;
const tagNameToAthenaColumn = (tagName) => {
let columnName = 'resource_tags_';
for (const char of tagName) {
if (char == ':') {
columnName = columnName + '_';
}
else {
if (isUpperCase(char) && !lastCharacterIs(columnName, '_')) {
columnName = columnName + '_';
}
columnName = columnName + char.toLowerCase();
}
}
return columnName;
};
exports.tagNameToAthenaColumn = tagNameToAthenaColumn;
const isUpperCase = (text) => {
return text.toUpperCase() === text;
};
const lastCharacterIs = (text, char) => {
if (text.length === 0) {
return false;
}
return text[text.length - 1] === char;
};
//# sourceMappingURL=CostAndUsageReports.js.map