public async Task <ActionResult> CreateOrUpdateAsync(string offerName, string planName, string meterName, [FromBody] CustomMeterDimension customMeterDimension) { AADAuthHelper.VerifyUserAccess(this.HttpContext, _logger, true); if (customMeterDimension == null) { throw new LunaBadRequestUserException(LoggingUtils.ComposePayloadNotProvidedErrorMessage(nameof(customMeterDimension)), UserErrorCode.PayloadNotProvided); } if (!planName.Equals(customMeterDimension.PlanName)) { throw new LunaBadRequestUserException(LoggingUtils.ComposeNameMismatchErrorMessage(nameof(offerName)), UserErrorCode.NameMismatch); } if (!meterName.Equals(customMeterDimension.MeterName)) { throw new LunaBadRequestUserException(LoggingUtils.ComposeNameMismatchErrorMessage(nameof(meterName)), UserErrorCode.NameMismatch); } if (await _customMeterDimensionService.ExistsAsync(offerName, planName, meterName)) { await _customMeterDimensionService.UpdateAsync(offerName, planName, meterName, customMeterDimension); return(Ok(customMeterDimension)); } else { await _customMeterDimensionService.CreateAsync(offerName, planName, meterName, customMeterDimension); return(CreatedAtRoute(nameof(GetAsync) + nameof(CustomMeterDimension), new { offerName = offerName, planName = planName, meterName = meterName }, customMeterDimension));; } }
private async Task ReportBatchMeterEventsInternal(Offer offer, CustomMeter meter, DateTime effectiveStartTime) { _logger.LogInformation($"Query and report meter event {meter.MeterName} starting {effectiveStartTime}."); DateTime effectiveEndTime = effectiveStartTime.AddHours(1); var telemetryDataConnector = await _telemetryDataConnectorService.GetAsync(meter.TelemetryDataConnectorName); ITelemetryDataConnector connector = _telemetryConnectionManager.CreateTelemetryDataConnector( telemetryDataConnector.Type, telemetryDataConnector.Configuration); IEnumerable <Usage> meterEvents = await connector.GetMeterEventsByHour(effectiveStartTime, meter.TelemetryQuery); // Get the billable meter events List <Usage> billableMeterEvents = new List <Usage>(); foreach (var meterEvent in meterEvents) { // Send the meter event only if: // 1. Subscription exists // 2. Subscription is in a plan using the current custom meter // 3. Meter usage is enabled // 4. The meter usage is not reported // 5. There's no error or error happened after the effective start time (we don't skip error). Guid subscriptionId; if (!Guid.TryParse(meterEvent.ResourceId, out subscriptionId)) { _logger.LogWarning($"ResourceId {meterEvent.ResourceId} is not a valid subscription. The data type should be GUID."); continue; } if (!await _subscriptionService.ExistsAsync(subscriptionId)) { _logger.LogWarning($"The subscription {subscriptionId} doesn't exist. Will not report the meter event {meterEvent.Dimension}."); continue; } if (!await _subscriptionCustomMeterUsageService.ExistsAsync(subscriptionId, meter.MeterName)) { _logger.LogWarning($"The subscription usage record with {subscriptionId} and meter name {meter.MeterName} doesn't exist. Will not report the meter event {meterEvent.Dimension}."); continue; } var subscription = await _subscriptionService.GetAsync(subscriptionId); var meterUsage = await _subscriptionCustomMeterUsageService.GetAsync(subscriptionId, meter.MeterName); if (await _customMeterDimensionService.ExistsAsync(offer.OfferName, subscription.PlanName, meter.MeterName) && meterUsage.IsEnabled && meterUsage.LastUpdatedTime < effectiveEndTime && (meterUsage.LastErrorReportedTime == DateTime.MinValue || meterUsage.LastErrorReportedTime >= effectiveStartTime)) { meterEvent.Dimension = meter.MeterName; meterEvent.PlanId = subscription.PlanName; billableMeterEvents.Add(meterEvent); } } CustomMeteringRequestResult requestResult = new CustomMeteringBatchSuccessResult(); if (billableMeterEvents.Count > 0) { requestResult = await _customMeteringClient.RecordBatchUsageAsync( Guid.NewGuid(), Guid.NewGuid(), billableMeterEvents, default); } else { // Create an empty result ((CustomMeteringBatchSuccessResult)requestResult).Success = true; ((CustomMeteringBatchSuccessResult)requestResult).Result = new List <CustomMeteringSuccessResult>(); } if (requestResult.Success) { CustomMeteringBatchSuccessResult batchResult = (CustomMeteringBatchSuccessResult)requestResult; foreach (var result in batchResult.Result) { var subscriptionId = Guid.Parse(result.ResourceId); var subscriptionMeterUsage = await _subscriptionCustomMeterUsageService.GetAsync(subscriptionId, meter.MeterName); if (result.Status.Equals(nameof(CustomMeterEventStatus.Accepted), StringComparison.InvariantCultureIgnoreCase) || result.Status.Equals(nameof(CustomMeterEventStatus.Duplicate), StringComparison.InvariantCultureIgnoreCase)) { _logger.LogWarning($"Meter event {result.Dimension} for subscription {result.ResourceId} at {result.EffectiveStartTime} reported at {DateTime.Now}, with status {result.Status}."); subscriptionMeterUsage.LastUpdatedTime = effectiveEndTime; subscriptionMeterUsage.LastErrorReportedTime = DateTime.MinValue; // Always record the reported meter event to Azure table if it is duplicate. var tableEntity = new CustomMeteringAzureTableEntity( result.Status.Equals(nameof(CustomMeterEventStatus.Accepted), StringComparison.InvariantCultureIgnoreCase) ? result : result.Error.AdditionalInfo.AcceptedMessage); await _storageUtility.InsertTableEntity(REPORTED_METER_EVENT_TABLE_NAME, tableEntity); if (effectiveEndTime > subscriptionMeterUsage.UnsubscribedTime) { _logger.LogInformation($"Disabled meter usage for meter {meter.MeterName} subscription {subscriptionId} at {effectiveEndTime}."); subscriptionMeterUsage.IsEnabled = false; subscriptionMeterUsage.DisabledTime = effectiveEndTime; } } else if (result.Status.Equals(nameof(CustomMeterEventStatus.Expired), StringComparison.InvariantCultureIgnoreCase)) { // If the meter event is expired, record a warning and move on _logger.LogWarning($"Meter event {result.Dimension} for subscription {result.ResourceId} at {result.EffectiveStartTime} expired at {DateTime.Now}."); subscriptionMeterUsage.LastUpdatedTime = effectiveEndTime; subscriptionMeterUsage.LastErrorReportedTime = DateTime.MinValue; await _storageUtility.InsertTableEntity(EXPIRED_METER_EVENT_TABLE_NAME, new CustomMeteringAzureTableEntity(result)); if (effectiveEndTime > subscriptionMeterUsage.UnsubscribedTime) { _logger.LogInformation($"Disabled meter usage for meter {meter.MeterName} subscription {subscriptionId} at {effectiveEndTime}."); subscriptionMeterUsage.IsEnabled = false; subscriptionMeterUsage.DisabledTime = effectiveEndTime; } } else { _logger.LogError($"Meter event {result.Dimension} for subscription {result.ResourceId} at {result.EffectiveStartTime} failed to report at {DateTime.Now}."); subscriptionMeterUsage.LastErrorReportedTime = effectiveEndTime; string errorMessage = ComposeErrorMessage(result.Error); subscriptionMeterUsage.LastError = $"Meter event failed with error {result.Status}. Details: {errorMessage}."; await _storageUtility.InsertTableEntity(FAILED_METER_EVENT_TABLE_NAME, new FailedCustomMeteringAzureTableEntity(result, errorMessage)); // If the meter event request failed with ResourceNotFound error, it could be caused by the time different between // end user cancel the subscription and SaaS service send the cancel request by calling the notification webhook if (effectiveEndTime > subscriptionMeterUsage.UnsubscribedTime && result.Status.Equals("ResourceNotFound")) { _logger.LogInformation($"Disabled meter usage for meter {meter.MeterName} subscription {subscriptionId} at {effectiveEndTime}."); subscriptionMeterUsage.IsEnabled = false; subscriptionMeterUsage.DisabledTime = effectiveEndTime; } } await _subscriptionCustomMeterUsageService.UpdateAsync(subscriptionId, meter.MeterName, subscriptionMeterUsage); } await _subscriptionCustomMeterUsageService.UpdateLastUpdatedTimeForUnreportedSubscriptions(offer.OfferName, meter.MeterName, effectiveEndTime); _logger.LogInformation($"Completed reporting custom meter events {meter.MeterName} starting {effectiveStartTime}."); } else { _logger.LogWarning($"Failed to send the batch meter event. Response code: {requestResult.Code}"); } }