Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metrics APIs in SDK 2.6.0-beta3: Tutorial & Examples #774

Closed
macrogreg opened this issue Apr 15, 2018 · 14 comments
Closed

Metrics APIs in SDK 2.6.0-beta3: Tutorial & Examples #774

macrogreg opened this issue Apr 15, 2018 · 14 comments

Comments

@macrogreg
Copy link
Contributor

macrogreg commented Apr 15, 2018

(See also: Metrics APIs in SDK 2.6.0-beta3: Pre-aggregating events-to-metrics auto-extraction: Tutorial #784)

// *
// * This file contains usage examples for the new metrics aggregation APIs in Application
// * Insights SDK 2.6.0-beta3.
// * The file is split into several sections. We begin with the simplest use case: You have a
// * simple application with low volumes (less 50 requests per second) and you just want to
// * track metrics. Later we move on to progressively more advanced examples: High-throughput
// * applications, metric configurations, unit testing and others.
// *

namespace User.Namespace.Example01
{
    using System;
    using Microsoft.ApplicationInsights;

    using TraceSeveretyLevel = Microsoft.ApplicationInsights.DataContracts.SeverityLevel;

    /// <summary>
    /// Most simple cases are one-liners.
    /// All telemetry-related code is expected to import Microsoft.ApplicationInsights.
    /// Basic metric tracking is possible without importing an additional namespace.
    /// </summary>
    public class Sample01
    {
        /// <summary />
        public static void Exec()
        {
            // *** SENDING METRICS ***

            // Recall how you send custom telemetry with Application Insights in other cases,
            // e.g. Events.
            // For example, the following will result in an EventTelemetry object to be sent to
            // the cloud right away:

            TelemetryClient client = new TelemetryClient();
            client.TrackEvent("SomethingInterestingHappened");

            // Metrics work very similar. However, the value is not sent right away.
            // It is aggregated with other values for the same metric time series, and the
            // resulting summary (aka "aggregate") is sent automatically every minute.
            // To mark this difference, we use a pattern that is similar, but different from the
            // established TrackXxx(..) pattern that sends telemetry right away:

            client.GetMetric("CowsSold").TrackValue(42);

            // *** HIGH-THROUGHPUT USAGE: CACHING A METRIC REFERENCE ***

            // In some cases metric values are observed very frequently. For example, a
            // high -throughput service that processes 500 requests/second may want to emit 20
            // telemetry metrics for each request. This means tracking 10,000 values per second.
            // Our goal is to support up to 100,000 tracked values per second with a "reasonable"
            // overhead. In such high-throughput scenarios users may need to help the SDK by
            // avoiding some lookups. 

            // For example, in this case, the above statement performed a lookup for a handle for
            // the metric "CowsSold" and then tracked an observed value 42. Instead, the handle
            // may be cached for pultiple track invocations:

            Metric cowsSold = client.GetMetric("CowsSold");
            //...
            cowsSold.TrackValue(18);
            //...
            cowsSold.TrackValue(138);
            //...

            // *** MULTI-DIMENSIONAL METRICS ***

            // The above example shows a zero-dimensional metric.
            // Metrics can also be multi-dimensional. We are supporting up to 10 dimensions.
            // Here is an example for a one-dimensional metric:

            Metric animalsSold = client.GetMetric("AnimalsSold", "Species");

            animalsSold.TryTrackValue(42, "Pigs");
            animalsSold.TryTrackValue(24, "Horses");

            // The values for Pigs and Horses will be aggregated separately from each other and
            // will result in two distinct aggregates.


            // *** DIMENSION CAPPING and TIME-SERIES CAPPING ***

            // To prevent the telemetry sub-system from accidentally using up your ressources,
            // you can control the maximum number of data series per metric. The default limits
            // are no more than 1000 total data series per metric, and no more than 100 different
            // values per dimension. We discuss later how to change the defaults.

            // *** TrackValue(..) vs. TryTrackValue(..) ***

            // In the context of dimension and time series capping we use a common .Net pattern:
            // TryXxx(..) to make sure that the limits are observed. If the limits are already
            // reached, Metric.TryTrackValue(..) will return False and the value will not be
            // tracked. Otherwise it will return True. This is particularly useful if the data
            // for a metric originates from user input, e.g. a file:

            Tuple<int, string> countAndSpecies = ReadSpeciesFromUserInput();
            int count = countAndSpecies.Item1;
            string species = countAndSpecies.Item2;

            if (! animalsSold.TryTrackValue(count, species))
            {
                client.TrackTrace(
                            $"Data series cap or dimension cap was reached for metric {animalsSold.Identifier.MetricId}.",
                            TraceSeveretyLevel.Error);
            }

            // You can inspect a metric object to reason about its current state. For example:
            int currentNumberOfSpecies = animalsSold.GetDimensionValues(1).Count;

            // Note that we did not provide a TrackValue(..) equivalent for every
            // TryTrackValue(..) method. 
            // This is because if we did, you would need to wrap every metric tracking invocation
            // into a try-catch block. We do not believe that telemetry code should require so
            // much effort. Just use TryTrackValue(..) and decide yourself whether you want to
            // process the return value or just accept a possibility that your value may not be
            // tracked. Either way, you can rely on the metric telemetry sub -system that it will
            // not throw an exception and break your business logic.

            // Notably, there is one TrackValue(..) method - for zero dimensional metrics.
            // This is becasue they always have exactly one time series and a cap can never be
            // reached.
        }

        private static void ResetDataStructure()
        {
            // Do stuff
        }

        private static Tuple<int, string> ReadSpeciesFromUserInput()
        {
            return Tuple.Create(18, "Cows");
        }

        private static int AddItemsToDataStructure()
        {
            // Do stuff
            return 5;
        }
    }
}
// ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
namespace User.Namespace.Example02
{
    using System;

    using Microsoft.ApplicationInsights;
    using Microsoft.ApplicationInsights.Metrics;

    using TraceSeveretyLevel = Microsoft.ApplicationInsights.DataContracts.SeverityLevel;

    /// <summary>
    /// Importing the <c>Microsoft.ApplicationInsights.Metrics</c> namespace supports some more
    /// interesting use cases. For example, these include:
    ///  - Configuring a metric
    ///  - Working directly with the MetricManager
    ///  
    /// In this example we cover working with MetricSeries.
    /// </summary>
    public class Sample02
    {
        /// <summary />
        public static void Exec()
        {
            // *** ACCESSING METRIC DATA SERIES ***

            // Recall that metrics can be multidimensional. For example, assume that we want to
            // track the number of books sold by Genre and by Language.

            TelemetryClient client = new TelemetryClient();
            Metric booksSold = client.GetMetric("BooksSold", "Genre", "Language");
            booksSold.TryTrackValue(10, "Science Fiction", "English");
            booksSold.TryTrackValue(15, "Historic Novels", "English");
            booksSold.TryTrackValue(20, "Epic Tragedy", "Russian");

            // Recall from a previous example that each of the above TryTrackValue(..) statements
            // will contribute to a different data series. If you repeatedly use the same dimension
            // values, then the system will look up and use an existing series:

            booksSold.TryTrackValue(8, "Science Fiction", "English");  // 18 Sci Fi books in English

            // If you use certain data series frequently you can avoid this lookup by keeping a
            // reference to them:

            MetricSeries epicTragedyInRussianSold;
            booksSold.TryGetDataSeries(out epicTragedyInRussianSold, "Epic Tragedy", "Russian");

            epicTragedyInRussianSold.TrackValue(6); // Now we have 26 Epic Tragedies in Russian
            epicTragedyInRussianSold.TrackValue(5); // Now we have 31 Epic Tragedies in Russian

            // Notice the "Try" in TryGetDataSeries(..). Recall the previous example where we 
            // explained the TryTrackValue(..) pattern. The same reasoning applies here.

            // So Metric is a container for one or more data series.
            // The actual data belongs a specific MetricSeries object and the Metric object is a
            // grouping of one or more series.

            // A zero-dimensional metric has exactly one metric data series:
            Metric cowsSold = client.GetMetric("CowsSold");
            
            MetricSeries cowsSoldValues;
            cowsSold.TryGetDataSeries(out cowsSoldValues);
            cowsSoldValues.TrackValue(25);

            AssertAreEqual(0, cowsSold.Identifier.DimensionsCount);
            AssertAreEqual(1, cowsSold.SeriesCount);

            // For zero-dimensional metrics you can also get the series in a single line:
            MetricSeries cowsSoldValues2 = cowsSold.GetAllSeries()[0].Value;

            cowsSoldValues2.TrackValue(18); // Now we have 25 + 18 = 43 cows.
            AssertAreSame(cowsSoldValues, cowsSoldValues2);

            // Note, however, that you cannot play this trick with multi-dimensional series, 
            // because GetAllSeries() does not provide any guarantees about the ordering of the 
            // series it returns.

            // Multi-dimensional metrics can have more than one data series:
            MetricSeries unspecifiedBooksSold, cookbookInGermanSold;
            booksSold.TryGetDataSeries(out unspecifiedBooksSold);
            booksSold.TryGetDataSeries(out cookbookInGermanSold, "Cookbook", "German");

            // You can get the "special" zero-dimensional series from every metric, regardless of 
            // now many dimensions it has. But if you specify any dimension values at all, you 
            // must specify the correct number, otherwise an exception is thrown.

            try
            {
                MetricSeries epicTragediesSold;
                booksSold.TryGetDataSeries(out epicTragediesSold, "Epic Tragedy");
            }
            catch (ArgumentException)
            {
                client.TrackTrace(
                                $"This error will always happen because '{nameof(booksSold)}' has 2 dimensions, but we only specified one.",
                                TraceSeveretyLevel.Error);
            }


            // *** HIGH-THROUGHPUT USAGE: CACHING A METRIC-SERIES REFERENCE ***

            // Recall from a previous example that the the most basic usage of zero-dimensional 
            // metric aggregation uses a table lookup to find the metric handle. Multi-dimensional 
            // metric usage uses a second lookup to find the right series within the metric. 
            // These lookups are very fast. However, if you are tracking 100s or 1000s of values 
            // per second, you can increase your performance by caching a metric series reference.

            // Simple usage. Uses 2 fast lookups for each tracked value:
            client.GetMetric("BooksSold", "Genre", "Language")
                  .TryTrackValue(9, "Epic Tragedy", "Russian");

            // Fast usage. Uses 2 fast lookups to initially get the series reference.
            // Then uses no lookups (and no objects allocations) for each tracked value:
            MetricSeries epicTragedyInRussianSold2;
            client.GetMetric("BooksSold", "Genre", "Language")
                  .TryGetDataSeries(out epicTragedyInRussianSold2, "Epic Tragedy", "Russian");

            epicTragedyInRussianSold2.TrackValue(10);
            epicTragedyInRussianSold2.TrackValue(11);

            // Note that the method on the MetricSeries is TrackValue and not TryTrackValue(.
            // That is becasue we already have a reference to a concrete time series.
            // Thus, we can be sure that dimension capping does not apply.

            // Can you count how many epic tragedies in Russian we sold so far?
            // It's 61.  :)


            // *** SPECIAL DIMENSION NAMES ***

            // Note that metrics do not usually respect the TelemetryContext of the 
            // TelemetryClient used to access the metric. There is a detailed discussion of the 
            // reasons and workarounds in a latter example. For now, just a clarification:

            TelemetryClient specialClient = new TelemetryClient();
            specialClient.Context.Operation.Name = "Special Operation";

            int requestSize = GetCurrentRequestSize();

            specialClient.GetMetric("Special Operation Request Size")
                         .TrackValue(requestSize);

            // Metric aggregates sent using the above statement will NOT have their 
            // Context.Operation.Name set to "Special Operation". However, you can use special 
            // dimension names in order to specify TelemetryContext values.
            // For example, when the metric aggregate resulting form the next statement is sent 
            // to the Application Insights cloud endpoint, its 'Context.Operation.Name' data field
            // will be set to "Special Operation":

            client.GetMetric("Request Size", MetricDimensionNames.TelemetryContext.Operation.Name)
                  .TryTrackValue(requestSize, "Special Operation");

            // Note: The values of this special dimension will be copied into the TelemetryContext
            // and will not be used as a 'normal' dimension. If you want to also keep an operation 
            // name dimension for normal metric exploration, you need to create a separate
            // dimension for that purpose:

            client.GetMetric("Request Size", MetricDimensionNames.TelemetryContext.Operation.Name, "Operation Name")
                  .TryTrackValue(requestSize, "Special Operation", "Special Operation");

            // In this last case, the aggregates will have a dimension "Operation Name" with the
            // value "Special Operation", and, in addition, their Context.Operation.Name will be 
            // set to "Special Operation".

            // The static class MetricDimensionNames contains a list of constants for all special 
            // dimension names.
        }

        private static void AssertAreEqual(int x, int y)
        {
            if (x != y)
            {
                throw new Exception($"Values were expected to be equal, but they are not ({x} != {y}).");
            }
        }

        private static void AssertAreSame(object x, object y)
        {
            if (! Object.ReferenceEquals(x, y))
            {
                throw new Exception($"References were expected to point to the same object, but they do not.");
            }
        }

        private static int GetCurrentRequestSize()
        {
            // Do stuff
            return 11000;
        }
    }
}
// ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
namespace User.Namespace.Example03
{
    using System;

    using Microsoft.ApplicationInsights;
    using Microsoft.ApplicationInsights.Metrics;

    using TraceSeveretyLevel = Microsoft.ApplicationInsights.DataContracts.SeverityLevel;

    /// <summary>
    /// In this example we cover configuring a metric.
    /// </summary>
    public class Sample03
    {
        /// <summary />
        public static void Exec()
        {
            // *** SEPARATION OF CONCERNS: TRACKING AND CONFIGURING METRICS ARE INDEPENDENT ***

            // Recall from an earlier example that a metric can be configured. For example, it is 
            // possible to specify the aggregation kind: i.e. what statistics will be collected 
            // from the set of tracked values. 
            // Known aggrigation kinds are Simple Measurements (min/max/count/sum), Percentiles 
            // via T-Digest, Accumulators, Distinct Counts, Gauges and varous others. In the 
            // current Beta version we support Simple Measurements only.

            // A strong architectural conviction of this Metrics SDK is that metrics tracking 
            // and metrics aggregation are distinct concepts that must be kept separate. This 
            // means that a metric is ALWAYS tracked in the same way regardless of its 
            // aggregation kind. You can change the aggregation kind of a metric in one place,
            // without changing every place in code where you tracked a value.

            TelemetryClient client = new TelemetryClient();
            Metric anyKindOfMetric = client.GetMetric("...");

            anyKindOfMetric.TrackValue(42);

            // If you want to affect the way a metric is aggregated, you need to do this in the
            // one place where the metric is initialized.

            // Example A: By defaukt, metrics are simple measurement. Here is an explicit 
            // statement to ensure that:

            Metric measurementMetric = client.GetMetric("Items Processed per Minute", MetricConfigurations.Common.Measurement());

            // Example B: A completely custom metric configuration:

            MetricConfiguration customMetricConfig = CreateCustomMetricAggregationAndConfiguration();
            Metric customAggregatedMetric = client.GetMetric("UFOs observed", customMetricConfig);

            // Example C: Use the existing Mearurement aggregation kind, but restrict the metric 
            // to only 10 value per dimension (i.e. 10 sports, 10 teams and 10 regions). Also, 
            // restrict to integer values only:

            Metric veryStrictDimCapMeasurement = client.GetMetric(
                        metricId:           "Games played",
                        dimension1Name:     "Sport",
                        dimension2Name:     "Team name",
                        dimension3Name:     "Region",
                        metricConfiguration: new MetricConfigurationForMeasurement(
                                    seriesCountLimit:        1000,
                                    valuesPerDimensionLimit: 10,
                                    seriesConfig:            new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: true)));
            
            // Then, elsewhere in code:

            measurementMetric.TrackValue(10);
            measurementMetric.TrackValue(20);
            customAggregatedMetric.TrackValue(25);
            veryStrictDimCapMeasurement.TryTrackValue(42, "Football", "Real Madrid", "Spain");

            // Note that this is an important and intentional difference to some other metric 
            // aggregation libraries that declare a strongly typed metric object class for 
            // different aggregators.

            // If you prefer not to cache the metric reference, you can simply avoid specifying
            // the metric configuration in all except the first call. However, you MUST specify 
            // a configuration when you initialize the metric for the first time, or we will 
            // assume a Measurement.
            // E.g., all three of customAggregatedMetric2, customAggregatedMetric2a and
            // customAggregatedMetric2b below have the same customMetricConfig. (In fact, they 
            // are all references to the same object.)

            Metric customAggregatedMetric2 = client.GetMetric("UFOs observed", customMetricConfig);

            Metric customAggregatedMetric2a = client.GetMetric("UFOs observed");
            Metric customAggregatedMetric2b = client.GetMetric("UFOs observed", metricConfiguration: null);

            // On contrary, metric3 and metric3a are Measurements, because no configuration was 
            // specified during the first call:

            Metric metric3 = client.GetMetric("Metric 3");
            Metric metric3a = client.GetMetric("Metric 3", metricConfiguration: null);

            // Be careful: If you specify inconsistent metric configurations, you will get an
            // exception:

            try
            {
                Metric customAggregatedMetric2c = client.GetMetric("UFOs observed", MetricConfigurations.Common.Measurement());
            }
            catch(ArgumentException)
            {
                client.TrackTrace(
                            "A Metric with the specified Id and dimension names already exists,"
                          + " but it has a configuration that is different from the specified"
                          + " configuration. You may not change configurations once a  metric"
                          + " was created for the first time. Either specify the same"
                          + " configuration every time, or specify 'null' during every"
                          + " invocation except the first one. 'Null' will match against any"
                          + " previously specified configuration when retrieving existing"
                          + " metrics, or fall back to"
                          + " MetricConfigurations.Common.Measurement() when creating new"
                          + " metrics.",
                            TraceSeveretyLevel.Error);
            }


            // *** CUSTOM METRIC CONFIGURATIONS ***

            // Above we have seen a fixed preset for metric configurations: 
            // MetricConfigurations.Common.Measurement().
            // You can provide your own implementations of IMetricSeriesConfiguration which is 
            // used by MetricConfiguration if you want to implement your own custom aggregators; 
            // that is covered elsewhere. Here, let's focus on creating your own instances of 
            // MetricConfiguration to configure more options. MetricConfiguration ctor takes some 
            // options on how to manage different series within the respective metric and an 
            // object of a class implementing IMetricSeriesConfiguration that specifies 
            // aggregation behavior for each individual series of the metric:

            Metric customConfiguredMeasurement = client.GetMetric(
                        "Custom Metric 1",
                        new MetricConfiguration(
                                    seriesCountLimit:           1000,
                                    valuesPerDimensionLimit:    100,
                                    seriesConfig:               new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false)));

            // seriesCountLimit is the max total number of series the metric can contain before 
            // TryTrackValue(..) and TryGetDataSeries(..) stop creating new data series and start 
            // returning false.
            // valuesPerDimensionLimit limits the number of distinct values per dimension in a 
            // similar manner.
            // restrictToUInt32Values can be used to force a metric to consume non-negtive integer 
            // values only. Certain non-negative-integer-only standard system metrics are stored 
            // in the cloud in an optimized, more efficient manner. Custom metrics are currently 
            // always stored as doubles.

            // In fact, the above customConfiguredMeasurement is how 
            // MetricConfigurations.Common.Measurement() is defined by default.

            // If you want to change some of the above configuration values for all metrics in 
            // your application without the need to specify a custom configuration every time, 
            // you can do so by using the MetricConfigurations.Common.SetDefaultForXxx(..) methods.
            // Note that this will only affect metrics created after the change:

            Metric someMeasurement1 = client.GetMetric("Some Measurement 1", MetricConfigurations.Common.Measurement()); 

            MetricConfigurations.Common.SetDefaultForMeasurement(
                                            new MetricConfigurationForMeasurement(
                                                        seriesCountLimit:        10000,
                                                        valuesPerDimensionLimit: 5000,
                                                        seriesConfig:            new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false)));

            Metric someMeasurement2 = client.GetMetric("Some Measurement 2", MetricConfigurations.Common.Measurement());

            // someMeasurement1 has SeriesCountLimit = 1000 and ValuesPerDimensionLimit = 100.
            // someMeasurement2 has SeriesCountLimit = 10000 and ValuesPerDimensionLimit = 5000.

            try
            {
                Metric someMeasurement1a = client.GetMetric("Some Measurement 1", MetricConfigurations.Common.Measurement());
            }
            catch(ArgumentException)
            {
                // This exception will always occur because the configuration object behind 
                // MetricConfigurations.Common.Measurement() has changed when 
                // MetricConfigurations.FutureDefaults when was modified.
            }
        }

        private static MetricConfiguration CreateCustomMetricAggregationAndConfiguration()
        {
            // In this simple example, the custom metric config is just a simple measurement that
            // has no effective dim or series capping and only accepts integer metric values.
            // However, we could implement a completely custom aggregation logic my implementing 
            // our own IMetricSeriesConfiguration.
            return new MetricConfiguration(
                            seriesCountLimit:        Int32.MaxValue,
                            valuesPerDimensionLimit: Int32.MaxValue,
                            seriesConfig:            new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: true));
        }
    }
}
// ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
namespace User.Namespace.Example04
{
    using System;
    using System.Collections.Generic;

    using Microsoft.ApplicationInsights.Metrics;
    using Microsoft.ApplicationInsights.Extensibility;
    
    /// <summary>
    /// In this example we cover working directly with the MetricManager.
    /// </summary>
    public class Sample04
    {
        /// <summary />
        public static void Exec()
        {
            // *** MANUALLY CREATING METRIC SERIES WITHOUT THE CONTEXT OF A METRIC ***

            // In previous examples we learned that a Metric is merely a grouping of one or more 
            // MetricSeries, and the actual tracking is performed by the respective MetricSeries.
            // MetricSeries are managed by a class called MetricManager. The MetricManager creates 
            // all MetricSeries objects that share a scope, and encapsulates the corresponding 
            // aggregation cycles. The default aggregation cycle takes care of sending metrics 
            // to the cloud at regular intervals (1 minute). For that, it uses a dedicated
            // managed background thread. This model is aimed at ensuring that metrics are sent 
            // regularly even in case of thread pool starvation. However, it can cost significant 
            // resources when creating too many custom metric managers (this advanced situation 
            // is discussed later).

            // The default scope for a MetricManager is an instance of the Application Insights 
            // telemetry pipeline. Other scopes are discussed in later examples.
            // Recall that although in some special circumstances users can create many instances 
            // of the Application Insights telemetry pipeline, the normal case is that there is 
            // single default pipeline per application, accessible via the static object
            // at TelemetryConfiguration.Active.

            // Expert users can choose to manage their metric series directly, rather than using 
            // a Metric container object. In that case they will obtain metric series directly 
            // from the MetricManager:

            MetricManager metrics = TelemetryConfiguration.Active.GetMetricManager();

            MetricSeries requestSize = metrics.CreateNewSeries(
                            "Example Metrics",
                            "Size of Service Resquests",
                            new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false));

            requestSize.TrackValue(256);
            requestSize.TrackValue(314);
            requestSize.TrackValue(189);

            // Note that MetricManager.CreateNewSeries(..) will ALWAYS create a new metric series. 
            // It is your responsibility to keep a reference to it so that you can access it 
            // later. If you do not want to worry about keeping that reference, just use the
            // Metric class as in the previous examples.

            // If you choose to use MetricManager directly, you can specify the dimension names
            // and values associated with a new metric series. Note how dimensions can be 
            // specified as a dictionary or as an array. On contrary to the Metric class APIs,
            // this approach does not take care of series capping and dimension capping. 
            // You need to take care of it yourself.

            MetricSeries purpleCowsSold = metrics.CreateNewSeries(
                            "Example Metrics",
                            "Animals Sold",
                            new Dictionary<string, string>() { ["Species"] = "Cows", ["Color"] = "Purple" },
                            new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false));

            MetricSeries yellowHorsesSold = metrics.CreateNewSeries(
                            "Example Metrics",
                            "Animals Sold",
                            new[] { new KeyValuePair<string, string>("Species", "Horses"), new KeyValuePair<string, string>("Color", "Yellow") },
                            new MetricSeriesConfigurationForMeasurement(restrictToUInt32Values: false));

            purpleCowsSold.TrackValue(42);
            yellowHorsesSold.TrackValue(132);

            // *** FLUSHING ***

            // MetricManager also allows you to flush all your metric aggregators and send the 
            // current aggregates to the cloud without waiting for the end of the ongoing 
            // aggregation period:

            TelemetryConfiguration.Active.GetMetricManager().Flush();
        }
    }
}
// ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
namespace User.Namespace.Example05
{
    using System;

    using Microsoft.ApplicationInsights;

    /// <summary>
    /// In this example we cover AggregationScope.
    /// </summary>
    public class Sample05
    {
        /// <summary />
        public static void Exec()
        {
            // *** AGGREGATION SCOPE ***

            // Previously we saw that metrics do not use the Telemetry Context of the Telemetry 
            // Client used to access them. We learned that using special dimension names available
            // as constants in MetricDimensionNames class is the best workaround for this
            // limitation. Here, we discuss the reasons for the limitation and other possible
            // workarounds.

            // Recall the problem description:
            // Metric aggregates sent by the below "Special Operation Request Size"-metric will 
            // NOT have their Context.Operation.Name set to "Special Operation".

            TelemetryClient specialClient = new TelemetryClient();
            specialClient.Context.Operation.Name = "Special Operation";
            specialClient.GetMetric("Special Operation Request Size").TrackValue(GetCurrentRequestSize());

            // The reason for that is that by default, metrics are aggregated at the scope of the 
            // TelemetryConfiguration pipeline and not at the scope of a particular
            // TelemetryClient. This is because of a very common pattern for Application Insights 
            // users, where a TelemetryClient is created for a small scope. For example:

            {
                // ...
                (new TelemetryClient()).TrackEvent("Something Interesting Happened");
                // ...
            }

            {
                try
                {
                    RunSomeCode();
                }
                catch (Exception apEx)
                {
                    (new TelemetryClient()).TrackException(apEx);
                }
            }

            // ...and so on.
            // We wanted to support this pattern and to allow users to write code like this:

            {
                // ...
                (new TelemetryClient()).GetMetric("Temperature").TrackValue(36.6);
                // ---
            }

            {
                // ...
                (new TelemetryClient()).GetMetric("Temperature").TrackValue(39.1);
                // ---
            }

            // In this case the expected behavior is that these values are aggregated together 
            // into a single aggregate with Count = 2, Sum = 75.7 and so on. In order to achieve 
            // that, we use a single MetricManager to create all the respective metric series. 
            // This manager is attached to the TelemetryConfiguration that stands behind a
            // TelemetryClient. This ensures that the two 
            // (new TelemetryClient()).GetMetric("Temperature") statements above return the same
            // Metric object. However, if different TelemetryClient instances return the name 
            // Metric instance, then what client's Context should the Metric respect? 
            // To avoid confusion, it respects none.

            // The best workaround for this circumstance was mentioned in a previous example:
            // use the special dimension names listed in the MetricDimensionNames class. However,
            // sometimes it is inconvenient. For example, if you already created a cached
            // TelemetryClient for a specific scope and set some custom Context properties. 
            // It is actually possible to create a metric that is only scoped to a single 
            // TelemetryClient instance. This will cause the creation of a special MetricManager 
            // instance at the scope of that one TelemetryClient. We highly recommend using this
            // feature with restraint, as a MetricManager can use a non-trivial amount of 
            // resources, including separate aggregators for each metric series and a managed 
            // thread for sending aggregated telemetry.
            // Here is how this works:

            TelemetryClient operationClient = new TelemetryClient();

            // This client will only send telemetry related to a specific operation:
            operationClient.Context.Operation.Name = "Operation XYZ";

            // This client sends telemetry to a special Application Insights component:
            operationClient.InstrumentationKey = "05B5093A-F137-4A68-B826-A950CB68C68F"; 

            Metric operationRequestSize = operationClient.GetMetric(
                        "XYZ Request Size", 
                        MetricConfigurations.Common.Measurement(), 
                        MetricAggregationScope.TelemetryClient);

            int requestSize = GetCurrentRequestSize();
            operationRequestSize.TrackValue(306000);

            // Note the last parameter to GetMetric(..): MetricAggregationScope.TelemetryClient.
            // This instructed the GetMetric API not to use the metric manager at the 
            // TelemetryConfiguration scope, but to create and use a metric manager at the 
            // respective client's scope instead.
        }

        private static void RunSomeCode()
        {
            throw new Exception();
        }

        private static int GetCurrentRequestSize()
        {
            // Do stuff
            return 11000;
        }
    }
}
// ----------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
namespace User.Namespace.Example06ab
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using Microsoft.ApplicationInsights;
    using Microsoft.ApplicationInsights.Metrics;
    using Microsoft.ApplicationInsights.Channel;
    using Microsoft.ApplicationInsights.DataContracts;
    using Microsoft.ApplicationInsights.Extensibility;
    using Microsoft.ApplicationInsights.Extensibility.Implementation;

    using TraceSeveretyLevel = Microsoft.ApplicationInsights.DataContracts.SeverityLevel;
    
    /// <summary>
    /// In this example we discuss how to write unit tests that validate that metrics are sent 
    /// correctly. We will consider two approaches:
    ///  a) Capturing all telemetry emitted by a method, including, but not limited to Metric
    ///     Telemetry, where a telemetry client can be injected.
    ///  b) Capturing all telemetry emitted by a method, including, but not limited to Metric
    ///     Telemetry, where the (new TelemetryClient()).TrackXxx(..) pattern in used in-line.
    /// </summary>
    public class Sample06ab
    {
        /// <summary />
        public static void ExecA()
        {
            // *** UNIT TESTS: CAPTURING TELEMETRY BY INJECTING A TELEMETRY CLIENT ***

            // Here we will use a common unit test to capture and verify all Application Insights 
            // telemetry emitted by a method SellPurpleDucks() of class ServiceClassA. We also
            // assume that the class has been prepared for testing by allowing
            // to specify a telemetry client using dependency injection. The code for the class is
            // listed below. In a production application the class will probably be instantiated
            // and called like this:

            {
                ServiceClassA serviceA = new ServiceClassA(new TelemetryClient());
                serviceA.SellPurpleDucks(42);
            }

            // In a unit test you will need to create a custom telemetry configuration that routs
            // the emitted telemetry into a data structure for later inspection. 
            // There is a TestUtil class below that shows how to do that. Here is the unit test:

            {
                // Create the test pipeline and client:
                IList<ITelemetry> telemetrySentToChannel;
                TelemetryConfiguration telemetryPipeline = TestUtil.CreateApplicationInsightsTelemetryConfiguration(out telemetrySentToChannel);
                using (telemetryPipeline)
                { 
                    TelemetryClient telemetryClient = new TelemetryClient(telemetryPipeline);

                    // Invoke method being tested:
                    ServiceClassA serviceA = new ServiceClassA(telemetryClient);
                    serviceA.SellPurpleDucks(42);

                    // Make sure all telemetry is collected:
                    telemetryClient.Flush();

                    // Flushing the MetricManager is particularly important since the aggregation
                    // period of 1 minute has just started:
                    telemetryPipeline.GetMetricManager().Flush();

                    // Verify that the right telemetry was sent:
                    TestUtil.AssertAreEqual(2, telemetrySentToChannel.Count);

                    TraceTelemetry[] traceItems = telemetrySentToChannel.Where( (t) => ((t != null) && (t is TraceTelemetry)) )
                                                                        .Select( (t) => ((TraceTelemetry) t) )
                                                                        .ToArray();
                    TestUtil.AssertAreEqual(1, traceItems.Length);
                    TestUtil.AssertAreEqual("Stuff #1 completed", traceItems[0].Message);
                    TestUtil.AssertAreEqual(TraceSeveretyLevel.Information, traceItems[0].SeverityLevel);

                    MetricTelemetry[] metricItems = telemetrySentToChannel.Where( (t) => ((t != null) && (t is MetricTelemetry)) )
                                                                          .Select( (t) => ((MetricTelemetry) t) )
                                                                          .ToArray();
                    TestUtil.AssertAreEqual(1, metricItems.Length);
                    TestUtil.AssertAreEqual("Ducks Sold", metricItems[0].Name);
                    TestUtil.AssertAreEqual(1, metricItems[0].Count);
                    TestUtil.AssertAreEqual(42.0, metricItems[0].Sum);
                    TestUtil.AssertAreEqual(42.0, metricItems[0].Min);
                    TestUtil.AssertAreEqual(42.0, metricItems[0].Max);
                    TestUtil.AssertAreEqual(0.0, metricItems[0].StandardDeviation);
                    TestUtil.AssertAreEqual(2, metricItems[0].Properties.Count);
                    TestUtil.AssertAreEqual(true, metricItems[0].Properties.ContainsKey("_MS.AggregationIntervalMs"));
                    TestUtil.AssertAreEqual(true, metricItems[0].Properties.ContainsKey("Color"));
                    TestUtil.AssertAreEqual("Purple", metricItems[0].Properties["Color"]);
                }
            }
        }

        /// <summary />
        public static void ExecB()
        {
            // *** UNIT TESTS: CAPTURING TELEMETRY BY SUBSTITUTING THE TELEMETRY CHANNEL ***

            // Previously we used dependency injection to provide a custom telemetry client to
            // test a method.

            // Consider now a slightly modified class ServiceClassB that does not expect a custom
            // telemetry client. We can test it by substituting the channel used in the default 
            // telemetry pipeline. 

            // In a production application the class will probably be instantiated and called like
            // this:

            {
                ServiceClassB serviceB = new ServiceClassB();
                serviceB.SellPurpleDucks(42);
            }

            // Here is the unit test:

            {
                // Do not forget to set the InstrumentationKey to some value, otherwise the 
                // pipeline will not send any telemetry to the channel.
                TelemetryConfiguration.Active.InstrumentationKey = Guid.NewGuid().ToString("D");

                // This approach is more widely applicable, and does not require to prepare your
                // code for injection of a telemetry client. However, a significant drawback is
                // that in this model different unit tests can interfere with each other via the
                // static default telemetry pipeline. Such interference may be non-trivial. 
                // E.g., for this simple test, we need to flush out all the tracked values from 
                // the code that just run. 
                TelemetryConfiguration.Active.GetMetricManager().Flush();
                (new TelemetryClient(TelemetryConfiguration.Active)).Flush();

                // Create the test pipeline and client.
                StubTelemetryChannel telemetryCollector = new StubTelemetryChannel();
                TelemetryConfiguration.Active.TelemetryChannel = telemetryCollector;
                TelemetryConfiguration.Active.InstrumentationKey = Guid.NewGuid().ToString("D");

                // Invoke method being tested:
                ServiceClassB serviceB = new ServiceClassB();
                serviceB.SellPurpleDucks(42);

                // Flushing the MetricManager is particularly important since the aggregation 
                // period of 1 minute has just started:
                TelemetryConfiguration.Active.GetMetricManager().Flush();

                // Verify that the right telemetry was sent:

                TestUtil.AssertAreEqual(2, telemetryCollector.TelemetryItems.Count);

                TraceTelemetry[] traceItems = telemetryCollector.TelemetryItems.Where( (t) => ((t != null) && (t is TraceTelemetry)) )
                                                                               .Select( (t) => ((TraceTelemetry) t) )
                                                                               .ToArray();
                TestUtil.AssertAreEqual(1, traceItems.Length);
                TestUtil.AssertAreEqual("Stuff #1 completed", traceItems[0].Message);
                TestUtil.AssertAreEqual(TraceSeveretyLevel.Information, traceItems[0].SeverityLevel);

                MetricTelemetry[] metricItems = telemetryCollector.TelemetryItems.Where( (t) => ((t != null) && (t is MetricTelemetry)) )
                                                                                 .Select( (t) => ((MetricTelemetry) t) )
                                                                                 .ToArray();
                TestUtil.AssertAreEqual(1, metricItems.Length);
                TestUtil.AssertAreEqual("Ducks Sold", metricItems[0].Name);
                TestUtil.AssertAreEqual(1, metricItems[0].Count);
                TestUtil.AssertAreEqual(42, metricItems[0].Sum);
                TestUtil.AssertAreEqual(42, metricItems[0].Min);
                TestUtil.AssertAreEqual(42, metricItems[0].Max);
                TestUtil.AssertAreEqual(0, metricItems[0].StandardDeviation);
                TestUtil.AssertAreEqual(2, metricItems[0].Properties.Count);
                TestUtil.AssertAreEqual(true, metricItems[0].Properties.ContainsKey("_MS.AggregationIntervalMs"));
                TestUtil.AssertAreEqual(true, metricItems[0].Properties.ContainsKey("Color"));
                TestUtil.AssertAreEqual("Purple", metricItems[0].Properties["Color"]);
            }
        }
    }

    internal class ServiceClassA
    {
        private TelemetryClient _telemetryClient = null;

        public ServiceClassA(TelemetryClient telemetryClient)
        {
            if (telemetryClient == null)
            {
                throw new ArgumentNullException(nameof(telemetryClient));
            }

            _telemetryClient = telemetryClient;
        }

        public void SellPurpleDucks(int count)
        {
            // Do some stuff #1...
            _telemetryClient.TrackTrace("Stuff #1 completed", TraceSeveretyLevel.Information);

            // Do more stuff...
            _telemetryClient.GetMetric("Ducks Sold", "Color").TryTrackValue(count, "Purple");
        }
    }

    internal class ServiceClassB
    {
        public ServiceClassB()
        {
        }

        public void SellPurpleDucks(int count)
        {
            // Do some stuff #1...
            (new TelemetryClient()).TrackTrace("Stuff #1 completed", TraceSeveretyLevel.Information);

            // Do more stuff...
            (new TelemetryClient()).GetMetric("Ducks Sold", "Color").TryTrackValue(count, "Purple");
        }
    }

    internal class TestUtil
    {
        public static TelemetryConfiguration CreateApplicationInsightsTelemetryConfiguration(out IList<ITelemetry> telemetrySentToChannel)
        {
            StubTelemetryChannel channel = new StubTelemetryChannel();
            string iKey = Guid.NewGuid().ToString("D");
            TelemetryConfiguration telemetryConfig = new TelemetryConfiguration(iKey, channel);

            var channelBuilder = new TelemetryProcessorChainBuilder(telemetryConfig);
            channelBuilder.Build();

            foreach (ITelemetryProcessor initializer in telemetryConfig.TelemetryInitializers)
            {
                ITelemetryModule m = initializer as ITelemetryModule;
                if (m != null)
                {
                    m.Initialize(telemetryConfig);
                }
            }

            foreach (ITelemetryProcessor processor in telemetryConfig.TelemetryProcessors)
            {
                ITelemetryModule m = processor as ITelemetryModule;
                if (m != null)
                {
                    m.Initialize(telemetryConfig);
                }
            }

            telemetrySentToChannel = channel.TelemetryItems;
            return telemetryConfig;
        }

        public static void AssertAreEqual(object x, object y)
        {
            if (x == null && y == null)
            {
                return;
            }

            if (x == null)
            {
                throw new Exception($"Values were expected to be equal, but x is null.");
            }

            if (! x.Equals(y))
            {
                throw new Exception($"Values were expected to be equal, but they are not ({x} != {y ?? "null"}).");
            }
        }
    }

    internal class StubTelemetryChannel : ITelemetryChannel
    {
        public StubTelemetryChannel()
        {
            TelemetryItems = new List<ITelemetry>();
        }

        public bool? DeveloperMode { get; set; }

        public string EndpointAddress { get; set; }

        public IList<ITelemetry> TelemetryItems { get; }

        public void Send(ITelemetry item)
        {
            TelemetryItems.Add(item);
        }

        public void Dispose()
        {
        }

        public void Flush()
        {
        }
    }
}
// ----------- ----------- ----------- ----------- ----------- ----------- ----------- ----------- 
@macrogreg macrogreg self-assigned this Apr 15, 2018
@macrogreg macrogreg changed the title Examples for new metrics APIs in Application Insights .Net SDK 2.6.0-beta3 Examples for new metrics APIs in .Net SDK 2.6.0-beta3 Apr 15, 2018
@macrogreg macrogreg changed the title Examples for new metrics APIs in .Net SDK 2.6.0-beta3 Examples / Tutorial for new metrics APIs in .Net SDK 2.6.0-beta3 Apr 15, 2018
@macrogreg macrogreg changed the title Examples / Tutorial for new metrics APIs in .Net SDK 2.6.0-beta3 New metrics APIs in .Net SDK 2.6.0-beta3: Tutorial & Examples Apr 15, 2018
@macrogreg macrogreg changed the title New metrics APIs in .Net SDK 2.6.0-beta3: Tutorial & Examples Metrics APIs in SDK 2.6.0-beta3: Tutorial & Examples Apr 15, 2018
@SergeyKanzhelev
Copy link
Contributor

Great work! When do you plan to move this to docs https://docs.microsoft.com/azure/application-insights/app-insights-api-custom-events-metrics?

@macrogreg
Copy link
Contributor Author

@SergeyKanzhelev , I think the time for it is when we are definitely moving out of Beta.
But please let me know if you feel different.

@SergeyKanzhelev
Copy link
Contributor

CC: @MS-TimothyMothra we have very limited time to decide. The process may already have started. Timothy can help with dates.

@TimothyMothra
Copy link
Member

I would recommend making your changes now, and annotating that these changes are available with the release of 2.6.0. It takes a week or two for azure docs to review and accept edits.

@macrogreg
Copy link
Contributor Author

macrogreg commented Apr 19, 2018

I am working with PM to get a recommendation about another Beta vs Stable by the end o the week.
Talking about this: if we go with another Beta, when is the next stable release?

@TimothyMothra
Copy link
Member

The next stable release (2.6.0) will be May 1st.
Your namespace change will be in 2.7-Beta1 which will be early May.

@macrogreg
Copy link
Contributor Author

Right. What I meant was when is the next Stable after May.
I recall there used to be a published page with the schedule, but I seem not to be able to find again. :)

@macrogreg
Copy link
Contributor Author

Oh, I was searching under .Net, but just found this in AI-Home:
https://github.com/Microsoft/ApplicationInsights-Home/wiki/SDK-Release-Schedule

This does not go beyond May. So if we were to choose not to move the bits released in the current Beta to stable and to leave them in Beta for one more cycle, when would be the next stable release?

@TimothyMothra
Copy link
Member

Just updated the Release Schedule :)
Stable releases will be every other month. 2.6 May, 2.7 July

@github-actions
Copy link

This issue is stale because it has been open 300 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@zendu
Copy link

zendu commented Mar 16, 2022

It's been 4 years and these important examples are missing from official documentation. I spent half an hour hunting for examples of App insights with custom metrics to understand how to use them. Please make them official as soon as possible.

@cijothomas
Copy link
Contributor

To the best of my knowledge, no one is working to add this tutorials to the official documentation. It might be a good idea to link to this from the docs page. Consider opening an issue in the docs repo itself.

Also, the biggest reason why pre-aggregated metrics are to be used - is still in preview https://docs.microsoft.com/en-us/azure/azure-monitor/app/pre-aggregated-metrics-log-metrics#custom-metrics-dimensions-and-pre-aggregation.
^ Are you also using this feature?

@github-actions
Copy link

This issue is stale because it has been open 300 days with no activity. Remove stale label or this will be closed in 7 days. Commenting will instruct the bot to automatically remove the label.

Copy link

github-actions bot commented Nov 9, 2023

This issue is stale because it has been open 300 days with no activity. Remove stale label or this will be closed in 7 days. Commenting will instruct the bot to automatically remove the label.

@github-actions github-actions bot added the stale label Nov 9, 2023
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Nov 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants