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;