diff --git a/src/Insights/Customizations/MetricOperations.Customization.cs b/src/Insights/Customizations/MetricOperations.Customization.cs
index 1ae43a233dc8..dba9f0a36caf 100644
--- a/src/Insights/Customizations/MetricOperations.Customization.cs
+++ b/src/Insights/Customizations/MetricOperations.Customization.cs
@@ -21,125 +21,63 @@
using System.Threading;
using System.Threading.Tasks;
using Hyak.Common;
+using Microsoft.Azure.Insights.Customizations.Shoebox;
using Microsoft.Azure.Insights.Models;
namespace Microsoft.Azure.Insights
{
+ ///
+ /// Thick client class for getting metrics
+ ///
internal partial class MetricOperations
{
public async Task GetMetricsAsync(string resourceUri, string filterString, CancellationToken cancellationToken)
{
- // Ensure exactly one '/' at the start
- resourceUri = '/' + resourceUri.TrimStart('/');
+ if (resourceUri == null)
+ {
+ throw new ArgumentNullException("resourceUri");
+ }
// Generate filter strings
MetricFilter filter = MetricFilterExpressionParser.Parse(filterString);
- string metricFilterString = GenerateNamelessMetricFilterString(filter);
- string definitionFilterString = filter.DimensionFilters == null ? null
+ string filterStringNamesOnly = filter.DimensionFilters == null ? null
: ShoeboxHelper.GenerateMetricDefinitionFilterString(filter.DimensionFilters.Select(df => df.Name));
// Get definitions for requested metrics
IList definitions =
(await this.Client.MetricDefinitionOperations.GetMetricDefinitionsAsync(
resourceUri,
- definitionFilterString,
+ filterStringNamesOnly,
cancellationToken).ConfigureAwait(false)).MetricDefinitionCollection.Value;
- // Separate passthrough metrics with dimensions specified
- IEnumerable passthruDefinitions = definitions.Where(d => !IsShoebox(d, filter.TimeGrain));
- IEnumerable shoeboxDefinitions = definitions.Where(d => IsShoebox(d, filter.TimeGrain));
-
- // Get Passthru definitions
- List passthruMetrics = new List();
- string invocationId = TracingAdapter.NextInvocationId.ToString(CultureInfo.InvariantCulture);
- this.LogStartGetMetrics(invocationId, resourceUri, filterString, passthruDefinitions);
- if (passthruDefinitions.Any())
- {
- // Create new filter for passthru metrics
- List passthruDimensionFilters = filter.DimensionFilters == null ? new List() :
- filter.DimensionFilters.Where(df => passthruDefinitions.Any(d => string.Equals(d.Name.Value, df.Name, StringComparison.OrdinalIgnoreCase))).ToList();
-
- foreach (MetricDefinition def in passthruDefinitions
- .Where(d => !passthruDimensionFilters.Any(pdf => string.Equals(pdf.Name, d.Name.Value, StringComparison.OrdinalIgnoreCase))))
- {
- passthruDimensionFilters.Add(new MetricDimension() { Name = def.Name.Value });
- }
-
- MetricFilter passthruFilter = new MetricFilter()
- {
- TimeGrain = filter.TimeGrain,
- StartTime = filter.StartTime,
- EndTime = filter.EndTime,
- DimensionFilters = passthruDimensionFilters
- };
-
- // Create passthru filter string
- string passthruFilterString = ShoeboxHelper.GenerateMetricFilterString(passthruFilter);
-
- // Get Metrics from passthrough (hydra) client
- MetricListResponse passthruResponse = await this.GetMetricsInternalAsync(resourceUri, passthruFilterString, cancellationToken).ConfigureAwait(false);
- passthruMetrics = passthruResponse.MetricCollection.Value.ToList();
-
- this.LogMetricCountFromResponses(invocationId, passthruMetrics.Count());
-
- // Fill in values (resourceUri, displayName, unit) from definitions
- CompleteShoeboxMetrics(passthruMetrics, passthruDefinitions, resourceUri);
-
- // Add empty objects for metrics that had no values come back, ensuring a metric is returned for each definition
- IEnumerable emptyMetrics = passthruDefinitions
- .Where(d => !passthruMetrics.Any(m => string.Equals(m.Name.Value, d.Name.Value, StringComparison.OrdinalIgnoreCase)))
- .Select(d => new Metric()
- {
- Name = d.Name,
- Unit = d.Unit,
- ResourceId = resourceUri,
- StartTime = filter.StartTime,
- EndTime = filter.EndTime,
- TimeGrain = filter.TimeGrain,
- MetricValues = new List(),
- Properties = new Dictionary()
- });
-
- passthruMetrics.AddRange(emptyMetrics);
- }
-
- // Get Metrics by definitions
- MetricListResponse shoeboxResponse = await this.GetMetricsAsync(resourceUri, metricFilterString, shoeboxDefinitions, cancellationToken).ConfigureAwait(false);
-
- // Create response (merge and wrap metrics)
- MetricListResponse result = new MetricListResponse()
- {
- RequestId = Guid.NewGuid().ToString("D"),
- StatusCode = HttpStatusCode.OK,
- MetricCollection = new MetricCollection()
- {
- Value = passthruMetrics.Union(shoeboxResponse.MetricCollection.Value).ToList()
- }
- };
-
- this.LogEndGetMetrics(invocationId, result);
-
- return result;
+ // Get Metrics with definitions
+ return await this.GetMetricsAsync(resourceUri, filterString, definitions, cancellationToken);
}
// Alternate method for getting metrics by passing in the definitions
- // TODO [davmc]: Revisit - this method cannot support dimensions
public async Task GetMetricsAsync(
string resourceUri, string filterString, IEnumerable definitions, CancellationToken cancellationToken)
{
- MetricListResponse result;
-
if (definitions == null)
{
throw new ArgumentNullException("definitions");
}
+ if (resourceUri == null)
+ {
+ throw new ArgumentNullException("resourceUri");
+ }
+
+ // Ensure exactly one '/' at the start
+ resourceUri = '/' + resourceUri.TrimStart('/');
+
+ MetricListResponse result;
string invocationId = TracingAdapter.NextInvocationId.ToString(CultureInfo.InvariantCulture);
- this.LogStartGetMetrics(invocationId, resourceUri, filterString, definitions);
// If no definitions provided, return empty collection
if (!definitions.Any())
{
+ this.LogStartGetMetrics(invocationId, resourceUri, filterString, definitions);
result = new MetricListResponse()
{
RequestId = Guid.NewGuid().ToString("D"),
@@ -158,35 +96,73 @@ public async Task GetMetricsAsync(
// Parse MetricFilter
MetricFilter filter = MetricFilterExpressionParser.Parse(filterString);
- // Names not allowed in filter since the names are in the definitions
+ // Names in filter must match the names in the definitions
if (filter.DimensionFilters != null && filter.DimensionFilters.Any())
{
- throw new ArgumentException("Cannot specify names (or dimensions) when MetricDefinitions are included", "filterString");
- }
+ IEnumerable filterNames = filter.DimensionFilters.Select(df => df.Name);
+ IEnumerable definitionNames = definitions.Select(d => d.Name.Value);
+ IEnumerable filterOnly = filterNames.Where(fn => !definitionNames.Contains(fn, StringComparer.InvariantCultureIgnoreCase));
+ IEnumerable definitionOnly = definitionNames.Where(df => !filterNames.Contains(df, StringComparer.InvariantCultureIgnoreCase));
- // Ensure every definition has at least one availability matching the filter timegrain
- if (!definitions.All(d => d.MetricAvailabilities.Any(a => a.TimeGrain == filter.TimeGrain)))
+ if (filterOnly.Any() || definitionOnly.Any())
+ {
+ throw new ArgumentException("Set of names specified in filter string must match set of names in provided definitions", "filterString");
+ }
+
+ // "Filter out" metrics with unsupported dimensions
+ definitions = definitions.Where(d => SupportsRequestedDimensions(d, filter));
+ }
+ else
{
- throw new ArgumentException("Definition contains no availability for the timeGrain requested", "definitions");
+ filter = new MetricFilter()
+ {
+ TimeGrain = filter.TimeGrain,
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ DimensionFilters = definitions.Select(d => new MetricDimension()
+ {
+ Name = d.Name.Value
+ })
+ };
}
- // Group definitions by location so we can make one request to each location
- Dictionary groups =
- definitions.GroupBy(d => d.MetricAvailabilities.First(a => a.TimeGrain == filter.TimeGrain),
- new AvailabilityComparer()).ToDictionary(g => g.Key, g => new MetricFilter()
- {
- TimeGrain = filter.TimeGrain,
- StartTime = filter.StartTime,
- EndTime = filter.EndTime,
- DimensionFilters = g.Select(d => new MetricDimension() { Name = d.Name.Value })
- });
+ // Parse out provider name and determine if provider is storage
+ string providerName = this.GetProviderFromResourceId(resourceUri);
+ bool isStorageProvider =
+ string.Equals(providerName, "Microsoft.Storage", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(providerName, "Microsoft.ClassicStorage", StringComparison.OrdinalIgnoreCase);
+
+ // Create supported MetricRetrievers
+ IMetricRetriever proxyRetriever = new ProxyMetricRetriever(this);
+ IMetricRetriever shoeboxRetriever = new ShoeboxMetricRetriever();
+ IMetricRetriever storageRetriever = new StorageMetricRetriever();
+ IMetricRetriever emptyRetriever = EmptyMetricRetriever.Instance;
- // Get Metrics from each location (group)
- IEnumerable> locationTasks = groups.Select(g => g.Key.Location == null
- ? this.GetMetricsInternalAsync(resourceUri, ShoeboxHelper.GenerateMetricFilterString(g.Value), cancellationToken)
- : ShoeboxClient.GetMetricsInternalAsync(g.Value, g.Key.Location, invocationId));
+ // Create the selector function here so it has access to the retrievers, filter, and providerName
+ Func retrieverSelector = (d) =>
+ {
+ if (!d.MetricAvailabilities.Any())
+ {
+ return emptyRetriever;
+ }
+
+ if (!IsSasMetric(d, filter.TimeGrain))
+ {
+ return proxyRetriever;
+ }
+
+ return isStorageProvider ? storageRetriever : shoeboxRetriever;
+ };
+
+ // Group definitions by retriever so we can make one request to each retriever
+ IEnumerable> groups = definitions.GroupBy(retrieverSelector);
+
+ // Get Metrics from each retriever (group)
+ IEnumerable> locationTasks = groups.Select(g =>
+ g.Key.GetMetricsAsync(resourceUri, GetFilterStringForDefinitions(filter, g), g, invocationId));
// Aggregate metrics from all groups
+ this.LogStartGetMetrics(invocationId, resourceUri, filterString, definitions);
MetricListResponse[] results = (await Task.Factory.ContinueWhenAll(locationTasks.ToArray(), tasks => tasks.Select(t => t.Result))).ToArray();
IEnumerable metrics = results.Aggregate>(
new List(), (list, response) => list.Union(response.MetricCollection.Value));
@@ -197,19 +173,11 @@ public async Task GetMetricsAsync(
CompleteShoeboxMetrics(metrics, definitions, resourceUri);
// Add empty objects for metrics that had no values come back, ensuring a metric is returned for each definition
- IEnumerable emptyMetrics = definitions
- .Where(d => !metrics.Any(m => string.Equals(m.Name.Value, d.Name.Value, StringComparison.OrdinalIgnoreCase)))
- .Select(d => new Metric()
- {
- Name = d.Name,
- Unit = d.Unit,
- ResourceId = resourceUri,
- StartTime = filter.StartTime,
- EndTime = filter.EndTime,
- TimeGrain = filter.TimeGrain,
- MetricValues = new List(),
- Properties = new Dictionary()
- });
+ IEnumerable emptyMetrics = (await emptyRetriever.GetMetricsAsync(
+ resourceUri,
+ filterString,
+ definitions.Where(d => !metrics.Any(m => string.Equals(m.Name.Value, d.Name.Value, StringComparison.OrdinalIgnoreCase))),
+ invocationId)).MetricCollection.Value;
// Create response (merge and wrap metrics)
result = new MetricListResponse()
@@ -227,6 +195,65 @@ public async Task GetMetricsAsync(
return result;
}
+ private string GetProviderFromResourceId(string resourceId)
+ {
+ // Find start index of provider name
+ string knownStart = "/subscriptions/" + this.Client.Credentials.SubscriptionId + "/resourceGroups/";
+ int endOfResourceGroup = resourceId.IndexOf('/', knownStart.Length);
+
+ // skip /providers/
+ // plus 1 to start index to skip first '/', plus 1 to result to skip last '/'
+ int startOfProviderName = resourceId.IndexOf('/', endOfResourceGroup + 1) + 1;
+ int endOfProviderName = resourceId.IndexOf('/', startOfProviderName);
+
+ return resourceId.Substring(startOfProviderName, endOfProviderName - startOfProviderName);
+ }
+
+ private string GetFilterStringForDefinitions(MetricFilter filter, IEnumerable definitions)
+ {
+ return ShoeboxHelper.GenerateMetricFilterString(new MetricFilter()
+ {
+ TimeGrain = filter.TimeGrain,
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ DimensionFilters = filter.DimensionFilters.Where(df =>
+ definitions.Any(d => string.Equals(df.Name, d.Name.Value, StringComparison.OrdinalIgnoreCase)))
+ });
+ }
+
+ private bool SupportsRequestedDimensions(MetricDefinition definition, MetricFilter filter)
+ {
+ MetricDimension metric = filter.DimensionFilters.FirstOrDefault(df => string.Equals(df.Name, definition.Name.Value, StringComparison.OrdinalIgnoreCase));
+ var supportedDimensionNames = definition.Dimensions.Select(dim => dim.Name);
+ var supportedDimensionValues = definition.Dimensions.ToDictionary(dim => dim.Name.Value, dim => dim.Values.Select(v => v.Value));
+
+ // No dimensions specified for this metric
+ if (metric == null || metric.Dimensions == null)
+ {
+ return true;
+ }
+
+ foreach (FilterDimension dimension in metric.Dimensions)
+ {
+ // find dimension in definition
+ Dimension d = definition.Dimensions.FirstOrDefault(dim => string.Equals(dim.Name.Value, dimension.Name));
+
+ // Dimension name does't show up in definition
+ if (d == null)
+ {
+ return false;
+ }
+
+ // Requested dimension has any value that don't show up in the values list for the definiton
+ if (dimension.Values.Any(value => !d.Values.Select(v => v.Value).Contains(value, StringComparer.OrdinalIgnoreCase)))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private void LogMetricCountFromResponses(string invocationId, int metricsCount)
{
if (TracingAdapter.IsEnabled)
@@ -270,52 +297,27 @@ private static void CompleteShoeboxMetrics(IEnumerable collection, IEnum
}
}
- private static bool IsShoebox(MetricDefinition definition, TimeSpan timeGrain)
+ private static bool IsSasMetric(MetricDefinition definition, TimeSpan timeGrain)
{
MetricAvailability availability = definition.MetricAvailabilities.FirstOrDefault(a => a.TimeGrain.Equals(timeGrain));
- if (availability == null)
+ // Definition has requested availability, Location is null (non-SAS) or contains SAS key
+ if (availability != null)
{
- throw new InvalidOperationException(string.Format(
- CultureInfo.InvariantCulture, "MetricDefinition for {0} does not contain an availability with timegrain {1}", definition.Name.Value, timeGrain));
+ return availability.Location != null;
}
- return availability.Location != null;
- }
-
- private static string GenerateNamelessMetricFilterString(MetricFilter filter)
- {
- MetricFilter nameless = new MetricFilter()
+ // Definition has availabilities, but none with the requested timegrain (Bad request)
+ if (definition.MetricAvailabilities.Any())
{
- TimeGrain = filter.TimeGrain,
- StartTime = filter.StartTime,
- EndTime = filter.EndTime
- };
-
- return ShoeboxHelper.GenerateMetricFilterString(nameless);
- }
-
- private class AvailabilityComparer : IEqualityComparer
- {
- public bool Equals(MetricAvailability x, MetricAvailability y)
- {
- if (x.Location == null && y.Location == null)
- {
- return true;
- }
-
- if (x.Location == null || y.Location == null)
- {
- return false;
- }
-
- return x.Location.TableEndpoint == y.Location.TableEndpoint;
- }
-
- public int GetHashCode(MetricAvailability obj)
- {
- return obj.Location == null ? 0 : obj.Location.TableEndpoint.GetHashCode();
+ throw new InvalidOperationException(string.Format(
+ CultureInfo.InvariantCulture,
+ "MetricDefinition for {0} does not contain an availability with timegrain {1}",
+ definition.Name.Value, timeGrain));
}
+
+ // Definition has no availablilities (metrics are not configured for this resource), return empty metrics (non-SAS)
+ return false;
}
}
}
diff --git a/src/Insights/Customizations/Shoebox/EmptyMetricRetriever.cs b/src/Insights/Customizations/Shoebox/EmptyMetricRetriever.cs
new file mode 100644
index 000000000000..cd4d75d0034c
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/EmptyMetricRetriever.cs
@@ -0,0 +1,55 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.Azure.Insights.Models;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// EmptyMetricRetriever always returns a metric for every definition passed in. These metrics will have an empty list of MetricValues
+ /// EmptyMetricRetriever ignores dimensions
+ ///
+ internal class EmptyMetricRetriever : IMetricRetriever
+ {
+ private static readonly EmptyMetricRetriever _instance = new EmptyMetricRetriever();
+
+ private EmptyMetricRetriever()
+ {
+ }
+
+ public static EmptyMetricRetriever Instance
+ {
+ get { return _instance; }
+ }
+
+ public Task GetMetricsAsync(string resourceId, string filterString, IEnumerable definitions, string invocationId)
+ {
+ MetricFilter filter = MetricFilterExpressionParser.Parse(filterString);
+
+ return Task.Factory.StartNew(() => new MetricListResponse()
+ {
+ RequestId = invocationId,
+ StatusCode = HttpStatusCode.OK,
+ MetricCollection = new MetricCollection()
+ {
+ Value = definitions == null ? new List() : definitions.Select(d => new Metric()
+ {
+ Name = d.Name,
+ Unit = d.Unit,
+ ResourceId = resourceId,
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ TimeGrain = filter.TimeGrain,
+ MetricValues = new List(),
+ Properties = new Dictionary()
+ }).ToList()
+ }
+ });
+ }
+ }
+}
diff --git a/src/Insights/Customizations/Shoebox/IMetricRetriever.cs b/src/Insights/Customizations/Shoebox/IMetricRetriever.cs
new file mode 100644
index 000000000000..b79dc0dd98d3
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/IMetricRetriever.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Azure.Insights.Models;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// Generic interface for retrieving various types of metrics
+ ///
+ internal interface IMetricRetriever
+ {
+ Task GetMetricsAsync(string resourceId, string filterString, IEnumerable definitions, string invocationId);
+ }
+}
diff --git a/src/Insights/Customizations/Shoebox/ProxyMetricRetriever.cs b/src/Insights/Customizations/Shoebox/ProxyMetricRetriever.cs
new file mode 100644
index 000000000000..0f14104d916b
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/ProxyMetricRetriever.cs
@@ -0,0 +1,30 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Insights.Models;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// Metric retriever for delivering proxy metrics by calling RP via REST
+ /// ProxyMetricRetriever supports dimensions (if the RP supports them)
+ ///
+ internal class ProxyMetricRetriever : IMetricRetriever
+ {
+ private readonly MetricOperations metricOperations;
+
+ public ProxyMetricRetriever(MetricOperations operations)
+ {
+ this.metricOperations = operations;
+ }
+
+ public Task GetMetricsAsync(string resourceId, string filterString, IEnumerable definitions, string invocationId)
+ {
+ return this.metricOperations.GetMetricsInternalAsync(resourceId, filterString, CancellationToken.None);
+ }
+ }
+}
diff --git a/src/Insights/Customizations/Shoebox/SasMetricRetriever.cs b/src/Insights/Customizations/Shoebox/SasMetricRetriever.cs
new file mode 100644
index 000000000000..1f3b3ad51123
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/SasMetricRetriever.cs
@@ -0,0 +1,235 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Hyak.Common;
+using Microsoft.Azure.Insights.Models;
+using Microsoft.WindowsAzure.Storage.Table;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// Base metric retriever for SAS-based metrics
+ ///
+ internal abstract class SasMetricRetriever : IMetricRetriever
+ {
+ public async Task GetMetricsAsync(string resourceId, string filterString, IEnumerable definitions, string invocationId)
+ {
+ MetricFilter filter = MetricFilterExpressionParser.Parse(filterString);
+
+ // Group definitions by location so we can make one request to each location
+ Dictionary groups =
+ definitions.GroupBy(d => d.MetricAvailabilities.First(a => a.TimeGrain == filter.TimeGrain),
+ new SasMetricRetriever.AvailabilityComparer()).ToDictionary(g => g.Key, g => new MetricFilter()
+ {
+ TimeGrain = filter.TimeGrain,
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ DimensionFilters = g.Select(d =>
+ filter.DimensionFilters.FirstOrDefault(df => string.Equals(df.Name, d.Name.Value, StringComparison.OrdinalIgnoreCase))
+ ?? new MetricDimension() {Name = d.Name.Value})
+ });
+
+ // Verify all groups represent shoebox metrics
+ if (groups.Any(g => g.Key.Location == null))
+ {
+ throw new ArgumentException("All definitions provided to ShoeboxMetricRetriever must include location information.", "definitions");
+ }
+
+ // Get Metrics from each location (group)
+ IEnumerable> locationTasks = groups.Select(g => this.GetMetricsInternalAsync(g.Value, g.Key.Location, invocationId));
+
+ // Aggregate metrics from all groups
+ MetricListResponse[] results = (await Task.Factory.ContinueWhenAll(locationTasks.ToArray(), tasks => tasks.Select(t => t.Result))).ToArray();
+ IEnumerable metrics = results.Aggregate>(
+ new List(), (list, response) => list.Union(response.MetricCollection.Value));
+
+ // Return aggregated results (the MetricOperations class will fill in additional info from the MetricDefinitions)
+ return new MetricListResponse()
+ {
+ RequestId = invocationId,
+ StatusCode = HttpStatusCode.OK,
+ MetricCollection = new MetricCollection()
+ {
+ Value = metrics.ToList()
+ }
+ };
+ }
+
+ ///
+ /// Retrieves the metric values from the shoebox
+ ///
+ /// The $filter query string
+ /// The MetricLocation object
+ /// The invocation id
+ /// The MetricValueListResponse
+ // Note: Does not populate Metric fields unrelated to query (i.e. "display name", resourceUri, and properties)
+ internal MetricListResponse GetMetricsInternal(MetricFilter filter, MetricLocation location, string invocationId)
+ {
+ return GetMetricsInternalAsync(filter, location, invocationId).Result;
+ }
+
+ ///
+ /// Retrieves the metric values from the shoebox
+ ///
+ /// The $filter query string
+ /// The MetricLocation object
+ /// The invocation id
+ /// The MetricValueListResponse
+ // Note: Does not populate Metric fields unrelated to query (i.e. "display name", resourceUri, and properties)
+ internal abstract Task GetMetricsInternalAsync(MetricFilter filter, MetricLocation location, string invocationId);
+
+ protected static async Task> GetEntitiesAsync(CloudTable table, TableQuery query, string invocationId, int maxBatchSize = 0) where TEntity : ITableEntity, new()
+ {
+ string traceRequestId;
+ List results = new List();
+
+ try
+ {
+ // If 0 or less then there is no max value
+ maxBatchSize = maxBatchSize <= 0 ? int.MaxValue : maxBatchSize;
+ TableContinuationToken continuationToken = null;
+ do
+ {
+ continuationToken = null;
+ TableQuerySegment resultSegment = null;
+
+ traceRequestId = Guid.NewGuid().ToString();
+
+ TableOperationContextLogger operationContextLogger = new TableOperationContextLogger(
+ accountName: table.ServiceClient.Credentials.AccountName,
+ resourceUri: table.Name,
+ operationName: "ExecuteQuerySegmentedAsync",
+ requestId: invocationId);
+
+ resultSegment = await table.ExecuteQuerySegmentedAsync(query, continuationToken, requestOptions: new TableRequestOptions(), operationContext: operationContextLogger.OperationContext);
+
+ if (resultSegment != null)
+ {
+ var count = resultSegment.Results == null ? 0 : resultSegment.Results.Count;
+ var recordsToInclude = Math.Min(maxBatchSize - results.Count, count);
+
+ if (resultSegment.Results != null)
+ {
+ results.AddRange(resultSegment.Results.Take(recordsToInclude));
+ }
+
+ continuationToken = resultSegment.ContinuationToken;
+ }
+ }
+ while (continuationToken != null && results.Count < maxBatchSize);
+ }
+ catch (Exception ex)
+ {
+ TracingAdapter.Error(invocationId, ex);
+ throw ex.GetBaseException();
+ }
+
+ return results;
+ }
+
+ protected static MetricValue GetMetricValueFromEntity(DynamicTableEntity entity, string metricName)
+ {
+ // Get the key (metric name) from the entity properties dictionary to retrieve the correctly-cased key since the key lookup is case sensitive
+ string key = entity.Properties.Keys.FirstOrDefault(k => string.Equals(k, metricName, StringComparison.OrdinalIgnoreCase));
+
+ // metric name does not exist in keys
+ if (key == null)
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to retrieve metric {0}. Metric name does not exist", metricName), "metricName");
+ }
+
+ MetricValue value = new MetricValue();
+ DateTime created;
+
+ if (!DateTime.TryParseExact(entity.PartitionKey, "yyyyMMddTHHmm", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out created))
+ {
+ // trace failure as best possible
+ TracingAdapter.Information("Failed to parse partition key (date) {0}", entity.PartitionKey);
+ }
+
+ // dateTime is not parsing and correctly setting the kind so we force it here
+ value.Timestamp = DateTime.SpecifyKind(created, DateTimeKind.Utc);
+ value.Count = 1;
+
+ // convert value to double
+ switch (entity.Properties[key].PropertyType)
+ {
+ case EdmType.Double:
+ SetAllMetricValues(value, entity.Properties[key].DoubleValue ?? 0);
+ break;
+ case EdmType.Int32:
+ SetAllMetricValues(value, entity.Properties[key].Int32Value ?? 0);
+ break;
+ case EdmType.Int64:
+ SetAllMetricValues(value, entity.Properties[key].Int64Value ?? 0);
+ break;
+ default:
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Table value for column {0} is not a numeric type", metricName));
+ }
+
+ return value;
+ }
+
+ private static void SetAllMetricValues(MetricValue metric, double value)
+ {
+ metric.Average = value;
+ metric.Last = value;
+ metric.Maximum = value;
+ metric.Minimum = value;
+ metric.Total = value;
+ }
+
+ protected static IEnumerable CollectResults(IEnumerable>> tasks)
+ {
+ return tasks.Aggregate((IEnumerable)new T[0], (list, t) => list.Union(t.Result));
+ }
+
+ protected static async Task> CollectResultsAsync(IEnumerable>> tasks)
+ {
+ return await Task.Factory.ContinueWhenAll(tasks.ToArray(), (t) => CollectResults(t)).ConfigureAwait(false);
+ }
+
+ // Creates a TableQuery object which filters entities to a particular partition key and the given row key range
+ protected static TableQuery GenerateMetricQuery(string partitionKey, string startRowKey, string endRowKey)
+ {
+ string partitionFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey);
+ string rowStartFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, startRowKey);
+ string rowEndFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThan, endRowKey);
+
+ return new TableQuery()
+ {
+ FilterString = TableQuery.CombineFilters(partitionFilter, TableOperators.And, TableQuery.CombineFilters(rowStartFilter, TableOperators.And, rowEndFilter))
+ };
+ }
+
+ private class AvailabilityComparer : IEqualityComparer
+ {
+ public bool Equals(MetricAvailability x, MetricAvailability y)
+ {
+ if (x.Location == null && y.Location == null)
+ {
+ return true;
+ }
+
+ if (x.Location == null || y.Location == null)
+ {
+ return false;
+ }
+
+ return x.Location.TableEndpoint == y.Location.TableEndpoint;
+ }
+
+ public int GetHashCode(MetricAvailability obj)
+ {
+ return obj.Location == null ? 0 : obj.Location.TableEndpoint.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/Insights/Customizations/Shoebox/ShoeboxClient.cs b/src/Insights/Customizations/Shoebox/ShoeboxClient.cs
index 1b6b1a525d8e..ef8274925cbb 100644
--- a/src/Insights/Customizations/Shoebox/ShoeboxClient.cs
+++ b/src/Insights/Customizations/Shoebox/ShoeboxClient.cs
@@ -17,12 +17,16 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Hyak.Common;
using Microsoft.Azure.Insights.Models;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Table;
namespace Microsoft.Azure.Insights
{
+ ///
+ /// Thick client component for retrieving shoebox metrics
+ ///
internal static class ShoeboxClient
{
internal static int MaxParallelRequestsByName = 4;
@@ -53,7 +57,22 @@ internal static async Task GetMetricsInternalAsync(MetricFil
// TODO [davmc]: ShoeboxClient doesn't support dimensions
if (filter.DimensionFilters != null && filter.DimensionFilters.Any(df => df.Dimensions != null))
{
- throw new ArgumentException("Shoebox client does not support dimensions", "filter");
+ if (TracingAdapter.IsEnabled)
+ {
+ TracingAdapter.Information("InvocationId: {0}. ShoeboxClient encountered metrics with dimensions specified. These will be ignored.", invocationId);
+ }
+
+ // Remove dimensions from filter (The MetricFilter class has strict mutation rules used in parsing so the best way to modify it is to create a new one)
+ filter = new MetricFilter()
+ {
+ TimeGrain = filter.TimeGrain,
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ DimensionFilters = filter.DimensionFilters.Select(df => new MetricDimension()
+ {
+ Name = df.Name
+ })
+ };
}
// If metrics are requested by name, get those metrics specifically, unless too many are requested.
diff --git a/src/Insights/Customizations/Shoebox/ShoeboxHelper.cs b/src/Insights/Customizations/Shoebox/ShoeboxHelper.cs
index dd6301b4538d..178f264b15e5 100644
--- a/src/Insights/Customizations/Shoebox/ShoeboxHelper.cs
+++ b/src/Insights/Customizations/Shoebox/ShoeboxHelper.cs
@@ -22,6 +22,9 @@
namespace Microsoft.Azure.Insights
{
+ ///
+ /// Helper class for shoebox operations
+ ///
internal static class ShoeboxHelper
{
private const int KeyLimit = 432;
diff --git a/src/Insights/Customizations/Shoebox/ShoeboxMetricRetriever.cs b/src/Insights/Customizations/Shoebox/ShoeboxMetricRetriever.cs
new file mode 100644
index 000000000000..e2d38f4fd281
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/ShoeboxMetricRetriever.cs
@@ -0,0 +1,22 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System.Threading.Tasks;
+using Microsoft.Azure.Insights.Models;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// Metric retriever for getting metrics in "shoebox" storage accounts using provided SAS keys
+ /// ShoeboxMetricRetriever ignores dimensions
+ ///
+ /// TODO: Refactor shoebox client to inherit table operations and diminsions support from SasMetricRetriever
+ internal class ShoeboxMetricRetriever : SasMetricRetriever
+ {
+ internal override async Task GetMetricsInternalAsync(MetricFilter filter, MetricLocation location, string invocationId)
+ {
+ return await ShoeboxClient.GetMetricsInternalAsync(filter, location, invocationId);
+ }
+ }
+}
diff --git a/src/Insights/Customizations/Shoebox/StorageConstants.cs b/src/Insights/Customizations/Shoebox/StorageConstants.cs
new file mode 100644
index 000000000000..82a4574275f1
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/StorageConstants.cs
@@ -0,0 +1,75 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// Constants used for retrieving Storage metrics
+ ///
+ // TODO: Update StorageMetricRetriever to determine dimension name and capacity metrics from definitons
+ internal static class StorageConstants
+ {
+ // Timegrain constants
+ internal static readonly TimeSpan PT1M = TimeSpan.FromMinutes(1);
+ internal static readonly TimeSpan PT1H = TimeSpan.FromHours(1);
+ internal static readonly TimeSpan P1D = TimeSpan.FromDays(1);
+
+ internal static readonly string CapacityMetricsTableName = "$MetricsCapacityBlob";
+
+ // supported dimensions
+ internal static class Dimensions
+ {
+ // Data type dimension (not supported)
+ public static readonly string TransactionDataTypeDimensionUserValue = "user";
+
+ // Operation name dimension
+ public static readonly string ApiDimensionName = "apiName";
+ public static readonly string ApiDimensionAggregateValue = "All";
+ }
+
+ internal static class MetricNames
+ {
+ public static readonly string Capacity = "Capacity";
+ public static readonly string ContainerCount = "ContainerCount";
+ public static readonly string ObjectCount = "ObjectCount";
+
+ public static readonly HashSet CapacityMetrics = new HashSet(new[] { Capacity, ContainerCount, ObjectCount });
+
+ public static bool IsCapacityMetric(string metricName)
+ {
+ return CapacityMetrics.Contains(metricName, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ // Table name funtions
+ internal static bool IsCapacityMetricsTable(string tableName)
+ {
+ return CapacityMetricsTableName.Equals(tableName);
+ }
+
+ internal static bool IsTransactionMetricsTable(string tableName)
+ {
+ return tableName.IndexOf("Transaction", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
+ internal static bool IsBlobMetricsTable(string tableName)
+ {
+ return tableName.IndexOf("Blob", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
+ internal static bool IsTableMetricsTable(string tableName)
+ {
+ return tableName.IndexOf("Table", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+
+ internal static bool IsQueueMetricsTable(string tableName)
+ {
+ return tableName.IndexOf("Queue", StringComparison.OrdinalIgnoreCase) >= 0;
+ }
+ }
+}
diff --git a/src/Insights/Customizations/Shoebox/StorageMetricRetriever.cs b/src/Insights/Customizations/Shoebox/StorageMetricRetriever.cs
new file mode 100644
index 000000000000..83ef17e3125a
--- /dev/null
+++ b/src/Insights/Customizations/Shoebox/StorageMetricRetriever.cs
@@ -0,0 +1,306 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.Azure.Insights.Models;
+using Microsoft.WindowsAzure.Storage.Auth;
+using Microsoft.WindowsAzure.Storage.Table;
+
+namespace Microsoft.Azure.Insights.Customizations.Shoebox
+{
+ ///
+ /// Metric retriever for getting Storage metrics directly from Storage
+ ///
+ internal class StorageMetricRetriever : SasMetricRetriever
+ {
+ // Calling this based on a grouping (in SasMetricRetriever) should guarantee that it will have metric names specified (cannot have empty group)
+ internal override async Task GetMetricsInternalAsync(MetricFilter filter, MetricLocation location, string invocationId)
+ {
+ if (filter == null)
+ {
+ throw new ArgumentNullException("filter");
+ }
+
+ if (location == null)
+ {
+ throw new ArgumentNullException("location");
+ }
+
+ // This is called based on the definitions no the dimension portion of the filter should never be null or empty
+ if (filter.DimensionFilters == null || !filter.DimensionFilters.Any())
+ {
+ throw new ArgumentNullException("filter.DimensionFilters");
+ }
+
+ // Separate out capacity metrics and transaction metrics into two groups
+ IEnumerable capacityMetrics = filter.DimensionFilters.Select(df => df.Name).Where(StorageConstants.MetricNames.IsCapacityMetric);
+ IEnumerable transactionMetrics = filter.DimensionFilters.Select(df => df.Name).Where(n => !StorageConstants.MetricNames.IsCapacityMetric(n));
+
+ List>> queryTasks = new List>>();
+
+ // Add task to get capacity metrics (if any)
+ if (capacityMetrics.Any())
+ {
+ MetricTableInfo capacityTableInfo = location.TableInfo.FirstOrDefault(ti => StorageConstants.IsCapacityMetricsTable(ti.TableName));
+ if (capacityTableInfo == null)
+ {
+ throw new InvalidOperationException("Definitions for capacity metrics must contain table info for capacity metrics table");
+ }
+
+ queryTasks.Add(GetCapacityMetricsAsync(filter, GetTableReference(location, capacityTableInfo), capacityMetrics, invocationId));
+ }
+
+ // Add tasks to get transaction metrics (if any)
+ if (transactionMetrics.Any())
+ {
+ IEnumerable transactionTableInfos = location.TableInfo.Where(ti => !StorageConstants.IsCapacityMetricsTable(ti.TableName));
+ if (!transactionTableInfos.Any())
+ {
+ throw new InvalidOperationException("Definitions for transaction metrics must contain table info for transaction metrics table");
+ }
+
+ queryTasks.AddRange(transactionTableInfos
+ .Select(info => GetTransactionMetricsAsync(filter, GetTableReference(location, info), transactionMetrics, invocationId)));
+ }
+
+ // Collect results and wrap
+ return new MetricListResponse()
+ {
+ RequestId = invocationId,
+ StatusCode = HttpStatusCode.OK,
+ MetricCollection = new MetricCollection()
+ {
+ Value = (await CollectResultsAsync(queryTasks)).ToList()
+ }
+ };
+ }
+
+ private static CloudTable GetTableReference(MetricLocation location, MetricTableInfo tableInfo)
+ {
+ return new CloudTableClient(new Uri(location.TableEndpoint), new StorageCredentials(tableInfo.SasToken)).GetTableReference(tableInfo.TableName);
+ }
+
+ private static async Task> GetCapacityMetricsAsync(MetricFilter filter, CloudTable table, IEnumerable metricNames, string invocationId)
+ {
+ IEnumerable entities = await SasMetricRetriever.GetEntitiesAsync(table, GetCapacityQuery(filter), invocationId);
+
+ return metricNames.Select(n => new Metric()
+ {
+ Name = new LocalizableString()
+ {
+ Value = n,
+ LocalizedValue = n
+ },
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ TimeGrain = filter.TimeGrain,
+ Properties = new Dictionary(),
+ MetricValues = entities.Select(entity => GetMetricValueFromEntity(entity, n)).ToList()
+ });
+ }
+
+ private static async Task> GetTransactionMetricsAsync(MetricFilter filter, CloudTable table, IEnumerable metricNames, string invocationId)
+ {
+ // Get relevant dimensions
+ IEnumerable metricDimensions = filter.DimensionFilters.Where(df => metricNames.Contains(df.Name));
+
+ // Get appropriate entities from table
+ IEnumerable entities = await GetEntitiesAsync(table, GetTransactionQuery(filter, GetOperationNameForQuery(
+ metricDimensions,
+ StorageConstants.Dimensions.ApiDimensionName,
+ StorageConstants.Dimensions.ApiDimensionAggregateValue)), invocationId);
+
+ // Construct Metrics and accumulate results
+ return metricDimensions
+ .Select(md => CreateTransactionMetric(filter, md, entities))
+ .Aggregate,IEnumerable>(new Metric[0], (a, b) => a.Union(b));
+ }
+
+ private static string GetOperationNameForQuery(IEnumerable dimensions, string dimensionName, string dimensionAggregateValue)
+ {
+ string operationName = null;
+
+ // Long story short: look at all the dimension values for all the metrics (dimensions) for the dimensionName,
+ // if any two dimensionValues are specified, we will get all the dimensions (rows) and filter afterward (return null),
+ // if no dimension values are specified anywhere, only get the "Aggregate" row (specified by dimensionAggregateValue),
+ // if exactly one dimension value is specified across all the metrics, get only that row
+ foreach (string value in
+ from d in dimensions
+ where d.Dimensions != null
+ select d.Dimensions.FirstOrDefault(fd => string.Equals(fd.Name, dimensionName))
+ into filterDimension
+ where filterDimension != null
+ from value in filterDimension.Values
+ select value)
+ {
+ if (operationName == null)
+ {
+ operationName = value;
+ }
+ else if (!string.Equals(operationName, value, StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+ }
+
+ return operationName ?? dimensionAggregateValue;
+ }
+
+ private static IEnumerable CreateTransactionMetric(MetricFilter filter, MetricDimension metricDimension, IEnumerable entities)
+ {
+ List metrics = new List();
+
+ // Get supported metric dimension
+ FilterDimension filterDimension = metricDimension.Dimensions == null
+ ? null
+ : metricDimension.Dimensions.FirstOrDefault(fd =>
+ string.Equals(fd.Name, StorageConstants.Dimensions.ApiDimensionName, StringComparison.OrdinalIgnoreCase));
+
+ // no dimensions (or no supported dimensions) means only get aggregate values (user;All)
+ if (filterDimension == null)
+ {
+ metrics.Add(new Metric()
+ {
+ Name = new LocalizableString()
+ {
+ Value = metricDimension.Name,
+ LocalizedValue = metricDimension.Name
+ },
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ TimeGrain = filter.TimeGrain,
+ Properties = new Dictionary(),
+ MetricValues = entities
+ .Where(e => string.Equals(e.RowKey, GetTransactionRowKey(StorageConstants.Dimensions.ApiDimensionAggregateValue), StringComparison.OrdinalIgnoreCase))
+ .Select(e => GetMetricValueFromEntity(e, metricDimension.Name)).ToList()
+ });
+ }
+
+ // Dimension specified, get samples with requested dimension value
+ else
+ {
+ // This is the function for filtering based on dimension value (row key)
+ Func, bool> groupFilter;
+
+ // dimension specified, but no values means get all and group by dimension value (row key)
+ if (filterDimension.Values == null || !filterDimension.Values.Any())
+ {
+ // select all groups, but leave off aggregate. Each group becomes one metric
+ groupFilter = (entityGroup) =>
+ !string.Equals(entityGroup.Key, GetTransactionRowKey(StorageConstants.Dimensions.ApiDimensionName), StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ // select only groups specified by dimension values
+ groupFilter = (entityGroup) => filterDimension.Values.Select(GetTransactionRowKey).Contains(entityGroup.Key);
+ }
+
+ // Construct and add the metrics to the collection to return
+ metrics.AddRange(entities
+ .GroupBy(e => e.RowKey)
+ .Where(groupFilter)
+ .Select(entityGroup => new Metric()
+ {
+ Name = new LocalizableString()
+ {
+ Value = metricDimension.Name,
+ LocalizedValue = metricDimension.Name
+ },
+ StartTime = filter.StartTime,
+ EndTime = filter.EndTime,
+ TimeGrain = filter.TimeGrain,
+ Properties = new Dictionary(),
+ MetricValues = entityGroup.Select(e => GetMetricValueFromEntity(e, metricDimension.Name)).ToList()
+ }));
+ }
+
+ // return only values specified
+ return metrics;
+ }
+
+ private static string GetTransactionRowKey(string dimensionValue)
+ {
+ return string.Concat(StorageConstants.Dimensions.TransactionDataTypeDimensionUserValue, ";", dimensionValue);
+ }
+
+ // Copied from Microsoft.WindowsAzure.Management.Monitoring.ResourceProviders.Storage.Rest.V2011_12.MetricBaseController
+ private static TableQuery GetTransactionQuery(MetricFilter filter, string operationName = null)
+ {
+ // storage transaction queries are only supported for 1 hr and 1 min timegrains
+ if (filter.TimeGrain != StorageConstants.PT1H && filter.TimeGrain != StorageConstants.PT1M)
+ {
+ return null;
+ }
+
+ DateTime partitionKeyStartTime = filter.StartTime;
+ DateTime partitionKeyEndTime = filter.EndTime;
+
+ // start by assuming that we are querying for hr metrics
+ // since the timestamp field does not represent the actual sample period the only time value represented is the partitionkey
+ // this is basically truncated to the hr with the min zeroed out.
+ string startKey = partitionKeyStartTime.ToString("yyyyMMddTHH00");
+ string endKey = partitionKeyEndTime.ToString("yyyyMMddTHH00");
+
+ // if this is actually a minute metric request correct the partition keys and table name format
+ if (filter.TimeGrain == TimeSpan.FromMinutes(1))
+ {
+ startKey = partitionKeyStartTime.ToString("yyyyMMddTHHmm");
+ endKey = partitionKeyEndTime.ToString("yyyyMMddTHHmm");
+ }
+
+ string rowKey = "user;";
+ string rowComparison = QueryComparisons.Equal;
+
+ // If requesting a particular operation, get only that one (dimension value), otherwise get all
+ if (operationName == null)
+ {
+ rowComparison = QueryComparisons.GreaterThanOrEqual;
+ }
+ else
+ {
+ rowKey += operationName;
+ }
+
+ var filter1 = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.GreaterThanOrEqual, startKey);
+ var filter2 = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.LessThanOrEqual, endKey);
+ var filter3 = TableQuery.GenerateFilterCondition("RowKey", rowComparison, rowKey);
+
+ var tableQuery = new TableQuery().Where(
+ TableQuery.CombineFilters(TableQuery.CombineFilters(filter1, TableOperators.And, filter2), TableOperators.And, filter3));
+
+ return tableQuery;
+ }
+
+ // Copied from Microsoft.WindowsAzure.Management.Monitoring.ResourceProviders.Storage.Rest.V2011_12.MetricBaseController
+ private static TableQuery GetCapacityQuery(MetricFilter filter)
+ {
+ // capacity only applies for blob service and only for a timegrain of 1 day
+ if (filter.TimeGrain != TimeSpan.FromDays(1))
+ {
+ return null;
+ }
+
+ // since the timestamp field does not represent the actual sample period the only time vaule represented is the partitionkey
+ // this is basically truncated to the hr with the min zeroed out.
+ DateTime partitionKeyStartTime = filter.StartTime;
+ DateTime partitionKeyEndTime = filter.EndTime;
+
+ string startKey = partitionKeyStartTime.ToString("yyyyMMddTHH00");
+ string endKey = partitionKeyEndTime.ToString("yyyyMMddTHH00");
+
+ var filter1 = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.GreaterThanOrEqual, startKey);
+ var filter2 = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.LessThanOrEqual, endKey);
+ var filter3 = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, "data");
+
+ var tableQuery = new TableQuery()
+ .Where(TableQuery.CombineFilters(TableQuery.CombineFilters(filter1, TableOperators.And, filter2), TableOperators.And, filter3));
+
+ return tableQuery;
+ }
+ }
+}
diff --git a/src/Insights/Insights.csproj b/src/Insights/Insights.csproj
index 1eacb6b8f81c..86cfe489b121 100644
--- a/src/Insights/Insights.csproj
+++ b/src/Insights/Insights.csproj
@@ -24,15 +24,22 @@
+
+
+
+
+
+
+