diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 63bf9532f5a..ec69c97b5fc 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -161,6 +161,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metrics", "metrics", "{3277B1C0-BDFE-4460-9B0D-D9A661FB48DB}" ProjectSection(SolutionItems) = preProject docs\metrics\building-your-own-exporter.md = docs\metrics\building-your-own-exporter.md + docs\metrics\README.md = docs\metrics\README.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "logs", "logs", "{3862190B-E2C5-418E-AFDC-DB281FB5C705}" @@ -213,6 +214,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests", "test\OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests\OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests.csproj", "{4D7201BC-7124-4401-AD65-FAB58A053D45}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "getting-started-histogram", "docs\metrics\getting-started-histogram\getting-started-histogram.csproj", "{92ED77A6-37B4-447D-B4C4-15DB005A589C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -423,6 +426,10 @@ Global {4D7201BC-7124-4401-AD65-FAB58A053D45}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D7201BC-7124-4401-AD65-FAB58A053D45}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D7201BC-7124-4401-AD65-FAB58A053D45}.Release|Any CPU.Build.0 = Release|Any CPU + {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -455,6 +462,7 @@ Global {08D29501-F0A3-468F-B18D-BD1821A72383} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} {64E3D8BB-93AB-4571-93F7-ED8D64DFFD06} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {92ED77A6-37B4-447D-B4C4-15DB005A589C} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/docs/metrics/README.md b/docs/metrics/README.md new file mode 100644 index 00000000000..c1f16fa5116 --- /dev/null +++ b/docs/metrics/README.md @@ -0,0 +1,4 @@ +# Getting Started with OpenTelemetry .NET Metrics in 5 Minutes + +* [Getting started with Counter](.\getting-started\README.md) +* [Getting started with Histogram](.\getting-started-histogram\README.md) diff --git a/docs/metrics/getting-started-histogram/Program.cs b/docs/metrics/getting-started-histogram/Program.cs new file mode 100644 index 00000000000..9b74b5e5052 --- /dev/null +++ b/docs/metrics/getting-started-histogram/Program.cs @@ -0,0 +1,55 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +public class Program +{ + private static readonly Meter MyMeter = new Meter("TestMeter", "0.0.1"); + private static readonly Histogram MyHistogram = MyMeter.CreateHistogram("histogram"); + private static readonly Random RandomGenerator = new Random(); + + public static async Task Main(string[] args) + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddSource("TestMeter") + .AddConsoleExporter() + .Build(); + + using var token = new CancellationTokenSource(); + Task writeMetricTask = new Task(() => + { + while (!token.IsCancellationRequested) + { + MyHistogram.Record( + RandomGenerator.Next(1, 1000), + new KeyValuePair("tag1", "value1"), + new KeyValuePair("tag2", "value2")); + Task.Delay(10).Wait(); + } + }); + writeMetricTask.Start(); + + token.CancelAfter(10000); + await writeMetricTask; + } +} diff --git a/docs/metrics/getting-started-histogram/README.md b/docs/metrics/getting-started-histogram/README.md new file mode 100644 index 00000000000..0b8e9fabc9f --- /dev/null +++ b/docs/metrics/getting-started-histogram/README.md @@ -0,0 +1,67 @@ +# Getting Started with OpenTelemetry .NET in 5 Minutes + +First, download and install the [.NET Core +SDK](https://dotnet.microsoft.com/download) on your computer. + +Create a new console application and run it: + +```sh +dotnet new console --output getting-started-histogram +cd getting-started +dotnet run +``` + +You should see the following output: + +```text +Hello World! +``` + +Install the +[OpenTelemetry.Exporter.Console](../../../src/OpenTelemetry.Exporter.Console/README.md) +package: + +```sh +dotnet add package OpenTelemetry.Exporter.Console +``` + +Update the `Program.cs` file with the code from [Program.cs](./Program.cs): + +Run the application again (using `dotnet run`) and you should see the metric +output from the console, similar to shown below: + + +```text +Export 14:30:58.201 14:30:59.177 histogram [tag1=value1;tag2=value2] Histogram, Meter: TestMeter/0.0.1 +Value: Sum: 33862 Count: 62 +(-? - 0) : 0 +(0 - 5) : 0 +(5 - 10) : 0 +(10 - 25) : 2 +(25 - 50) : 0 +(50 - 75) : 1 +(75 - 100) : 1 +(100 - 250) : 6 +(250 - 500) : 18 +(500 - 1000) : 34 +(1000 - ?) : 0 +``` + + +Congratulations! You are now collecting histogram metrics using OpenTelemetry. + +What does the above program do? + +The program creates a +[Meter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meter) +instance named "TestMeter" and then creates a +[Histogram](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#histogram) +instrument from it. This histogram is used to repeatedly report random metric +measurements until exited after 10 seconds. + +An OpenTelemetry +[MeterProvider](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meterprovider) +is configured to subscribe to instruments from the Meter `TestMeter`, and +aggregate the measurements in-memory. The pre-aggregated metrics are exported +every 1 second to a `ConsoleExporter`. `ConsoleExporter` simply displays it on +the console. diff --git a/docs/metrics/getting-started-histogram/getting-started-histogram.csproj b/docs/metrics/getting-started-histogram/getting-started-histogram.csproj new file mode 100644 index 00000000000..9f5b6b79bc3 --- /dev/null +++ b/docs/metrics/getting-started-histogram/getting-started-histogram.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index e641db63d81..f07e7480fcd 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -88,7 +88,15 @@ public override ExportResult Export(in Batch batch) case MetricType.Histogram: { var histogramMetric = metric as IHistogramMetric; - valueDisplay = string.Format("Sum: {0} Count: {1}", histogramMetric.PopulationSum, histogramMetric.PopulationCount); + var bucketsBuilder = new StringBuilder(); + bucketsBuilder.Append($"Sum: {histogramMetric.PopulationSum} Count: {histogramMetric.PopulationCount} \n"); + foreach (var bucket in histogramMetric.Buckets) + { + bucketsBuilder.Append($"({bucket.LowBoundary} - {bucket.HighBoundary}) : {bucket.Count}"); + bucketsBuilder.AppendLine(); + } + + valueDisplay = bucketsBuilder.ToString(); break; } @@ -102,7 +110,7 @@ public override ExportResult Export(in Batch batch) string time = $"{metric.StartTimeExclusive.ToLocalTime().ToString("HH:mm:ss.fff")} {metric.EndTimeInclusive.ToLocalTime().ToString("HH:mm:ss.fff")}"; - var msg = new StringBuilder($"Export {time} {metric.Name} [{string.Join(";", tags)}] {metric.MetricType} Value: {valueDisplay}"); + var msg = new StringBuilder($"Export {time} {metric.Name} [{string.Join(";", tags)}] {metric.MetricType}"); if (!string.IsNullOrEmpty(metric.Description)) { @@ -124,6 +132,8 @@ public override ExportResult Export(in Batch batch) } } + msg.AppendLine(); + msg.Append($"Value: {valueDisplay}"); Console.WriteLine(msg); } } diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs index 448bf1215d3..b54ae5e9dd2 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs +++ b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs @@ -18,8 +18,8 @@ namespace OpenTelemetry.Metrics { public struct HistogramBucket { - internal double LowBoundary; - internal double HighBoundary; - internal long Count; + public double LowBoundary; + public double HighBoundary; + public long Count; } } diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetric.cs b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetric.cs new file mode 100644 index 00000000000..cf740093875 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetric.cs @@ -0,0 +1,70 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics +{ + internal class HistogramMetric : IHistogramMetric + { + internal HistogramMetric(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes, int bucketCount) + { + this.Name = name; + this.Description = description; + this.Unit = unit; + this.Meter = meter; + this.StartTimeExclusive = startTimeExclusive; + this.Attributes = attributes; + this.MetricType = MetricType.Histogram; + this.BucketsArray = new HistogramBucket[bucketCount]; + } + + public string Name { get; private set; } + + public string Description { get; private set; } + + public string Unit { get; private set; } + + public Meter Meter { get; private set; } + + public DateTimeOffset StartTimeExclusive { get; internal set; } + + public DateTimeOffset EndTimeInclusive { get; internal set; } + + public KeyValuePair[] Attributes { get; private set; } + + public bool IsDeltaTemporality { get; internal set; } + + public IEnumerable Exemplars { get; private set; } = new List(); + + public long PopulationCount { get; internal set; } + + public double PopulationSum { get; internal set; } + + public IEnumerable Buckets => this.BucketsArray; + + public MetricType MetricType { get; private set; } + + internal HistogramBucket[] BucketsArray { get; set; } + + public string ToDisplayString() + { + return $"Count={this.PopulationCount},Sum={this.PopulationSum}"; + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs index 0f1c79aec4c..7b5229fdbd3 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs +++ b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs @@ -20,14 +20,17 @@ namespace OpenTelemetry.Metrics { - internal class HistogramMetricAggregator : IHistogramMetric, IAggregator + internal class HistogramMetricAggregator : IAggregator { private static readonly double[] DefaultBoundaries = new double[] { 0, 5, 10, 25, 50, 75, 100, 250, 500, 1000 }; private readonly object lockUpdate = new object(); private HistogramBucket[] buckets; - + private long populationCount; + private double populationSum; private double[] boundaries; + private DateTimeOffset startTimeExclusive; + private HistogramMetric histogramMetric; internal HistogramMetricAggregator(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) : this(name, description, unit, meter, startTimeExclusive, attributes, DefaultBoundaries) @@ -36,12 +39,8 @@ internal HistogramMetricAggregator(string name, string description, string unit, internal HistogramMetricAggregator(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes, double[] boundaries) { - this.Name = name; - this.Description = description; - this.Unit = unit; - this.Meter = meter; - this.StartTimeExclusive = startTimeExclusive; - this.Attributes = attributes; + this.startTimeExclusive = startTimeExclusive; + this.histogramMetric = new HistogramMetric(name, description, unit, meter, startTimeExclusive, attributes, boundaries.Length + 1); if (boundaries.Length == 0) { @@ -50,35 +49,8 @@ internal HistogramMetricAggregator(string name, string description, string unit, this.boundaries = boundaries; this.buckets = this.InitializeBucket(boundaries); - this.MetricType = MetricType.Summary; } - public string Name { get; private set; } - - public string Description { get; private set; } - - public string Unit { get; private set; } - - public Meter Meter { get; private set; } - - public DateTimeOffset StartTimeExclusive { get; private set; } - - public DateTimeOffset EndTimeInclusive { get; private set; } - - public KeyValuePair[] Attributes { get; private set; } - - public bool IsDeltaTemporality { get; private set; } - - public IEnumerable Exemplars { get; private set; } = new List(); - - public long PopulationCount { get; private set; } - - public double PopulationSum { get; private set; } - - public MetricType MetricType { get; private set; } - - public IEnumerable Buckets => this.buckets; - public void Update(T value) where T : struct { @@ -111,37 +83,34 @@ public void Update(T value) lock (this.lockUpdate) { - this.PopulationCount++; - this.PopulationSum += val; + this.populationCount++; + this.populationSum += val; this.buckets[i].Count++; } } public IMetric Collect(DateTimeOffset dt, bool isDelta) { - if (this.PopulationCount == 0) + if (this.populationCount == 0) { // TODO: Output stale markers return null; } - var cloneItem = new HistogramMetricAggregator(this.Name, this.Description, this.Unit, this.Meter, this.StartTimeExclusive, this.Attributes, this.boundaries); - lock (this.lockUpdate) { - cloneItem.Exemplars = this.Exemplars; - cloneItem.EndTimeInclusive = dt; - cloneItem.PopulationCount = this.PopulationCount; - cloneItem.PopulationSum = this.PopulationSum; - cloneItem.boundaries = this.boundaries; - this.buckets.CopyTo(cloneItem.buckets, 0); - cloneItem.IsDeltaTemporality = isDelta; + this.histogramMetric.StartTimeExclusive = this.startTimeExclusive; + this.histogramMetric.EndTimeInclusive = dt; + this.histogramMetric.PopulationCount = this.populationCount; + this.histogramMetric.PopulationSum = this.populationSum; + this.buckets.CopyTo(this.histogramMetric.BucketsArray, 0); + this.histogramMetric.IsDeltaTemporality = isDelta; if (isDelta) { - this.StartTimeExclusive = dt; - this.PopulationCount = 0; - this.PopulationSum = 0; + this.startTimeExclusive = dt; + this.populationCount = 0; + this.populationSum = 0; for (int i = 0; i < this.buckets.Length; i++) { this.buckets[i].Count = 0; @@ -149,12 +118,13 @@ public IMetric Collect(DateTimeOffset dt, bool isDelta) } } - return cloneItem; - } - - public string ToDisplayString() - { - return $"Count={this.PopulationCount},Sum={this.PopulationSum}"; + // TODO: Confirm that this approach of + // re-using the same instance is correct. + // This avoids allocating a new instance. + // It is read only for Exporters, + // and also there is no parallel + // Collect allowed. + return this.histogramMetric; } private HistogramBucket[] InitializeBucket(double[] boundaries) diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs index fb8437dd687..e31fe0214a3 100644 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs @@ -44,9 +44,9 @@ public void HistogramDistributeToAllBuckets() var metric = hist.Collect(DateTimeOffset.UtcNow, false); Assert.NotNull(metric); - Assert.IsType(metric); + Assert.IsType(metric); - if (metric is HistogramMetricAggregator agg) + if (metric is HistogramMetric agg) { int len = 0; foreach (var bucket in agg.Buckets) @@ -71,9 +71,9 @@ public void HistogramCustomBoundaries() var metric = hist.Collect(DateTimeOffset.UtcNow, false); Assert.NotNull(metric); - Assert.IsType(metric); + Assert.IsType(metric); - if (metric is HistogramMetricAggregator agg) + if (metric is HistogramMetric agg) { int len = 0; foreach (var bucket in agg.Buckets) @@ -102,9 +102,9 @@ public void HistogramWithEmptyBuckets() var metric = hist.Collect(DateTimeOffset.UtcNow, false); Assert.NotNull(metric); - Assert.IsType(metric); + Assert.IsType(metric); - if (metric is HistogramMetricAggregator agg) + if (metric is HistogramMetric agg) { var expectedCounts = new int[] { 3, 0, 2, 1 }; int len = 0;