diff --git a/.gitignore b/.gitignore
index b6d3c01b7ff..af409279810 100644
--- a/.gitignore
+++ b/.gitignore
@@ -345,3 +345,6 @@ ASALocalRun/
/.sonarqube
/src/LastMajorVersionBinaries
+
+# Tempo files
+tempo-data/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 69352b976ef..9aec1d40d9c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -59,10 +59,6 @@ of Windows.
* Visual Studio 2022+ or Visual Studio Code
* .NET Framework 4.6.2+
-> **Note**
-> Visual Studio 2022 preview is **recommended** due to projects needing
-to target .NET preview versions.
-
### Public API
It is critical to keep public API surface small and clean. This repository is
diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln
index 0532a56ca84..1880cbb93c5 100644
--- a/OpenTelemetry.sln
+++ b/OpenTelemetry.sln
@@ -249,6 +249,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-console", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-jaeger", "docs\trace\getting-started-jaeger\getting-started-jaeger.csproj", "{A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "stratified-sampling-example", "docs\trace\advanced\stratified-sampling-example\stratified-sampling-example.csproj", "{9C99621C-343E-479C-A943-332DB6129B71}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -523,6 +525,10 @@ Global
{A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9C99621C-343E-479C-A943-332DB6129B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9C99621C-343E-479C-A943-332DB6129B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9C99621C-343E-479C-A943-332DB6129B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9C99621C-343E-479C-A943-332DB6129B71}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -562,6 +568,7 @@ Global
{DEDE8442-03CA-48CF-99B9-EA224D89D148} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
{EF4F6280-14D1-49D4-8095-1AC36E169AA8} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
{A0C0B77C-6C7B-4EC2-AC61-EA1F489811B9} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
+ {9C99621C-343E-479C-A943-332DB6129B71} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521}
diff --git a/build/Common.prod.props b/build/Common.prod.props
index 90119d36dab..872830ccbd3 100644
--- a/build/Common.prod.props
+++ b/build/Common.prod.props
@@ -6,6 +6,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
All
diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md
index 4162afa3da9..eb2227a20d1 100644
--- a/docs/metrics/customizing-the-sdk/README.md
+++ b/docs/metrics/customizing-the-sdk/README.md
@@ -403,6 +403,98 @@ AnotherFruitCounter.Add(4, new("name", "mango"), new("color", "yellow")); // Not
streams. There is no ability to apply different limits for each instrument at
this moment.
+### Exemplars
+
+Exemplars are example data points for aggregated data. They provide access to
+the raw measurement value, time stamp when measurement was made, and trace
+context, if any. It also provides "Filtered Tags", which are attributes (Tags)
+that are [dropped by a view](#select-specific-tags). Exemplars are an opt-in
+feature, and allow customization via ExemplarFilter and ExemplarReservoir.
+
+Exemplar collection in OpenTelemetry .NET is done automatically (once Exemplar
+feature itself is enabled on `MeterProvider`). There is no separate API
+to report exemplar data. If an app is already using existing Metrics API
+(manually or via instrumentation libraries), exemplars can be configured/enabled
+without requiring instrumentation changes.
+
+While the SDK is capable of producing exemplars automatically, the exporters
+(and the backends) must also support them in order to be useful. OTLP Metric
+Exporter has support for this today, and this [end-to-end
+tutorial](../exemplars/README.md) demonstrates how to use exemplars to achieve
+correlation from metrics to traces, which is one of the primary use cases for
+exemplars.
+
+#### ExemplarFilter
+
+`ExemplarFilter` determines which measurements are eligible to become an
+Exemplar. i.e. `ExemplarFilter` determines which measurements are offered to
+`ExemplarReservoir`, which makes the final decision about whether the offered
+measurement gets stored as an exemplar. They can be used to control the noise
+and overhead associated with Exemplar collection.
+
+OpenTelemetry SDK comes with the following Filters:
+
+* `AlwaysOnExemplarFilter` - makes all measurements eligible for being an Exemplar.
+* `AlwaysOffExemplarFilter` - makes no measurements eligible for being an
+ Exemplar. Using this is as good as turning off Exemplar feature, and is the current
+ default.
+* `TraceBasedExemplarFilter` - makes those measurements eligible for being an
+Exemplar, which are recorded in the context of a sampled parent `Activity`
+(span).
+
+`SetExemplarFilter` method on `MeterProviderBuilder` can be used to set the
+desired `ExemplarFilter`.
+
+The snippet below shows how to set `ExemplarFilter`.
+
+```csharp
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+
+using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ // rest of config not shown
+ .SetExemplarFilter(new TraceBasedExemplarFilter())
+ .Build();
+```
+
+> **Note**
+> As of today, there is no separate toggle for enable/disable Exemplar feature.
+Exemplars can be disabled by setting filter as `AlwaysOffExemplarFilter`, which
+is also the default (i.e Exemplar feature is disabled by default). Users can
+enable the feature by setting filter to anything other than
+`AlwaysOffExemplarFilter`. For example: `.SetExemplarFilter(new TraceBasedExemplarFilter())`.
+
+If the built-in `ExemplarFilter`s are not meeting the needs, one may author
+custom `ExemplarFilter` as shown
+[here](../extending-the-sdk/README.md#exemplarfilter). A custom filter, which
+eliminates all un-interesting measurements from becoming Exemplar is a
+recommended way to control performance overhead associated with collecting
+Exemplars. See
+[benchmark](../../../test/Benchmarks/Metrics/ExemplarBenchmarks.cs) to see how
+much impact can `ExemplarFilter` have on performance.
+
+#### ExemplarReservoir
+
+`ExemplarReservoir` receives the measurements sampled in by the `ExemplarFilter`
+and is responsible for storing Exemplars. `ExemplarReservoir` ultimately decides
+which measurements get stored as exemplars. The following are the default
+reservoirs:
+
+* `AlignedHistogramBucketExemplarReservoir` is the default reservoir used for
+Histograms with buckets, and it stores at most one exemplar per histogram
+bucket. The exemplar stored is the last measurement recorded - i.e. any new
+measurement overwrites the previous one in that bucket.
+
+`SimpleExemplarReservoir` is the default reservoir used for all metrics except
+Histograms with buckets. It has a fixed reservoir pool, and implements the
+equivalent of [naive
+reservoir](https://en.wikipedia.org/wiki/Reservoir_sampling). The reservoir pool
+size (currently defaulting to 10) determines the maximum number of exemplars
+stored.
+
+> **Note**
+> Currently there is no ability to change or configure Reservoir.
+
### Instrumentation
// TODO
diff --git a/docs/metrics/exemplars/README.md b/docs/metrics/exemplars/README.md
new file mode 100644
index 00000000000..0c66303fa88
--- /dev/null
+++ b/docs/metrics/exemplars/README.md
@@ -0,0 +1,99 @@
+# Using Exemplars in OpenTelemetry .NET
+
+Exemplars are example data points for aggregated data. They provide specific
+context to otherwise general aggregations. One common use case is to gain
+ability to correlate metrics to traces (and logs). While OpenTelemetry .NET
+supports Exemplars, it is only useful if the telemetry backend also supports the
+capabilities. This tutorial uses well known open source backends to demonstrate
+the concept. The following are the components involved:
+
+* Test App - We use existing example app from the repo. This app is already
+instrumented with OpenTelemetry for logs, metrics and traces, and is configured
+to export them to the configured OTLP end point.
+* OpenTelemetry Collector - An instance of collector is run, which receives
+telemetry from the above app using OTLP. The collector then exports metrics to
+Prometheus, traces to Tempo.
+* Prometheus - Prometheus is used as the Metric backend.
+* Tempo - Tempo is used as the Tracing backend.
+* Grafana - UI to query metrics from Prometheus, traces from Tempo, and to
+ navigate between metrics and traces using Exemplar.
+
+All these components except the test app require additional configuration to
+enable Exemplar feature. To make it easy for users, these components are
+pre-configured to enable Exemplars, and a docker-compose is provided to spun
+ them all up, in the required configurations.
+
+## Pre-requisite
+
+Install docker:
+
+## Setup
+
+As mentioned in the intro, this tutorial uses OTel Collector, Prometheus, Tempo,
+and Grafana, and they must be up and running before proceeding. The following
+spins all of them with the correct configurations to support Exemplars.
+
+Navigate to current directory and run the following:
+
+```sh
+docker-compose up -d
+```
+
+If the above step succeeds, all dependencies would be spun up and ready now. To
+test, navigate to Grafana running at: "http://localhost:3000/".
+
+## Run test app
+
+Now that the required dependencies are ready, lets run the demo app.
+This tutorial is using the existing ASP.NET Core app from the repo.
+
+Navigate to [Example Asp.Net Core App](../../../examples/AspNetCore/Program.cs)
+directory and run the following command:
+
+```sh
+dotnet run
+```
+
+Once the application is running, navigate to
+[http://localhost:5000/weatherforecast]("http://localhost:5000/weatherforecast")
+from a web browser. You may use the following Powershell script to generate load
+to the application.
+
+```powershell
+while($true)
+{
+ Invoke-WebRequest http://localhost:5000/weatherforecast
+ Start-Sleep -Milliseconds 500
+}
+```
+
+## Use Exemplars to navigate from Metrics to Traces
+
+The application sends metrics (with exemplars), and traces to the OTel
+Collector, which export metrics and traces to Prometheus and Tempo
+respectively.
+
+Please wait for 2 minutes before continuing so that enough data is generated
+and exported.
+
+Open Grafana, select Explore, and select Prometheus as the source. Select the
+metric named "http_server_duration_bucket", and plot the chart. Toggle on the
+"Exemplar" option from the UI and hit refresh.
+
+![Enable Exemplar](https://user-images.githubusercontent.com/16979322/218627781-9886f837-11ae-4d52-94d3-f1821503209c.png)
+
+The Exemplars appear as special "diamond shaped dots" along with the metric
+charts in the UI. Select any Exemplar to see the exemplar data, which includes
+the timestamp when the measurement was recorded, the raw value, and trace
+context when the recording was done. The "trace_id" enables jumping to the
+tracing backed (tempo). Click on the "Query with Tempo" button next to the
+"trace_id" field to open the corresponding `Trace` in Tempo.
+
+![Navigate to trace with exemplar](https://user-images.githubusercontent.com/16979322/218629999-1d1cd6ba-2385-4683-975a-d4797df8361a.png)
+
+## References
+
+* [Exemplar specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#exemplar)
+* [Exemplars in Prometheus](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage)
+* [Exemplars in Grafana](https://grafana.com/docs/grafana/latest/fundamentals/exemplars/)
+* [Tempo](https://github.com/grafana/tempo)
diff --git a/docs/metrics/exemplars/docker-compose.yaml b/docs/metrics/exemplars/docker-compose.yaml
new file mode 100644
index 00000000000..87cd7a6c6d6
--- /dev/null
+++ b/docs/metrics/exemplars/docker-compose.yaml
@@ -0,0 +1,51 @@
+version: "3"
+services:
+
+ # OTEL Collector to receive logs, metrics and traces from the application
+ otel-collector:
+ image: otel/opentelemetry-collector:0.70.0
+ command: [ "--config=/etc/otel-collector.yaml" ]
+ volumes:
+ - ./otel-collector.yaml:/etc/otel-collector.yaml
+ ports:
+ - "4317:4317"
+ - "4318:4318"
+ - "9201:9201"
+
+ # Exports Traces to Tempo
+ tempo:
+ image: grafana/tempo:latest
+ command: [ "-config.file=/etc/tempo.yaml" ]
+ volumes:
+ - ./tempo.yaml:/etc/tempo.yaml
+ - ./tempo-data:/tmp/tempo
+ ports:
+ - "3200" # tempo
+ - "4317" # otlp grpc
+ - "4318" # otlp http
+
+# Exports Metrics to Prometheus
+ prometheus:
+ image: prom/prometheus:latest
+ command:
+ - --config.file=/etc/prometheus.yaml
+ - --web.enable-remote-write-receiver
+ - --enable-feature=exemplar-storage
+ volumes:
+ - ./prometheus.yaml:/etc/prometheus.yaml
+ ports:
+ - "9090:9090"
+
+# UI to query traces and metrics
+ grafana:
+ image: grafana/grafana:9.3.2
+ volumes:
+ - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
+ environment:
+ - GF_AUTH_ANONYMOUS_ENABLED=true
+ - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
+ - GF_AUTH_DISABLE_LOGIN_FORM=true
+ - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor
+ ports:
+ - "3000:3000"
+
diff --git a/docs/metrics/exemplars/grafana-datasources.yaml b/docs/metrics/exemplars/grafana-datasources.yaml
new file mode 100644
index 00000000000..467975584ca
--- /dev/null
+++ b/docs/metrics/exemplars/grafana-datasources.yaml
@@ -0,0 +1,33 @@
+apiVersion: 1
+
+datasources:
+- name: Prometheus
+ type: prometheus
+ uid: prometheus
+ access: proxy
+ orgId: 1
+ url: http://prometheus:9090
+ basicAuth: false
+ isDefault: true
+ version: 1
+ editable: false
+ jsonData:
+ httpMethod: GET
+ exemplarTraceIdDestinations:
+ - name: trace_id
+ datasourceUid: Tempo
+- name: Tempo
+ type: tempo
+ access: proxy
+ orgId: 1
+ url: http://tempo:3200
+ basicAuth: false
+ isDefault: false
+ version: 1
+ editable: false
+ apiVersion: 1
+ uid: tempo
+ jsonData:
+ httpMethod: GET
+ serviceMap:
+ datasourceUid: prometheus
diff --git a/docs/metrics/exemplars/otel-collector.yaml b/docs/metrics/exemplars/otel-collector.yaml
new file mode 100644
index 00000000000..bcf0cb5d6d2
--- /dev/null
+++ b/docs/metrics/exemplars/otel-collector.yaml
@@ -0,0 +1,30 @@
+receivers:
+ otlp:
+ protocols:
+ grpc:
+ http:
+
+exporters:
+ logging:
+ loglevel: debug
+ prometheus:
+ endpoint: ":9201"
+ send_timestamps: true
+ metric_expiration: 180m
+ enable_open_metrics: true
+ otlp:
+ endpoint: tempo:4317
+ tls:
+ insecure: true
+
+service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ exporters: [logging,otlp]
+ metrics:
+ receivers: [otlp]
+ exporters: [logging,prometheus]
+ logs:
+ receivers: [otlp]
+ exporters: [logging]
diff --git a/docs/metrics/exemplars/prometheus.yaml b/docs/metrics/exemplars/prometheus.yaml
new file mode 100644
index 00000000000..4d6c5d18a6f
--- /dev/null
+++ b/docs/metrics/exemplars/prometheus.yaml
@@ -0,0 +1,8 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: 'otel'
+ static_configs:
+ - targets: [ 'otel-collector:9201' ]
diff --git a/docs/metrics/exemplars/tempo.yaml b/docs/metrics/exemplars/tempo.yaml
new file mode 100644
index 00000000000..0d46d4718e8
--- /dev/null
+++ b/docs/metrics/exemplars/tempo.yaml
@@ -0,0 +1,17 @@
+server:
+ http_listen_port: 3200
+
+distributor:
+ receivers:
+ otlp:
+ protocols:
+ http:
+ grpc:
+
+storage:
+ trace:
+ backend: local
+ wal:
+ path: /tmp/tempo/wal
+ local:
+ path: /tmp/tempo/blocks
diff --git a/docs/metrics/extending-the-sdk/README.md b/docs/metrics/extending-the-sdk/README.md
index 0777c8f8f1b..4c409961d86 100644
--- a/docs/metrics/extending-the-sdk/README.md
+++ b/docs/metrics/extending-the-sdk/README.md
@@ -2,7 +2,8 @@
* [Building your own exporter](#exporter)
* [Building your own reader](#reader)
-* [Building your own exemplar](#exemplar)
+* [Building your own exemplar filter](#exemplarfilter)
+* [Building your own exemplar reservoir](#exemplarreservoir)
* [References](#references)
## Exporter
@@ -70,7 +71,48 @@ to the `MeterProvider` as shown in the example [here](./Program.cs).
Not supported.
-## Exemplar
+## ExemplarFilter
+
+OpenTelemetry .NET SDK has provided the following built-in `ExemplarFilter`s:
+
+* [AlwaysOnExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs)
+* [AlwaysOffExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs)
+* [TraceBasedExemplarFilter](../../../src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs)
+
+Custom exemplar filters can be implemented to achieve filtering based on other criterion:
+
+* `ExemplarFilter` should derive from `OpenTelemetry.ExemplarFilter` (which
+ belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package)
+ and implement the `ShouldSample` method.
+
+One example is a filter, which filters all measurements of value lower
+than given threshold is given below. Such a filter prevents any measurements
+below the given threshold from ever becoming a `Exemplar`. Such filters could
+also incorporate the `TraceBasedExemplarFilter` condition as well, as storing
+exemplars for non-sampled traces may be undesired.
+
+```csharp
+public sealed class HighValueFilter : ExemplarFilter
+{
+ private readonly double maxValue;
+
+ public HighValueFilter(double maxValue)
+ {
+ this.maxValue = maxValue;
+ }
+ public override bool ShouldSample(long value, ReadOnlySpan> tags)
+ {
+ return Activity.Current?.Recorded && value > this.maxValue;
+ }
+
+ public override bool ShouldSample(double value, ReadOnlySpan> tags)
+ {
+ return Activity.Current?.Recorded && value > this.maxValue;
+ }
+}
+```
+
+## ExemplarReservoir
Not supported.
diff --git a/docs/trace/advanced/stratified-sampling-example/Program.cs b/docs/trace/advanced/stratified-sampling-example/Program.cs
new file mode 100644
index 00000000000..e85179126e7
--- /dev/null
+++ b/docs/trace/advanced/stratified-sampling-example/Program.cs
@@ -0,0 +1,72 @@
+//
+// 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.Diagnostics;
+using OpenTelemetry;
+using OpenTelemetry.Trace;
+
+namespace StratifiedSamplingByQueryTypeDemo;
+
+internal class Program
+{
+ private static readonly ActivitySource MyActivitySource = new("StratifiedSampling.POC");
+
+ public static void Main(string[] args)
+ {
+ // We wrap the stratified sampler within a parentbased sampler.
+ // This is to enable downstream participants (i.e., the non-root spans) to have
+ // the same consistent sampling decision as the root span (that uses the stratified sampler).
+ // Such downstream participants may not have access to the same attributes that were used to
+ // make the stratified sampling decision at the root.
+ using var tracerProvider = Sdk.CreateTracerProviderBuilder()
+ .SetSampler(new ParentBasedSampler(new StratifiedSampler()))
+ .AddSource("StratifiedSampling.POC")
+ .AddConsoleExporter()
+ .Build();
+
+ var random = new Random(2357);
+ var tagsList = new List>(1);
+
+ // Generate some spans
+ for (var i = 0; i < 20; i++)
+ {
+ // Simulate a mix of user-initiated (25%) and programmatic (75%) queries
+ var randomValue = random.Next(4);
+ switch (randomValue)
+ {
+ case 0:
+ tagsList.Add(new KeyValuePair("queryType", "userInitiated"));
+ break;
+ default:
+ tagsList.Add(new KeyValuePair("queryType", "programmatic"));
+ break;
+ }
+
+ // Note that the queryType attribute here is present as part of the tags list when the activity is started.
+ // We are using this attribute value to achieve stratified sampling.
+ using (var activity = MyActivitySource.StartActivity(ActivityKind.Internal, parentContext: default, tags: tagsList))
+ {
+ activity?.SetTag("foo", "bar");
+ using (var activity2 = MyActivitySource.StartActivity(ActivityKind.Internal, parentContext: default, tags: tagsList))
+ {
+ activity2?.SetTag("foo", "child");
+ }
+ }
+
+ tagsList.Clear();
+ }
+ }
+}
diff --git a/docs/trace/advanced/stratified-sampling-example/README.md b/docs/trace/advanced/stratified-sampling-example/README.md
new file mode 100644
index 00000000000..4b78921102d
--- /dev/null
+++ b/docs/trace/advanced/stratified-sampling-example/README.md
@@ -0,0 +1,105 @@
+# Stratified Sampling: An Example
+
+This example shows one possible way to achieve stratified sampling in
+OpenTelemetry.NET.
+
+## What is stratified sampling?
+
+Stratified sampling is a way to divide a population into mutually exclusive
+sub-populations or "strata". For example, the strata for a population of
+"queries" could be "user-initiated queries" and "programmatic queries". Each
+stratum is then sampled using a probabilistic sampling method. This ensures
+that all sub-populations are represented.
+
+## How does this example do stratified sampling?
+
+We achieve this by using a custom Sampler that internally holds two samplers.
+Based on the stratum, the appropriate sampler is invoked.
+
+One prerequisite for this is that the tag (e.g. queryType) used for the
+stratified sampling decision must be provided as part of activity creation.
+
+We use disproportionate stratified sampling (also known as "unequal probability
+sampling") here - i.e., the sample size of each sub-population is not
+proportionate to their occurrence in the overall population. In this example,
+we want to ensure that all user initiated queries are represented, so we use a
+100% sampling rate for it, while the sampling rate chosen for programmatic
+queries is much lower.
+
+## What is an example output?
+
+You should see the following output on the Console when you use "dotnet run" to
+run this application. This shows that the two sub-populations (strata) are being
+sampled independently.
+
+```text
+StratifiedSampler handling userinitiated query
+Activity.TraceId: 1a122d63e5f8d32cb8ebd3e402eb5389
+Activity.SpanId: 83bdc6bbebea1df8
+Activity.TraceFlags: Recorded
+Activity.ParentSpanId: 1ddd00d845ad645e
+Activity.ActivitySourceName: StratifiedSampling.POC
+Activity.DisplayName: Main
+Activity.Kind: Internal
+Activity.StartTime: 2023-02-09T05:19:30.8156879Z
+Activity.Duration: 00:00:00.0008656
+Activity.Tags:
+ queryType: userInitiated
+ foo: child
+Resource associated with Activity:
+ service.name: unknown_service:Examples.StratifiedSamplingByQueryType
+
+Activity.TraceId: 1a122d63e5f8d32cb8ebd3e402eb5389
+Activity.SpanId: 1ddd00d845ad645e
+Activity.TraceFlags: Recorded
+Activity.ActivitySourceName: StratifiedSampling.POC
+Activity.DisplayName: Main
+Activity.Kind: Internal
+Activity.StartTime: 2023-02-09T05:19:30.8115186Z
+Activity.Duration: 00:00:00.0424036
+Activity.Tags:
+ queryType: userInitiated
+ foo: bar
+Resource associated with Activity:
+ service.name: unknown_service:Examples.StratifiedSamplingByQueryType
+
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+Activity.TraceId: 03cddefbc0e0f61851135f814522a2df
+Activity.SpanId: 8d4fa3e27a12f666
+Activity.TraceFlags: Recorded
+Activity.ParentSpanId: 8c46e4dc6d0f418c
+Activity.ActivitySourceName: StratifiedSampling.POC
+Activity.DisplayName: Main
+Activity.Kind: Internal
+Activity.StartTime: 2023-02-09T05:19:30.8553756Z
+Activity.Duration: 00:00:00.0000019
+Activity.Tags:
+ queryType: programmatic
+ foo: child
+Resource associated with Activity:
+ service.name: unknown_service:Examples.StratifiedSamplingByQueryType
+
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+StratifiedSampler handling programmatic query
+StratifiedSampler handling userinitiated query
+Activity.TraceId: 8a5894524f1bea2a7bd8271fef9ec22d
+Activity.SpanId: 94b5b004287bd678
+Activity.TraceFlags: Recorded
+Activity.ParentSpanId: 99600e9fe011c1cc
+Activity.ActivitySourceName: StratifiedSampling.POC
+Activity.DisplayName: Main
+Activity.Kind: Internal
+Activity.StartTime: 2023-02-09T05:19:30.9660777Z
+Activity.Duration: 00:00:00.0000005
+Activity.Tags:
+ queryType: userInitiated
+ foo: child
+Resource associated with Activity:
+ service.name: unknown_service:Examples.StratifiedSamplingByQueryType
+```
diff --git a/docs/trace/advanced/stratified-sampling-example/StratifiedSampler.cs b/docs/trace/advanced/stratified-sampling-example/StratifiedSampler.cs
new file mode 100644
index 00000000000..16ca4b437ce
--- /dev/null
+++ b/docs/trace/advanced/stratified-sampling-example/StratifiedSampler.cs
@@ -0,0 +1,80 @@
+//
+// 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 OpenTelemetry.Trace;
+
+namespace StratifiedSamplingByQueryTypeDemo;
+
+internal class StratifiedSampler : Sampler
+{
+ // For this POC, we have two groups.
+ // 0 is the group corresponding to user-initiated queries where we want a 100% sampling rate.
+ // 1 is the group corresponding to programmatic queries where we want a lower sampling rate, say 10%
+ private const int NumGroups = 2;
+ private const string QueryTypeTag = "queryType";
+ private const string QueryTypeUserInitiated = "userInitiated";
+ private const string QueryTypeProgrammatic = "programmatic";
+
+ private readonly Dictionary samplingRatios = new();
+ private readonly List samplers = new();
+
+ public StratifiedSampler()
+ {
+ // Initialize sampling ratios for different groups
+ this.samplingRatios[0] = 1.0;
+ this.samplingRatios[1] = 0.2;
+
+ for (var i = 0; i < NumGroups; i++)
+ {
+ this.samplers.Add(new TraceIdRatioBasedSampler(this.samplingRatios[i]));
+ }
+ }
+
+ public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)
+ {
+ if (samplingParameters.Tags != null)
+ {
+ foreach (var tag in samplingParameters.Tags)
+ {
+ if (tag.Key.Equals(QueryTypeTag, StringComparison.OrdinalIgnoreCase))
+ {
+ var queryType = tag.Value as string;
+ if (queryType == null)
+ {
+ continue;
+ }
+
+ if (queryType.Equals(QueryTypeUserInitiated, StringComparison.OrdinalIgnoreCase))
+ {
+ Console.WriteLine($"StratifiedSampler handling userinitiated query");
+ return this.samplers[0].ShouldSample(samplingParameters);
+ }
+ else if (queryType.Equals(QueryTypeProgrammatic, StringComparison.OrdinalIgnoreCase))
+ {
+ Console.WriteLine($"StratifiedSampler handling programmatic query");
+ return this.samplers[1].ShouldSample(samplingParameters);
+ }
+ else
+ {
+ Console.WriteLine("Unexpected query type");
+ }
+ }
+ }
+ }
+
+ return new SamplingResult(SamplingDecision.Drop);
+ }
+}
diff --git a/docs/trace/advanced/stratified-sampling-example/stratified-sampling-example.csproj b/docs/trace/advanced/stratified-sampling-example/stratified-sampling-example.csproj
new file mode 100644
index 00000000000..e089b70bbcf
--- /dev/null
+++ b/docs/trace/advanced/stratified-sampling-example/stratified-sampling-example.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/examples/AspNetCore/Program.cs b/examples/AspNetCore/Program.cs
index 6018cad6090..8835f260fba 100644
--- a/examples/AspNetCore/Program.cs
+++ b/examples/AspNetCore/Program.cs
@@ -106,6 +106,7 @@
// Ensure the MeterProvider subscribes to any custom Meters.
builder
.AddMeter(Instrumentation.MeterName)
+ .SetExemplarFilter(new TraceBasedExemplarFilter())
.AddRuntimeInstrumentation()
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation();
diff --git a/examples/AspNetCore/appsettings.json b/examples/AspNetCore/appsettings.json
index b6756e818df..19f0513276b 100644
--- a/examples/AspNetCore/appsettings.json
+++ b/examples/AspNetCore/appsettings.json
@@ -28,5 +28,15 @@
},
"AspNetCoreInstrumentation": {
"RecordException": "true"
+ },
+ "Kestrel": {
+ "Endpoints": {
+ "Http": {
+ "Url": "http://localhost:5000"
+ },
+ "Https": {
+ "Url": "https://localhost:5001"
+ }
+ }
}
}
diff --git a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
index 0f8702ce863..f1b21474361 100644
--- a/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
+++ b/src/OpenTelemetry.Api.ProviderBuilderExtensions/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md
index fbed09bc483..067fb2eb9f2 100644
--- a/src/OpenTelemetry.Api/CHANGELOG.md
+++ b/src/OpenTelemetry.Api/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs b/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs
index a522cb5f223..7d114db7839 100644
--- a/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs
+++ b/src/OpenTelemetry.Api/Internal/OpenTelemetryApiEventSource.cs
@@ -96,12 +96,6 @@ public void TracestateExtractError(string error)
this.WriteEvent(6, error);
}
- [Event(7, Message = "Calling method '{0}' with invalid argument '{1}', issue '{2}'.", Level = EventLevel.Warning)]
- public void InvalidArgument(string methodName, string argumentName, string issue)
- {
- this.WriteEvent(7, methodName, argumentName, issue);
- }
-
[Event(8, Message = "Failed to extract activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)]
public void FailedToExtractActivityContext(string format, string exception)
{
diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md
index 00cbf436466..66b703d09c1 100644
--- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md
@@ -2,6 +2,13 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
+* Added Exemplar support. See [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars)
+ for instructions to enable exemplars.
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs
index db5ac7dcd1c..f66a34ba756 100644
--- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs
+++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs
@@ -167,6 +167,38 @@ public override ExportResult Export(in Batch batch)
}
}
+ var exemplarString = new StringBuilder();
+ foreach (var exemplar in metricPoint.GetExemplars())
+ {
+ if (exemplar.Timestamp != default)
+ {
+ exemplarString.Append("Value: ");
+ exemplarString.Append(exemplar.DoubleValue);
+ exemplarString.Append(" Timestamp: ");
+ exemplarString.Append(exemplar.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture));
+ exemplarString.Append(" TraceId: ");
+ exemplarString.Append(exemplar.TraceId);
+ exemplarString.Append(" SpanId: ");
+ exemplarString.Append(exemplar.SpanId);
+
+ if (exemplar.FilteredTags != null && exemplar.FilteredTags.Count > 0)
+ {
+ exemplarString.Append(" Filtered Tags : ");
+
+ foreach (var tag in exemplar.FilteredTags)
+ {
+ if (ConsoleTagTransformer.Instance.TryTransformTag(tag, out var result))
+ {
+ exemplarString.Append(result);
+ exemplarString.Append(' ');
+ }
+ }
+ }
+
+ exemplarString.AppendLine();
+ }
+ }
+
msg = new StringBuilder();
msg.Append('(');
msg.Append(metricPoint.StartTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture));
@@ -182,6 +214,14 @@ public override ExportResult Export(in Batch batch)
msg.Append(metric.MetricType);
msg.AppendLine();
msg.Append($"Value: {valueDisplay}");
+
+ if (exemplarString.Length > 0)
+ {
+ msg.AppendLine();
+ msg.AppendLine("Exemplars");
+ msg.Append(exemplarString.ToString());
+ }
+
this.WriteLine(msg.ToString());
}
}
diff --git a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md
index bb7cf65dd01..d8101b31014 100644
--- a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md
index f3735877eeb..cf675a45c3f 100644
--- a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Exporter.Jaeger/README.md b/src/OpenTelemetry.Exporter.Jaeger/README.md
index 643d9874819..3202341a9ea 100644
--- a/src/OpenTelemetry.Exporter.Jaeger/README.md
+++ b/src/OpenTelemetry.Exporter.Jaeger/README.md
@@ -9,10 +9,12 @@ following the [OpenTelemetry specification](https://github.com/open-telemetry/op
The exporter communicates to a Jaeger Agent through the Thrift protocol on
the Compact Thrift API port, and as such only supports Thrift over UDP.
-## Getting Started
-
-Refer to the [Getting Started with
-Jaeger](../../docs/trace/getting-started-jaeger/README.md) tutorial.
+> **Note** This component is scheduled to be
+> [deprecated](https://github.com/open-telemetry/opentelemetry-specification/pull/2858)
+and users are advised to move to [OTLP
+Exporter](../OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md). The
+[getting started with Jaeger](../../docs/trace/getting-started-jaeger/README.md)
+tutorial shows how to use OTLP Exporter to export traces to Jaeger.
## Installation
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md
index 95b6cd57bcd..3b22ef6f908 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0-rc.4
Released 2023-Feb-10
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md
index a9f86178631..7fad8554f47 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md
@@ -2,11 +2,18 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
* Bumped the version of `Google.Protobuf` used by the project to `3.22.0` so
that a new performance feature can be used instead of reflection. Removed the
dependency on `System.Reflection.Emit.Lightweight`.
([#4201](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4201))
+* Added Exemplar support. See [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars)
+ for instructions to enable exemplars.
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs
index 31e3438d8ca..9208eaa3e99 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs
@@ -16,6 +16,7 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
+using Google.Protobuf;
using Google.Protobuf.Collections;
using OpenTelemetry.Metrics;
using OtlpCollector = OpenTelemetry.Proto.Collector.Metrics.V1;
@@ -266,6 +267,40 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric)
}
}
+ var exemplars = metricPoint.GetExemplars();
+ foreach (var examplar in exemplars)
+ {
+ if (examplar.Timestamp != default)
+ {
+ byte[] traceIdBytes = new byte[16];
+ examplar.TraceId?.CopyTo(traceIdBytes);
+
+ byte[] spanIdBytes = new byte[8];
+ examplar.SpanId?.CopyTo(spanIdBytes);
+
+ var otlpExemplar = new OtlpMetrics.Exemplar
+ {
+ TimeUnixNano = (ulong)examplar.Timestamp.ToUnixTimeNanoseconds(),
+ TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes),
+ SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes),
+ AsDouble = examplar.DoubleValue,
+ };
+
+ if (examplar.FilteredTags != null)
+ {
+ foreach (var tag in examplar.FilteredTags)
+ {
+ if (OtlpKeyValueTransformer.Instance.TryTransformTag(tag, out var result))
+ {
+ otlpExemplar.FilteredAttributes.Add(result);
+ }
+ }
+ }
+
+ dataPoint.Exemplars.Add(otlpExemplar);
+ }
+ }
+
histogram.DataPoints.Add(dataPoint);
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
index 7a6c5164d07..a73abbb7f77 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
@@ -25,7 +25,6 @@
-
diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md
index c90d4bffbf3..a7e7b1acf7d 100644
--- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0-rc.4
Released 2023-Feb-10
diff --git a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md
index e6676934961..c93544a74b1 100644
--- a/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md
+++ b/src/OpenTelemetry.Exporter.Prometheus.AspNetCore/README.md
@@ -7,6 +7,14 @@ An [OpenTelemetry Prometheus exporter](https://github.com/open-telemetry/opentel
for configuring an ASP.NET Core application with an endpoint for Prometheus
to scrape.
+> **Note**
+> This exporter does not support [OpenMetrics
+format](https://github.com/OpenObservability/OpenMetrics), and consequently,
+does not support Exemplars. For using Exemplars, use the [OTLP
+Exporter](../OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) and use a
+component like OTel Collector to expose metrics (with exemplars) to Prometheus.
+This [tutorial](../../docs/metrics/exemplars/README.md) shows one way how to do that.
+
## Prerequisite
* [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/)
diff --git a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md
index 50dff5e73e0..fa18c1d9d7c 100644
--- a/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.Prometheus.HttpListener/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0-rc.4
Released 2023-Feb-10
diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md
index a910655e1e3..465de43b2b5 100644
--- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md
+++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md
index 5f0bc6f0601..f7773c88919 100644
--- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md
+++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Extensions.Hosting/README.md b/src/OpenTelemetry.Extensions.Hosting/README.md
index d7e9cedce72..4c2d480a0ec 100644
--- a/src/OpenTelemetry.Extensions.Hosting/README.md
+++ b/src/OpenTelemetry.Extensions.Hosting/README.md
@@ -86,7 +86,7 @@ A fully functional example can be found
## Migrating from pre-release versions of OpenTelemetry.Extensions.Hosting
Pre-release versions (all versions prior to 1.4.0) of
-`OpenTelemetry.Extenions.Hosting` contained signal-specific methods for
+`OpenTelemetry.Extensions.Hosting` contained signal-specific methods for
configuring tracing and metrics:
* `AddOpenTelemetryTracing`: Configure OpenTelemetry and register an
diff --git a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md
index e829d43b811..bdc1f71cff0 100644
--- a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md
+++ b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
index d732ac05166..715a06ff6dd 100644
--- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
+++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+* Fixed an issue of missing `http.client.duration` metric data in case of
+network failures (when response is not available).
+([#4098](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4098))
+
## 1.0.0-rc9.14
Released 2023-Feb-24
diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs
index d8306a84363..8a533f342cf 100644
--- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs
+++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerMetricsDiagnosticListener.cs
@@ -28,6 +28,7 @@ internal sealed class HttpHandlerMetricsDiagnosticListener : ListenerHandler
internal const string OnStopEvent = "System.Net.Http.HttpRequestOut.Stop";
private readonly PropertyFetcher stopResponseFetcher = new("Response");
+ private readonly PropertyFetcher stopRequestFetcher = new("Request");
private readonly Histogram httpClientDuration;
public HttpHandlerMetricsDiagnosticListener(string name, Meter meter)
@@ -46,14 +47,11 @@ public override void OnEventWritten(string name, object payload)
}
var activity = Activity.Current;
- if (this.stopResponseFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null)
+ if (this.stopRequestFetcher.TryFetch(payload, out HttpRequestMessage request) && request != null)
{
- var request = response.RequestMessage;
-
TagList tags = default;
tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpMethod, HttpTagHelper.GetNameForHttpMethod(request.Method)));
tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpScheme, request.RequestUri.Scheme));
- tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, (int)response.StatusCode));
tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.Version)));
tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerName, request.RequestUri.Host));
@@ -62,6 +60,11 @@ public override void OnEventWritten(string name, object payload)
tags.Add(new KeyValuePair(SemanticConventions.AttributeNetPeerPort, request.RequestUri.Port));
}
+ if (this.stopResponseFetcher.TryFetch(payload, out HttpResponseMessage response) && response != null)
+ {
+ tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, (int)response.StatusCode));
+ }
+
// We are relying here on HttpClient library to set duration before writing the stop event.
// https://github.com/dotnet/runtime/blob/90603686d314147017c8bbe1fa8965776ce607d0/src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs#L178
// TODO: Follow up with .NET team if we can continue to rely on this behavior.
diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/README.md b/src/OpenTelemetry.Instrumentation.SqlClient/README.md
index 35351307da4..a2ecc18e33f 100644
--- a/src/OpenTelemetry.Instrumentation.SqlClient/README.md
+++ b/src/OpenTelemetry.Instrumentation.SqlClient/README.md
@@ -11,6 +11,11 @@ and
[System.Data.SqlClient](https://www.nuget.org/packages/System.Data.SqlClient)
and collects traces about database operations.
+> **Warning**
+> Instrumentation is not working with `Microsoft.Data.SqlClient` v3.* due to
+the [issue](https://github.com/dotnet/SqlClient/pull/1258). It was fixed in 4.0
+and later.
+>
> **Note**
> This component is based on the OpenTelemetry semantic conventions for
[traces](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions).
diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt
index e69de29bb2d..955d808767a 100644
--- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt
@@ -0,0 +1,26 @@
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
+OpenTelemetry.Metrics.Exemplar
+OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
+OpenTelemetry.Metrics.Exemplar.Exemplar() -> void
+OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId?
+OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset
+OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId?
+OpenTelemetry.Metrics.ExemplarFilter
+OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void
+OpenTelemetry.Metrics.TraceBasedExemplarFilter
+OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void
+static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder!
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List>
+~OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder!
\ No newline at end of file
diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt
index e69de29bb2d..b334cfe1664 100644
--- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,26 @@
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
+OpenTelemetry.Metrics.Exemplar
+OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
+OpenTelemetry.Metrics.Exemplar.Exemplar() -> void
+OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId?
+OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset
+OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId?
+OpenTelemetry.Metrics.ExemplarFilter
+OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void
+OpenTelemetry.Metrics.TraceBasedExemplarFilter
+OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void
+static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder!
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List>
+~OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder!
diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt
index e69de29bb2d..b334cfe1664 100644
--- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,26 @@
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
+OpenTelemetry.Metrics.Exemplar
+OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
+OpenTelemetry.Metrics.Exemplar.Exemplar() -> void
+OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId?
+OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset
+OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId?
+OpenTelemetry.Metrics.ExemplarFilter
+OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void
+OpenTelemetry.Metrics.TraceBasedExemplarFilter
+OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void
+static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder!
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List>
+~OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder!
diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt
index e69de29bb2d..b334cfe1664 100644
--- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt
+++ b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt
@@ -0,0 +1,26 @@
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter
+OpenTelemetry.Metrics.AlwaysOffExemplarFilter.AlwaysOffExemplarFilter() -> void
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter
+OpenTelemetry.Metrics.AlwaysOnExemplarFilter.AlwaysOnExemplarFilter() -> void
+OpenTelemetry.Metrics.Exemplar
+OpenTelemetry.Metrics.Exemplar.DoubleValue.get -> double
+OpenTelemetry.Metrics.Exemplar.Exemplar() -> void
+OpenTelemetry.Metrics.Exemplar.SpanId.get -> System.Diagnostics.ActivitySpanId?
+OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset
+OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId?
+OpenTelemetry.Metrics.ExemplarFilter
+OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void
+OpenTelemetry.Metrics.TraceBasedExemplarFilter
+OpenTelemetry.Metrics.TraceBasedExemplarFilter.TraceBasedExemplarFilter() -> void
+static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(this OpenTelemetry.Metrics.MeterProviderBuilder! meterProviderBuilder, OpenTelemetry.Metrics.ExemplarFilter! exemplarFilter) -> OpenTelemetry.Metrics.MeterProviderBuilder!
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~abstract OpenTelemetry.Metrics.ExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~OpenTelemetry.Metrics.Exemplar.FilteredTags.get -> System.Collections.Generic.List>
+~OpenTelemetry.Metrics.MetricPoint.GetExemplars() -> OpenTelemetry.Metrics.Exemplar[]
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOffExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.AlwaysOnExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(double value, System.ReadOnlySpan> tags) -> bool
+~override OpenTelemetry.Metrics.TraceBasedExemplarFilter.ShouldSample(long value, System.ReadOnlySpan> tags) -> bool
+OpenTelemetry.Resources.ResourceBuilder.AddDetector(System.Func! resourceDetectorFactory) -> OpenTelemetry.Resources.ResourceBuilder!
diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md
index a213f19fa68..bb9e2e1714f 100644
--- a/src/OpenTelemetry/CHANGELOG.md
+++ b/src/OpenTelemetry/CHANGELOG.md
@@ -2,6 +2,16 @@
## Unreleased
+## 1.5.0-alpha.1
+
+Released 2023-Mar-07
+
+* Added Exemplar support. See [exemplars](../../docs/metrics/customizing-the-sdk/README.md#exemplars)
+ for instructions to enable exemplars.
+
+* Added `AddDetector` factory overload on `ResourceBuilder`.
+ ([#4261](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4261))
+
## 1.4.0
Released 2023-Feb-24
diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs
index 848737de80f..9f4c02c77f7 100644
--- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs
+++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs
@@ -16,7 +16,6 @@
using System.Diagnostics;
using System.Diagnostics.Tracing;
-using System.Security;
namespace OpenTelemetry.Internal
{
@@ -77,24 +76,6 @@ public void MetricReaderException(string methodName, Exception ex)
}
}
- [NonEvent]
- public void TracestateKeyIsInvalid(ReadOnlySpan key)
- {
- if (this.IsEnabled(EventLevel.Warning, EventKeywords.All))
- {
- this.TracestateKeyIsInvalid(key.ToString());
- }
- }
-
- [NonEvent]
- public void TracestateValueIsInvalid(ReadOnlySpan value)
- {
- if (this.IsEnabled(EventLevel.Warning, EventKeywords.All))
- {
- this.TracestateValueIsInvalid(value.ToString());
- }
- }
-
[NonEvent]
public void ActivityStarted(Activity activity)
{
@@ -107,7 +88,7 @@ public void ActivityStarted(Activity activity)
// https://github.com/dotnet/runtime/issues/61857
var activityId = string.Concat("00-", activity.TraceId.ToHexString(), "-", activity.SpanId.ToHexString());
activityId = string.Concat(activityId, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "-01" : "-00");
- this.ActivityStarted(activity.OperationName, activityId);
+ this.ActivityStarted(activity.DisplayName, activityId);
}
}
@@ -116,7 +97,7 @@ public void ActivityStopped(Activity activity)
{
if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All))
{
- this.ActivityStopped(activity.OperationName, activity.Id);
+ this.ActivityStopped(activity.DisplayName, activity.Id);
}
}
@@ -147,15 +128,6 @@ public void MeterProviderException(string methodName, Exception ex)
}
}
- [NonEvent]
- public void MissingPermissionsToReadEnvironmentVariable(SecurityException ex)
- {
- if (this.IsEnabled(EventLevel.Warning, EventKeywords.All))
- {
- this.MissingPermissionsToReadEnvironmentVariable(ex.ToInvariantString());
- }
- }
-
[NonEvent]
public void DroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount)
{
@@ -175,18 +147,6 @@ public void DroppedExportProcessorItems(string exportProcessorName, string expor
}
}
- [Event(1, Message = "Span processor queue size reached maximum. Throttling spans.", Level = EventLevel.Warning)]
- public void SpanProcessorQueueIsExhausted()
- {
- this.WriteEvent(1);
- }
-
- [Event(2, Message = "Shutdown complete. '{0}' spans left in queue unprocessed.", Level = EventLevel.Informational)]
- public void ShutdownEvent(int spansLeftUnprocessed)
- {
- this.WriteEvent(2, spansLeftUnprocessed);
- }
-
[Event(3, Message = "Exporter returned error '{0}'.", Level = EventLevel.Warning)]
public void ExporterErrorResult(ExportResult exportResult)
{
@@ -199,94 +159,34 @@ public void SpanProcessorException(string evnt, string ex)
this.WriteEvent(4, evnt, ex);
}
- [Event(5, Message = "Calling '{0}' on ended span.", Level = EventLevel.Warning)]
- public void UnexpectedCallOnEndedSpan(string methodName)
- {
- this.WriteEvent(5, methodName);
- }
-
- [Event(6, Message = "Attempting to dispose scope '{0}' that is not current", Level = EventLevel.Warning)]
- public void AttemptToEndScopeWhichIsNotCurrent(string spanName)
- {
- this.WriteEvent(6, spanName);
- }
-
- [Event(7, Message = "Attempting to activate span: '{0}'", Level = EventLevel.Informational)]
- public void AttemptToActivateActiveSpan(string spanName)
- {
- this.WriteEvent(7, spanName);
- }
-
[Event(8, Message = "Calling method '{0}' with invalid argument '{1}', issue '{2}'.", Level = EventLevel.Warning)]
public void InvalidArgument(string methodName, string argumentName, string issue)
{
this.WriteEvent(8, methodName, argumentName, issue);
}
- [Event(10, Message = "Failed to inject activity context in format: '{0}', context: '{1}'.", Level = EventLevel.Warning)]
- public void FailedToInjectActivityContext(string format, string error)
- {
- this.WriteEvent(10, format, error);
- }
-
- [Event(11, Message = "Failed to parse tracestate: too many items", Level = EventLevel.Warning)]
- public void TooManyItemsInTracestate()
- {
- this.WriteEvent(11);
- }
-
- [Event(12, Message = "Tracestate key is invalid, key = '{0}'", Level = EventLevel.Warning)]
- public void TracestateKeyIsInvalid(string key)
- {
- this.WriteEvent(12, key);
- }
-
- [Event(13, Message = "Tracestate value is invalid, value = '{0}'", Level = EventLevel.Warning)]
- public void TracestateValueIsInvalid(string value)
- {
- this.WriteEvent(13, value);
- }
-
[Event(14, Message = "Tracestate parse error: '{0}'", Level = EventLevel.Warning)]
public void TracestateExtractError(string error)
{
this.WriteEvent(14, error);
}
- [Event(15, Message = "Attempting to activate out-of-band span '{0}'", Level = EventLevel.Warning)]
- public void AttemptToActivateOobSpan(string spanName)
- {
- this.WriteEvent(15, spanName);
- }
-
[Event(16, Message = "Exception occurred while invoking Observable instrument callback. Exception: '{0}'", Level = EventLevel.Warning)]
public void ObservableInstrumentCallbackException(string exception)
{
this.WriteEvent(16, exception);
}
- [Event(22, Message = "ForceFlush complete. '{0}' spans left in queue unprocessed.", Level = EventLevel.Informational)]
- public void ForceFlushCompleted(int spansLeftUnprocessed)
- {
- this.WriteEvent(22, spansLeftUnprocessed);
- }
-
- [Event(23, Message = "Timeout reached waiting on SpanExporter. '{0}' spans attempted.", Level = EventLevel.Warning)]
- public void SpanExporterTimeout(int spansAttempted)
+ [Event(24, Message = "Activity started. Name = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)]
+ public void ActivityStarted(string name, string id)
{
- this.WriteEvent(23, spansAttempted);
+ this.WriteEvent(24, name, id);
}
- [Event(24, Message = "Activity started. OperationName = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)]
- public void ActivityStarted(string operationName, string id)
+ [Event(25, Message = "Activity stopped. Name = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)]
+ public void ActivityStopped(string name, string id)
{
- this.WriteEvent(24, operationName, id);
- }
-
- [Event(25, Message = "Activity stopped. OperationName = '{0}', Id = '{1}'.", Level = EventLevel.Verbose)]
- public void ActivityStopped(string operationName, string id)
- {
- this.WriteEvent(25, operationName, id);
+ this.WriteEvent(25, name, id);
}
[Event(26, Message = "Failed to create file. LogDirectory ='{0}', Id = '{1}'.", Level = EventLevel.Warning)]
@@ -301,18 +201,6 @@ public void TracerProviderException(string evnt, string ex)
this.WriteEvent(28, evnt, ex);
}
- [Event(29, Message = "Failed to parse environment variable: '{0}', value: '{1}'.", Level = EventLevel.Warning)]
- public void FailedToParseEnvironmentVariable(string name, string value)
- {
- this.WriteEvent(29, name, value);
- }
-
- [Event(30, Message = "Missing permissions to read environment variable: '{0}'", Level = EventLevel.Warning)]
- public void MissingPermissionsToReadEnvironmentVariable(string exception)
- {
- this.WriteEvent(30, exception);
- }
-
[Event(31, Message = "'{0}' exporting to '{1}' dropped '0' items.", Level = EventLevel.Informational)]
public void NoDroppedExportProcessorItems(string exportProcessorName, string exporterName)
{
diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs b/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs
index b2e6e2311e7..e46e2851c95 100644
--- a/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs
+++ b/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs
@@ -80,6 +80,8 @@ internal OpenTelemetryLoggerProvider(OpenTelemetryLoggerOptions options, IServic
Guard.ThrowIfNull(options);
+ this.ServiceProvider = serviceProvider;
+
this.IncludeScopes = options.IncludeScopes;
this.IncludeFormattedMessage = options.IncludeFormattedMessage;
this.ParseStateValues = options.ParseStateValues;
@@ -96,6 +98,8 @@ internal OpenTelemetryLoggerProvider(OpenTelemetryLoggerOptions options, IServic
OpenTelemetrySdkEventSource.Log.OpenTelemetryLoggerProviderEvent("OpenTelemetryLoggerProvider built successfully.");
}
+ internal IServiceProvider? ServiceProvider { get; }
+
internal IExternalScopeProvider? ScopeProvider { get; private set; }
internal ILogRecordPool LogRecordPool => this.threadStaticPool ?? LogRecordSharedPool.Current;
diff --git a/src/OpenTelemetry/Metrics/AggregationType.cs b/src/OpenTelemetry/Metrics/AggregationType.cs
index 0c4dec418f2..c1b550c1538 100644
--- a/src/OpenTelemetry/Metrics/AggregationType.cs
+++ b/src/OpenTelemetry/Metrics/AggregationType.cs
@@ -72,5 +72,15 @@ internal enum AggregationType
/// Histogram with sum, count, min, max.
///
HistogramWithMinMax = 9,
+
+ ///
+ /// Exponential Histogram with sum, count.
+ ///
+ Base2ExponentialHistogram = 10,
+
+ ///
+ /// Exponential Histogram with sum, count, min, max.
+ ///
+ Base2ExponentialHistogramWithMinMax = 11,
}
}
diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs
index 5b8909ef836..7f2732d3c0c 100644
--- a/src/OpenTelemetry/Metrics/AggregatorStore.cs
+++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs
@@ -38,9 +38,11 @@ internal sealed class AggregatorStore
private readonly int[] currentMetricPointBatch;
private readonly AggregationType aggType;
private readonly double[] histogramBounds;
+ private readonly int exponentialHistogramMaxBuckets;
private readonly UpdateLongDelegate updateLongCallback;
private readonly UpdateDoubleDelegate updateDoubleCallback;
private readonly int maxMetricPoints;
+ private readonly ExemplarFilter exemplarFilter;
private int metricPointIndex = 0;
private int batchSize = 0;
private int metricCapHitMessageLogged;
@@ -52,7 +54,9 @@ internal AggregatorStore(
AggregationTemporality temporality,
int maxMetricPoints,
double[] histogramBounds,
- string[] tagKeysInteresting = null)
+ int exponentialHistogramMaxBuckets,
+ string[] tagKeysInteresting = null,
+ ExemplarFilter exemplarFilter = null)
{
this.name = name;
this.maxMetricPoints = maxMetricPoints;
@@ -62,7 +66,9 @@ internal AggregatorStore(
this.aggType = aggType;
this.outputDelta = temporality == AggregationTemporality.Delta;
this.histogramBounds = histogramBounds;
+ this.exponentialHistogramMaxBuckets = exponentialHistogramMaxBuckets;
this.StartTimeExclusive = DateTimeOffset.UtcNow;
+ this.exemplarFilter = exemplarFilter ?? new AlwaysOffExemplarFilter();
if (tagKeysInteresting == null)
{
this.updateLongCallback = this.UpdateLong;
@@ -86,6 +92,13 @@ internal AggregatorStore(
internal DateTimeOffset EndTimeInclusive { get; private set; }
+ internal bool IsExemplarEnabled()
+ {
+ // Using this filter to indicate On/Off
+ // instead of another separate flag.
+ return this.exemplarFilter is not AlwaysOffExemplarFilter;
+ }
+
internal void Update(long value, ReadOnlySpan> tags)
{
this.updateLongCallback(value, tags);
@@ -123,7 +136,15 @@ internal void SnapshotDelta(int indexSnapshot)
continue;
}
- metricPoint.TakeSnapshot(outputDelta: true);
+ if (this.IsExemplarEnabled())
+ {
+ metricPoint.TakeSnapshotWithExemplar(outputDelta: true);
+ }
+ else
+ {
+ metricPoint.TakeSnapshot(outputDelta: true);
+ }
+
this.currentMetricPointBatch[this.batchSize] = i;
this.batchSize++;
}
@@ -144,7 +165,15 @@ internal void SnapshotCumulative(int indexSnapshot)
continue;
}
- metricPoint.TakeSnapshot(outputDelta: false);
+ if (this.IsExemplarEnabled())
+ {
+ metricPoint.TakeSnapshotWithExemplar(outputDelta: false);
+ }
+ else
+ {
+ metricPoint.TakeSnapshot(outputDelta: false);
+ }
+
this.currentMetricPointBatch[this.batchSize] = i;
this.batchSize++;
}
@@ -162,7 +191,7 @@ private void InitializeZeroTagPointIfNotInitialized()
{
if (!this.zeroTagMetricPointInitialized)
{
- this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds);
+ this.metricPoints[0] = new MetricPoint(this, this.aggType, null, this.histogramBounds, this.exponentialHistogramMaxBuckets);
this.zeroTagMetricPointInitialized = true;
}
}
@@ -229,7 +258,7 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValue
}
ref var metricPoint = ref this.metricPoints[aggregatorIndex];
- metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds);
+ metricPoint = new MetricPoint(this, this.aggType, sortedTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxBuckets);
// Add to dictionary *after* initializing MetricPoint
// as other threads can start writing to the
@@ -278,7 +307,7 @@ private int LookupAggregatorStore(KeyValuePair[] tagKeysAndValue
}
ref var metricPoint = ref this.metricPoints[aggregatorIndex];
- metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds);
+ metricPoint = new MetricPoint(this, this.aggType, givenTags.KeyValuePairs, this.histogramBounds, this.exponentialHistogramMaxBuckets);
// Add to dictionary *after* initializing MetricPoint
// as other threads can start writing to the
@@ -309,7 +338,16 @@ private void UpdateLong(long value, ReadOnlySpan> t
return;
}
- this.metricPoints[index].Update(value);
+ // TODO: can special case built-in filters to be bit faster.
+ if (this.IsExemplarEnabled())
+ {
+ var shouldSample = this.exemplarFilter.ShouldSample(value, tags);
+ this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample);
+ }
+ else
+ {
+ this.metricPoints[index].Update(value);
+ }
}
catch (Exception)
{
@@ -332,7 +370,16 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan
internal sealed class Base2ExponentialBucketHistogram
{
+ internal double RunningSum;
+ internal double SnapshotSum;
+
+ internal double RunningMin = double.PositiveInfinity;
+ internal double SnapshotMin;
+
+ internal double RunningMax = double.NegativeInfinity;
+ internal double SnapshotMax;
+
+ internal int IsCriticalSectionOccupied = 0;
+
private int scale;
private double scalingFactor; // 2 ^ scale / log(2)
diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs
index 12f46b18208..21763741efe 100644
--- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs
+++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs
@@ -303,6 +303,28 @@ public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder m
return meterProviderBuilder;
}
+ ///
+ /// Sets the to be used for this provider.
+ /// This is applied to all the metrics from this provider.
+ ///
+ /// .
+ /// ExemplarFilter to use.
+ /// The supplied for chaining.
+ public static MeterProviderBuilder SetExemplarFilter(this MeterProviderBuilder meterProviderBuilder, ExemplarFilter exemplarFilter)
+ {
+ Guard.ThrowIfNull(exemplarFilter);
+
+ meterProviderBuilder.ConfigureBuilder((sp, builder) =>
+ {
+ if (builder is MeterProviderBuilderSdk meterProviderBuilderSdk)
+ {
+ meterProviderBuilderSdk.SetExemplarFilter(exemplarFilter);
+ }
+ });
+
+ return meterProviderBuilder;
+ }
+
///
/// Run the given actions to initialize the .
///
diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs
index ba53a29460b..da4571184ae 100644
--- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs
+++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs
@@ -54,6 +54,8 @@ public MeterProviderBuilderSdk(IServiceProvider serviceProvider)
public ResourceBuilder? ResourceBuilder { get; private set; }
+ public ExemplarFilter? ExemplarFilter { get; private set; }
+
public MeterProvider? Provider => this.meterProvider;
public List Readers { get; } = new();
@@ -160,6 +162,15 @@ public MeterProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder)
return this;
}
+ public MeterProviderBuilder SetExemplarFilter(ExemplarFilter exemplarFilter)
+ {
+ Debug.Assert(exemplarFilter != null, "exemplarFilter was null");
+
+ this.ExemplarFilter = exemplarFilter;
+
+ return this;
+ }
+
public override MeterProviderBuilder AddMeter(params string[] names)
{
Debug.Assert(names != null, "names was null");
diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs
new file mode 100644
index 00000000000..73090ce9949
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs
@@ -0,0 +1,114 @@
+//
+// 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.Diagnostics;
+
+namespace OpenTelemetry.Metrics;
+
+///
+/// The AlignedHistogramBucketExemplarReservoir implementation.
+///
+internal sealed class AlignedHistogramBucketExemplarReservoir : ExemplarReservoir
+{
+ private readonly int length;
+ private readonly Exemplar[] runningExemplars;
+ private readonly Exemplar[] tempExemplars;
+
+ public AlignedHistogramBucketExemplarReservoir(int length)
+ {
+ this.length = length;
+ this.runningExemplars = new Exemplar[length + 1];
+ this.tempExemplars = new Exemplar[length + 1];
+ }
+
+ public override void Offer(long value, ReadOnlySpan> tags, int index = default)
+ {
+ this.OfferAtBoundary(value, tags, index);
+ }
+
+ public override void Offer(double value, ReadOnlySpan> tags, int index = default)
+ {
+ this.OfferAtBoundary(value, tags, index);
+ }
+
+ public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset)
+ {
+ for (int i = 0; i < this.runningExemplars.Length; i++)
+ {
+ this.tempExemplars[i] = this.runningExemplars[i];
+ if (this.runningExemplars[i].FilteredTags != null)
+ {
+ // TODO: Better data structure to avoid this Linq.
+ // This is doing filtered = alltags - storedtags.
+ // TODO: At this stage, this logic is done inside Reservoir.
+ // Kinda hard for end users who write own reservoirs.
+ // Evaluate if this logic can be moved elsewhere.
+ // TODO: The cost is paid irrespective of whether the
+ // Exporter supports Exemplar or not. One idea is to
+ // defer this until first exporter attempts read.
+ this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags.Except(actualTags.KeyAndValues.ToList()).ToList();
+ }
+
+ if (reset)
+ {
+ this.runningExemplars[i].Timestamp = default;
+ }
+ }
+
+ return this.tempExemplars;
+ }
+
+ private void OfferAtBoundary(double value, ReadOnlySpan> tags, int index)
+ {
+ ref var exemplar = ref this.runningExemplars[index];
+ exemplar.Timestamp = DateTimeOffset.UtcNow;
+ exemplar.DoubleValue = value;
+ exemplar.TraceId = Activity.Current?.TraceId;
+ exemplar.SpanId = Activity.Current?.SpanId;
+
+ if (tags == default)
+ {
+ // default tag is used to indicate
+ // the special case where all tags provided at measurement
+ // recording time are stored.
+ // In this case, Exemplars does not have to store any tags.
+ // In other words, FilteredTags will be empty.
+ return;
+ }
+
+ if (exemplar.FilteredTags == null)
+ {
+ exemplar.FilteredTags = new List>(tags.Length);
+ }
+ else
+ {
+ // Keep the list, but clear contents.
+ exemplar.FilteredTags.Clear();
+ }
+
+ // Though only those tags that are filtered need to be
+ // stored, finding filtered list from the full tag list
+ // is expensive. So all the tags are stored in hot path (this).
+ // During snapshot, the filtered list is calculated.
+ // TODO: Evaluate alternative approaches based on perf.
+ // TODO: This is not user friendly to Reservoir authors
+ // and must be handled as transparently as feasible.
+ foreach (var tag in tags)
+ {
+ exemplar.FilteredTags.Add(tag);
+ }
+ }
+}
diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs
new file mode 100644
index 00000000000..2d3f3cab898
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs
@@ -0,0 +1,34 @@
+//
+// 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.
+//
+
+namespace OpenTelemetry.Metrics;
+
+///
+/// An ExemplarFilter which makes no measurements eligible for being an Exemplar.
+/// Using this ExemplarFilter is as good as disabling Exemplar feature.
+///
+public sealed class AlwaysOffExemplarFilter : ExemplarFilter
+{
+ public override bool ShouldSample(long value, ReadOnlySpan> tags)
+ {
+ return false;
+ }
+
+ public override bool ShouldSample(double value, ReadOnlySpan> tags)
+ {
+ return false;
+ }
+}
diff --git a/test/Benchmarks/Helper/ValuesController.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs
similarity index 53%
rename from test/Benchmarks/Helper/ValuesController.cs
rename to src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs
index 41da05856f1..79adb9eeba3 100644
--- a/test/Benchmarks/Helper/ValuesController.cs
+++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs
@@ -1,4 +1,4 @@
-//
+//
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,21 +14,20 @@
// limitations under the License.
//
-#if !NETFRAMEWORK
-using System.Collections.Generic;
-using Microsoft.AspNetCore.Mvc;
+namespace OpenTelemetry.Metrics;
-namespace Benchmark.Helper
+///
+/// An ExemplarFilter which makes all measurements eligible for being an Exemplar.
+///
+public sealed class AlwaysOnExemplarFilter : ExemplarFilter
{
- [Route("api/[controller]")]
- public class ValuesController : Controller
+ public override bool ShouldSample(long value, ReadOnlySpan> tags)
{
- // GET api/values
- [HttpGet]
- public IEnumerable Get()
- {
- return new string[] { "value1", "value2" };
- }
+ return true;
+ }
+
+ public override bool ShouldSample(double value, ReadOnlySpan> tags)
+ {
+ return true;
}
}
-#endif
diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs
new file mode 100644
index 00000000000..32f3c2dab2a
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs
@@ -0,0 +1,54 @@
+//
+// 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.Diagnostics;
+
+namespace OpenTelemetry.Metrics
+{
+ ///
+ /// Represents an Exemplar data.
+ ///
+ public struct Exemplar
+ {
+ ///
+ /// Gets the timestamp (UTC).
+ ///
+ public DateTimeOffset Timestamp { get; internal set; }
+
+ ///
+ /// Gets the TraceId.
+ ///
+ public ActivityTraceId? TraceId { get; internal set; }
+
+ ///
+ /// Gets the SpanId.
+ ///
+ public ActivitySpanId? SpanId { get; internal set; }
+
+ // TODO: Leverage MetricPointValueStorage
+ // and allow double/long instead of double only.
+
+ ///
+ /// Gets the double value.
+ ///
+ public double DoubleValue { get; internal set; }
+
+ ///
+ /// Gets the FilteredTags (i.e any tags that were dropped during aggregation).
+ ///
+ public List> FilteredTags { get; internal set; }
+ }
+}
diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs
new file mode 100644
index 00000000000..9e79570ce77
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs
@@ -0,0 +1,58 @@
+//
+// 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.
+//
+namespace OpenTelemetry.Metrics;
+
+///
+/// The base class for defining Exemplar Filter.
+///
+public abstract class ExemplarFilter
+{
+ ///
+ /// Determines if a given measurement is eligible for being
+ /// considered for becoming Exemplar.
+ ///
+ /// The value of the measurement.
+ /// The complete set of tags provided with the measurement.
+ ///
+ /// Returns
+ /// true to indicate this measurement is eligible to become Exemplar
+ /// and will be given to an ExemplarReservoir.
+ /// Reservoir may further sample, so a true here does not mean that this
+ /// measurement will become an exemplar, it just means it'll be
+ /// eligible for being Exemplar.
+ /// false to indicate this measurement is not eligible to become Exemplar
+ /// and will not be given to the ExemplarReservoir.
+ ///
+ public abstract bool ShouldSample(long value, ReadOnlySpan> tags);
+
+ ///
+ /// Determines if a given measurement is eligible for being
+ /// considered for becoming Exemplar.
+ ///
+ /// The value of the measurement.
+ /// The complete set of tags provided with the measurement.
+ ///
+ /// Returns
+ /// true to indicate this measurement is eligible to become Exemplar
+ /// and will be given to an ExemplarReservoir.
+ /// Reservoir may further sample, so a true here does not mean that this
+ /// measurement will become an exemplar, it just means it'll be
+ /// eligible for being Exemplar.
+ /// false to indicate this measurement is not eligible to become Exemplar
+ /// and will not be given to the ExemplarReservoir.
+ ///
+ public abstract bool ShouldSample(double value, ReadOnlySpan> tags);
+}
diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs
new file mode 100644
index 00000000000..59b530e9d56
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs
@@ -0,0 +1,52 @@
+//
+// 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.
+//
+namespace OpenTelemetry.Metrics;
+
+///
+/// The base class for defining Exemplar Reservoir.
+///
+internal abstract class ExemplarReservoir
+{
+ ///
+ /// Offers measurement to the reservoir.
+ ///
+ /// The value of the measurement.
+ /// The complete set of tags provided with the measurement.
+ /// The histogram bucket index where this measurement is going to be stored.
+ /// This is optional and is only relevant for Histogram with buckets.
+ public abstract void Offer(long value, ReadOnlySpan> tags, int index = default);
+
+ ///
+ /// Offers measurement to the reservoir.
+ ///
+ /// The value of the measurement.
+ /// The complete set of tags provided with the measurement.
+ /// The histogram bucket index where this measurement is going to be stored.
+ /// This is optional and is only relevant for Histogram with buckets.
+ public abstract void Offer(double value, ReadOnlySpan> tags, int index = default);
+
+ ///
+ /// Collects all the exemplars accumulated by the Reservoir.
+ ///
+ /// The actual tags that are part of the metric. Exemplars are
+ /// only expected to contain any filtered tags, so this will allow the reservoir
+ /// to prepare the filtered tags from all the tags it is given by doing the
+ /// equivalent of filtered tags = all tags - actual tags.
+ ///
+ /// Flag to indicate if the reservoir should be reset after this call.
+ /// Array of Exemplars.
+ public abstract Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset);
+}
diff --git a/src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs
new file mode 100644
index 00000000000..c42efee38be
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs
@@ -0,0 +1,149 @@
+//
+// 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.Diagnostics;
+
+namespace OpenTelemetry.Metrics;
+
+///
+/// The SimpleExemplarReservoir implementation.
+///
+internal sealed class SimpleExemplarReservoir : ExemplarReservoir
+{
+ private readonly int poolSize;
+ private readonly Random random;
+ private readonly Exemplar[] runningExemplars;
+ private readonly Exemplar[] tempExemplars;
+
+ private long measurementsSeen;
+
+ public SimpleExemplarReservoir(int poolSize)
+ {
+ this.poolSize = poolSize;
+ this.runningExemplars = new Exemplar[poolSize];
+ this.tempExemplars = new Exemplar[poolSize];
+ this.measurementsSeen = 0;
+ this.random = new Random();
+ }
+
+ public override void Offer(long value, ReadOnlySpan> tags, int index = default)
+ {
+ this.Offer(value, tags);
+ }
+
+ public override void Offer(double value, ReadOnlySpan> tags, int index = default)
+ {
+ this.Offer(value, tags);
+ }
+
+ public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset)
+ {
+ for (int i = 0; i < this.runningExemplars.Length; i++)
+ {
+ this.tempExemplars[i] = this.runningExemplars[i];
+ if (this.runningExemplars[i].FilteredTags != null)
+ {
+ // TODO: Better data structure to avoid this Linq.
+ // This is doing filtered = alltags - storedtags.
+ // TODO: At this stage, this logic is done inside Reservoir.
+ // Kinda hard for end users who write own reservoirs.
+ // Evaluate if this logic can be moved elsewhere.
+ // TODO: The cost is paid irrespective of whether the
+ // Exporter supports Exemplar or not. One idea is to
+ // defer this until first exporter attempts read.
+ this.tempExemplars[i].FilteredTags = this.runningExemplars[i].FilteredTags.Except(actualTags.KeyAndValues.ToList()).ToList();
+ }
+
+ if (reset)
+ {
+ this.runningExemplars[i].Timestamp = default;
+ this.measurementsSeen = 0;
+ }
+ }
+
+ return this.tempExemplars;
+ }
+
+ private void Offer(double value, ReadOnlySpan> tags)
+ {
+ if (this.measurementsSeen < this.poolSize)
+ {
+ ref var exemplar = ref this.runningExemplars[this.measurementsSeen];
+ exemplar.Timestamp = DateTimeOffset.UtcNow;
+ exemplar.DoubleValue = value;
+ exemplar.TraceId = Activity.Current?.TraceId;
+ exemplar.SpanId = Activity.Current?.SpanId;
+ this.StoreTags(ref exemplar, tags);
+ }
+ else
+ {
+ // TODO: RandomNext64 is only available in .NET 6 or newer.
+ int upperBound = 0;
+ unchecked
+ {
+ upperBound = (int)this.measurementsSeen;
+ }
+
+ var index = this.random.Next(0, upperBound);
+ if (index < this.poolSize)
+ {
+ ref var exemplar = ref this.runningExemplars[index];
+ exemplar.Timestamp = DateTimeOffset.UtcNow;
+ exemplar.DoubleValue = value;
+ exemplar.TraceId = Activity.Current?.TraceId;
+ exemplar.SpanId = Activity.Current?.SpanId;
+ this.StoreTags(ref exemplar, tags);
+ }
+ }
+
+ this.measurementsSeen++;
+ }
+
+ private void StoreTags(ref Exemplar exemplar, ReadOnlySpan> tags)
+ {
+ if (tags == default)
+ {
+ // default tag is used to indicate
+ // the special case where all tags provided at measurement
+ // recording time are stored.
+ // In this case, Exemplars does not have to store any tags.
+ // In other words, FilteredTags will be empty.
+ return;
+ }
+
+ if (exemplar.FilteredTags == null)
+ {
+ exemplar.FilteredTags = new List>(tags.Length);
+ }
+ else
+ {
+ // Keep the list, but clear contents.
+ exemplar.FilteredTags.Clear();
+ }
+
+ // Though only those tags that are filtered need to be
+ // stored, finding filtered list from the full tag list
+ // is expensive. So all the tags are stored in hot path (this).
+ // During snapshot, the filtered list is calculated.
+ // TODO: Evaluate alternative approaches based on perf.
+ // TODO: This is not user friendly to Reservoir authors
+ // and must be handled as transparently as feasible.
+ foreach (var tag in tags)
+ {
+ exemplar.FilteredTags.Add(tag);
+ }
+ }
+}
diff --git a/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs
new file mode 100644
index 00000000000..69c4c29e9f8
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs
@@ -0,0 +1,36 @@
+//
+// 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.Diagnostics;
+
+namespace OpenTelemetry.Metrics;
+
+///
+/// An ExemplarFilter which makes those measurements eligible for being an Exemplar,
+/// which are recorded in the context of a sampled parent activity (span).
+///
+public sealed class TraceBasedExemplarFilter : ExemplarFilter
+{
+ public override bool ShouldSample(long value, ReadOnlySpan> tags)
+ {
+ return Activity.Current?.Recorded ?? false;
+ }
+
+ public override bool ShouldSample(double value, ReadOnlySpan> tags)
+ {
+ return Activity.Current?.Recorded ?? false;
+ }
+}
diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs
index d02c34865af..cffcf80a436 100644
--- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs
+++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs
@@ -80,6 +80,7 @@ internal MeterProviderSdk(
reader.SetParentProvider(this);
reader.SetMaxMetricStreams(state.MaxMetricStreams);
reader.SetMaxMetricPointsPerMetricStream(state.MaxMetricPointsPerMetricStream);
+ reader.SetExemplarFilter(state.ExemplarFilter);
if (this.reader == null)
{
diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs
index 63338d81c2e..d276070849b 100644
--- a/src/OpenTelemetry/Metrics/Metric.cs
+++ b/src/OpenTelemetry/Metrics/Metric.cs
@@ -23,6 +23,8 @@ namespace OpenTelemetry.Metrics
///
public sealed class Metric
{
+ internal const int DefaultExponentialHistogramMaxBuckets = 160;
+
internal static readonly double[] DefaultHistogramBounds = new double[] { 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 };
private readonly AggregatorStore aggStore;
@@ -33,7 +35,8 @@ internal Metric(
int maxMetricPointsPerMetricStream,
double[] histogramBounds = null,
string[] tagKeysInteresting = null,
- bool histogramRecordMinMax = true)
+ bool histogramRecordMinMax = true,
+ ExemplarFilter exemplarFilter = null)
{
this.InstrumentIdentity = instrumentIdentity;
@@ -126,7 +129,7 @@ internal Metric(
throw new NotSupportedException($"Unsupported Instrument Type: {instrumentIdentity.InstrumentType.FullName}");
}
- this.aggStore = new AggregatorStore(instrumentIdentity.InstrumentName, aggType, temporality, maxMetricPointsPerMetricStream, histogramBounds ?? DefaultHistogramBounds, tagKeysInteresting);
+ this.aggStore = new AggregatorStore(instrumentIdentity.InstrumentName, aggType, temporality, maxMetricPointsPerMetricStream, histogramBounds ?? DefaultHistogramBounds, DefaultExponentialHistogramMaxBuckets, tagKeysInteresting, exemplarFilter);
this.Temporality = temporality;
this.InstrumentDisposed = false;
}
diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs
index 8846ed86f3a..e4ce42c0799 100644
--- a/src/OpenTelemetry/Metrics/MetricPoint.cs
+++ b/src/OpenTelemetry/Metrics/MetricPoint.cs
@@ -24,11 +24,14 @@ namespace OpenTelemetry.Metrics
///
public struct MetricPoint
{
+ // TODO: Ask spec to define a default value for this.
+ private const int DefaultSimpleReservoirPoolSize = 10;
+
private readonly AggregatorStore aggregatorStore;
private readonly AggregationType aggType;
- private HistogramBuckets histogramBuckets;
+ private MetricPointOptionalComponents mpComponents;
// Represents temporality adjusted "value" for double/long metric types or "count" when histogram
private MetricPointValueStorage runningValue;
@@ -42,7 +45,8 @@ internal MetricPoint(
AggregatorStore aggregatorStore,
AggregationType aggType,
KeyValuePair[] tagKeysAndValues,
- double[] histogramExplicitBounds)
+ double[] histogramExplicitBounds,
+ int exponentialHistogramMaxSize)
{
Debug.Assert(aggregatorStore != null, "AggregatorStore was null.");
Debug.Assert(histogramExplicitBounds != null, "Histogram explicit Bounds was null.");
@@ -54,19 +58,47 @@ internal MetricPoint(
this.deltaLastValue = default;
this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ ExemplarReservoir reservoir = null;
if (this.aggType == AggregationType.HistogramWithBuckets ||
this.aggType == AggregationType.HistogramWithMinMaxBuckets)
{
- this.histogramBuckets = new HistogramBuckets(histogramExplicitBounds);
+ this.mpComponents = new MetricPointOptionalComponents();
+ this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds);
+ if (aggregatorStore.IsExemplarEnabled())
+ {
+ reservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds.Length);
+ }
}
else if (this.aggType == AggregationType.Histogram ||
this.aggType == AggregationType.HistogramWithMinMax)
{
- this.histogramBuckets = new HistogramBuckets(null);
+ this.mpComponents = new MetricPointOptionalComponents();
+ this.mpComponents.HistogramBuckets = new HistogramBuckets(null);
+ }
+ else if (this.aggType == AggregationType.Base2ExponentialHistogram ||
+ this.aggType == AggregationType.Base2ExponentialHistogramWithMinMax)
+ {
+ this.mpComponents = new MetricPointOptionalComponents();
+ this.mpComponents.Base2ExponentialBucketHistogram = new Base2ExponentialBucketHistogram(exponentialHistogramMaxSize);
}
else
{
- this.histogramBuckets = null;
+ this.mpComponents = null;
+ }
+
+ if (aggregatorStore.IsExemplarEnabled() && reservoir == null)
+ {
+ reservoir = new SimpleExemplarReservoir(DefaultSimpleReservoirPoolSize);
+ }
+
+ if (reservoir != null)
+ {
+ if (this.mpComponents == null)
+ {
+ this.mpComponents = new MetricPointOptionalComponents();
+ }
+
+ this.mpComponents.ExemplarReservoir = reservoir;
}
// Note: Intentionally set last because this is used to detect valid MetricPoints.
@@ -83,12 +115,12 @@ public readonly ReadOnlyTagCollection Tags
}
///
- /// Gets the start time associated with the metric point.
+ /// Gets the start time (UTC) associated with the metric point.
///
public readonly DateTimeOffset StartTime => this.aggregatorStore.StartTimeExclusive;
///
- /// Gets the end time associated with the metric point.
+ /// Gets the end time (UTC) associated with the metric point.
///
public readonly DateTimeOffset EndTime => this.aggregatorStore.EndTimeInclusive;
@@ -188,7 +220,9 @@ public readonly long GetHistogramCount()
if (this.aggType != AggregationType.HistogramWithBuckets &&
this.aggType != AggregationType.Histogram &&
this.aggType != AggregationType.HistogramWithMinMaxBuckets &&
- this.aggType != AggregationType.HistogramWithMinMax)
+ this.aggType != AggregationType.HistogramWithMinMax &&
+ this.aggType != AggregationType.Base2ExponentialHistogram &&
+ this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax)
{
this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramCount));
}
@@ -209,12 +243,16 @@ public readonly double GetHistogramSum()
if (this.aggType != AggregationType.HistogramWithBuckets &&
this.aggType != AggregationType.Histogram &&
this.aggType != AggregationType.HistogramWithMinMaxBuckets &&
- this.aggType != AggregationType.HistogramWithMinMax)
+ this.aggType != AggregationType.HistogramWithMinMax &&
+ this.aggType != AggregationType.Base2ExponentialHistogram &&
+ this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax)
{
this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum));
}
- return this.histogramBuckets.SnapshotSum;
+ return this.mpComponents.HistogramBuckets != null
+ ? this.mpComponents.HistogramBuckets.SnapshotSum
+ : this.mpComponents.Base2ExponentialBucketHistogram.SnapshotSum;
}
///
@@ -235,7 +273,7 @@ public readonly HistogramBuckets GetHistogramBuckets()
this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets));
}
- return this.histogramBuckets;
+ return this.mpComponents.HistogramBuckets;
}
///
@@ -250,10 +288,19 @@ public bool TryGetHistogramMinMaxValues(out double min, out double max)
if (this.aggType == AggregationType.HistogramWithMinMax ||
this.aggType == AggregationType.HistogramWithMinMaxBuckets)
{
- Debug.Assert(this.histogramBuckets != null, "histogramBuckets was null");
+ Debug.Assert(this.mpComponents.HistogramBuckets != null, "histogramBuckets was null");
- min = this.histogramBuckets.SnapshotMin;
- max = this.histogramBuckets.SnapshotMax;
+ min = this.mpComponents.HistogramBuckets.SnapshotMin;
+ max = this.mpComponents.HistogramBuckets.SnapshotMax;
+ return true;
+ }
+
+ if (this.aggType == AggregationType.Base2ExponentialHistogramWithMinMax)
+ {
+ Debug.Assert(this.mpComponents.Base2ExponentialBucketHistogram != null, "base2ExponentialBucketHistogram was null");
+
+ min = this.mpComponents.Base2ExponentialBucketHistogram.SnapshotMin;
+ max = this.mpComponents.Base2ExponentialBucketHistogram.SnapshotMax;
return true;
}
@@ -262,10 +309,21 @@ public bool TryGetHistogramMinMaxValues(out double min, out double max)
return false;
}
+ ///
+ /// Gets the exemplars associated with the metric point.
+ ///
+ /// .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public readonly Exemplar[] GetExemplars()
+ {
+ // TODO: Do not expose Exemplar data structure (array now)
+ return this.mpComponents?.Exemplars ?? Array.Empty();
+ }
+
internal readonly MetricPoint Copy()
{
MetricPoint copy = this;
- copy.histogramBuckets = this.histogramBuckets?.Copy();
+ copy.mpComponents = this.mpComponents?.Copy();
return copy;
}
@@ -314,6 +372,18 @@ internal void Update(long number)
this.UpdateHistogramWithBucketsAndMinMax((double)number);
break;
}
+
+ case AggregationType.Base2ExponentialHistogram:
+ {
+ this.UpdateBase2ExponentialHistogram((double)number);
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogramWithMinMax:
+ {
+ this.UpdateBase2ExponentialHistogramWithMinMax((double)number);
+ break;
+ }
}
// There is a race with Snapshot:
@@ -330,25 +400,30 @@ internal void Update(long number)
this.MetricPointStatus = MetricPointStatus.CollectPending;
}
- internal void Update(double number)
+ internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled)
{
switch (this.aggType)
{
- case AggregationType.DoubleSumIncomingDelta:
+ case AggregationType.LongSumIncomingDelta:
{
- double initValue, newValue;
var sw = default(SpinWait);
while (true)
{
- initValue = this.runningValue.AsDouble;
-
- unchecked
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
{
- newValue = initValue + number;
- }
+ // Lock acquired
+ unchecked
+ {
+ this.runningValue.AsLong += number;
+ }
- if (initValue == Interlocked.CompareExchange(ref this.runningValue.AsDouble, newValue, initValue))
- {
+ if (isSampled)
+ {
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
+ }
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
break;
}
@@ -358,39 +433,83 @@ internal void Update(double number)
break;
}
- case AggregationType.DoubleSumIncomingCumulative:
+ case AggregationType.LongSumIncomingCumulative:
{
- Interlocked.Exchange(ref this.runningValue.AsDouble, number);
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.runningValue.AsLong = number;
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
break;
}
- case AggregationType.DoubleGauge:
+ case AggregationType.LongGauge:
{
- Interlocked.Exchange(ref this.runningValue.AsDouble, number);
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.runningValue.AsLong = number;
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
break;
}
case AggregationType.Histogram:
{
- this.UpdateHistogram(number);
+ this.UpdateHistogram((double)number, tags, true);
break;
}
case AggregationType.HistogramWithMinMax:
{
- this.UpdateHistogramWithMinMax(number);
+ this.UpdateHistogramWithMinMax((double)number, tags, true);
break;
}
case AggregationType.HistogramWithBuckets:
{
- this.UpdateHistogramWithBuckets(number);
+ this.UpdateHistogramWithBuckets((double)number, tags, true);
break;
}
case AggregationType.HistogramWithMinMaxBuckets:
{
- this.UpdateHistogramWithBucketsAndMinMax(number);
+ this.UpdateHistogramWithBucketsAndMinMax((double)number, tags, true);
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogram:
+ {
+ this.UpdateBase2ExponentialHistogram((double)number, tags, true);
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogramWithMinMax:
+ {
+ this.UpdateBase2ExponentialHistogramWithMinMax((double)number, tags, true);
break;
}
}
@@ -409,135 +528,121 @@ internal void Update(double number)
this.MetricPointStatus = MetricPointStatus.CollectPending;
}
- internal void TakeSnapshot(bool outputDelta)
+ internal void Update(double number)
{
switch (this.aggType)
{
- case AggregationType.LongSumIncomingDelta:
- case AggregationType.LongSumIncomingCumulative:
+ case AggregationType.DoubleSumIncomingDelta:
{
- if (outputDelta)
+ double initValue, newValue;
+ var sw = default(SpinWait);
+ while (true)
{
- long initValue = Interlocked.Read(ref this.runningValue.AsLong);
- this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong;
- this.deltaLastValue.AsLong = initValue;
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ initValue = this.runningValue.AsDouble;
- // Check again if value got updated, if yes reset status.
- // This ensures no Updates get Lost.
- if (initValue != Interlocked.Read(ref this.runningValue.AsLong))
+ unchecked
{
- this.MetricPointStatus = MetricPointStatus.CollectPending;
+ newValue = initValue + number;
}
- }
- else
- {
- this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong);
+
+ if (initValue == Interlocked.CompareExchange(ref this.runningValue.AsDouble, newValue, initValue))
+ {
+ break;
+ }
+
+ sw.SpinOnce();
}
break;
}
- case AggregationType.DoubleSumIncomingDelta:
case AggregationType.DoubleSumIncomingCumulative:
{
- if (outputDelta)
- {
- // TODO:
- // Is this thread-safe way to read double?
- // As long as the value is not -ve infinity,
- // the exchange (to 0.0) will never occur,
- // but we get the original value atomically.
- double initValue = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity);
- this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble;
- this.deltaLastValue.AsDouble = initValue;
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
-
- // Check again if value got updated, if yes reset status.
- // This ensures no Updates get Lost.
- if (initValue != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity))
- {
- this.MetricPointStatus = MetricPointStatus.CollectPending;
- }
- }
- else
- {
- // TODO:
- // Is this thread-safe way to read double?
- // As long as the value is not -ve infinity,
- // the exchange (to 0.0) will never occur,
- // but we get the original value atomically.
- this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity);
- }
+ Interlocked.Exchange(ref this.runningValue.AsDouble, number);
+ break;
+ }
+ case AggregationType.DoubleGauge:
+ {
+ Interlocked.Exchange(ref this.runningValue.AsDouble, number);
break;
}
- case AggregationType.LongGauge:
+ case AggregationType.Histogram:
{
- this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong);
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ this.UpdateHistogram(number);
+ break;
+ }
- // Check again if value got updated, if yes reset status.
- // This ensures no Updates get Lost.
- if (this.snapshotValue.AsLong != Interlocked.Read(ref this.runningValue.AsLong))
- {
- this.MetricPointStatus = MetricPointStatus.CollectPending;
- }
+ case AggregationType.HistogramWithMinMax:
+ {
+ this.UpdateHistogramWithMinMax(number);
+ break;
+ }
+ case AggregationType.HistogramWithBuckets:
+ {
+ this.UpdateHistogramWithBuckets(number);
break;
}
- case AggregationType.DoubleGauge:
+ case AggregationType.HistogramWithMinMaxBuckets:
{
- // TODO:
- // Is this thread-safe way to read double?
- // As long as the value is not -ve infinity,
- // the exchange (to 0.0) will never occur,
- // but we get the original value atomically.
- this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity);
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ this.UpdateHistogramWithBucketsAndMinMax(number);
+ break;
+ }
- // Check again if value got updated, if yes reset status.
- // This ensures no Updates get Lost.
- if (this.snapshotValue.AsDouble != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity))
- {
- this.MetricPointStatus = MetricPointStatus.CollectPending;
- }
+ case AggregationType.Base2ExponentialHistogram:
+ {
+ this.UpdateBase2ExponentialHistogram(number);
+ break;
+ }
+ case AggregationType.Base2ExponentialHistogramWithMinMax:
+ {
+ this.UpdateBase2ExponentialHistogramWithMinMax(number);
break;
}
+ }
- case AggregationType.HistogramWithBuckets:
+ // There is a race with Snapshot:
+ // Update() updates the value
+ // Snapshot snapshots the value
+ // Snapshot sets status to NoCollectPending
+ // Update sets status to CollectPending -- this is not right as the Snapshot
+ // already included the updated value.
+ // In the absence of any new Update call until next Snapshot,
+ // this results in exporting an Update even though
+ // it had no update.
+ // TODO: For Delta, this can be mitigated
+ // by ignoring Zero points
+ this.MetricPointStatus = MetricPointStatus.CollectPending;
+ }
+
+ internal void UpdateWithExemplar(double number, ReadOnlySpan> tags, bool isSampled)
+ {
+ switch (this.aggType)
+ {
+ case AggregationType.DoubleSumIncomingDelta:
{
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
- this.snapshotValue.AsLong = this.runningValue.AsLong;
- this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum;
-
- if (outputDelta)
+ unchecked
{
- this.runningValue.AsLong = 0;
- this.histogramBuckets.RunningSum = 0;
+ this.runningValue.AsDouble += number;
}
- for (int i = 0; i < this.histogramBuckets.RunningBucketCounts.Length; i++)
+ if (isSampled)
{
- this.histogramBuckets.SnapshotBucketCounts[i] = this.histogramBuckets.RunningBucketCounts[i];
- if (outputDelta)
- {
- this.histogramBuckets.RunningBucketCounts[i] = 0;
- }
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
}
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
-
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
break;
}
@@ -547,27 +652,23 @@ internal void TakeSnapshot(bool outputDelta)
break;
}
- case AggregationType.Histogram:
+ case AggregationType.DoubleSumIncomingCumulative:
{
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
- this.snapshotValue.AsLong = this.runningValue.AsLong;
- this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum;
-
- if (outputDelta)
+ unchecked
{
- this.runningValue.AsLong = 0;
- this.histogramBuckets.RunningSum = 0;
+ this.runningValue.AsDouble = number;
}
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
break;
}
@@ -577,40 +678,23 @@ internal void TakeSnapshot(bool outputDelta)
break;
}
- case AggregationType.HistogramWithMinMaxBuckets:
+ case AggregationType.DoubleGauge:
{
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
- this.snapshotValue.AsLong = this.runningValue.AsLong;
- this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum;
- this.histogramBuckets.SnapshotMin = this.histogramBuckets.RunningMin;
- this.histogramBuckets.SnapshotMax = this.histogramBuckets.RunningMax;
-
- if (outputDelta)
- {
- this.runningValue.AsLong = 0;
- this.histogramBuckets.RunningSum = 0;
- this.histogramBuckets.RunningMin = double.PositiveInfinity;
- this.histogramBuckets.RunningMax = double.NegativeInfinity;
- }
-
- for (int i = 0; i < this.histogramBuckets.RunningBucketCounts.Length; i++)
+ unchecked
{
- this.histogramBuckets.SnapshotBucketCounts[i] = this.histogramBuckets.RunningBucketCounts[i];
- if (outputDelta)
- {
- this.histogramBuckets.RunningBucketCounts[i] = 0;
- }
+ this.runningValue.AsDouble = number;
}
- this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
break;
}
@@ -620,31 +704,645 @@ internal void TakeSnapshot(bool outputDelta)
break;
}
- case AggregationType.HistogramWithMinMax:
+ case AggregationType.Histogram:
+ {
+ this.UpdateHistogram(number, tags, true);
+ break;
+ }
+
+ case AggregationType.HistogramWithMinMax:
+ {
+ this.UpdateHistogramWithMinMax(number, tags, true);
+ break;
+ }
+
+ case AggregationType.HistogramWithBuckets:
+ {
+ this.UpdateHistogramWithBuckets(number, tags, true);
+ break;
+ }
+
+ case AggregationType.HistogramWithMinMaxBuckets:
+ {
+ this.UpdateHistogramWithBucketsAndMinMax(number, tags, true);
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogram:
+ {
+ this.UpdateBase2ExponentialHistogram(number, tags, true);
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogramWithMinMax:
+ {
+ this.UpdateBase2ExponentialHistogramWithMinMax(number, tags, true);
+ break;
+ }
+ }
+
+ // There is a race with Snapshot:
+ // Update() updates the value
+ // Snapshot snapshots the value
+ // Snapshot sets status to NoCollectPending
+ // Update sets status to CollectPending -- this is not right as the Snapshot
+ // already included the updated value.
+ // In the absence of any new Update call until next Snapshot,
+ // this results in exporting an Update even though
+ // it had no update.
+ // TODO: For Delta, this can be mitigated
+ // by ignoring Zero points
+ this.MetricPointStatus = MetricPointStatus.CollectPending;
+ }
+
+ internal void TakeSnapshot(bool outputDelta)
+ {
+ switch (this.aggType)
+ {
+ case AggregationType.LongSumIncomingDelta:
+ case AggregationType.LongSumIncomingCumulative:
+ {
+ if (outputDelta)
+ {
+ long initValue = Interlocked.Read(ref this.runningValue.AsLong);
+ this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong;
+ this.deltaLastValue.AsLong = initValue;
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Check again if value got updated, if yes reset status.
+ // This ensures no Updates get Lost.
+ if (initValue != Interlocked.Read(ref this.runningValue.AsLong))
+ {
+ this.MetricPointStatus = MetricPointStatus.CollectPending;
+ }
+ }
+ else
+ {
+ this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong);
+ }
+
+ break;
+ }
+
+ case AggregationType.DoubleSumIncomingDelta:
+ case AggregationType.DoubleSumIncomingCumulative:
+ {
+ if (outputDelta)
+ {
+ // TODO:
+ // Is this thread-safe way to read double?
+ // As long as the value is not -ve infinity,
+ // the exchange (to 0.0) will never occur,
+ // but we get the original value atomically.
+ double initValue = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity);
+ this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble;
+ this.deltaLastValue.AsDouble = initValue;
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Check again if value got updated, if yes reset status.
+ // This ensures no Updates get Lost.
+ if (initValue != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity))
+ {
+ this.MetricPointStatus = MetricPointStatus.CollectPending;
+ }
+ }
+ else
+ {
+ // TODO:
+ // Is this thread-safe way to read double?
+ // As long as the value is not -ve infinity,
+ // the exchange (to 0.0) will never occur,
+ // but we get the original value atomically.
+ this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity);
+ }
+
+ break;
+ }
+
+ case AggregationType.LongGauge:
+ {
+ this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong);
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Check again if value got updated, if yes reset status.
+ // This ensures no Updates get Lost.
+ if (this.snapshotValue.AsLong != Interlocked.Read(ref this.runningValue.AsLong))
+ {
+ this.MetricPointStatus = MetricPointStatus.CollectPending;
+ }
+
+ break;
+ }
+
+ case AggregationType.DoubleGauge:
+ {
+ // TODO:
+ // Is this thread-safe way to read double?
+ // As long as the value is not -ve infinity,
+ // the exchange (to 0.0) will never occur,
+ // but we get the original value atomically.
+ this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity);
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Check again if value got updated, if yes reset status.
+ // This ensures no Updates get Lost.
+ if (this.snapshotValue.AsDouble != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity))
+ {
+ this.MetricPointStatus = MetricPointStatus.CollectPending;
+ }
+
+ break;
+ }
+
+ case AggregationType.HistogramWithBuckets:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ }
+
+ for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++)
+ {
+ histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i];
+ if (outputDelta)
+ {
+ histogramBuckets.RunningBucketCounts[i] = 0;
+ }
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.Histogram:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ }
+
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.HistogramWithMinMaxBuckets:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+ histogramBuckets.SnapshotMin = histogramBuckets.RunningMin;
+ histogramBuckets.SnapshotMax = histogramBuckets.RunningMax;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ histogramBuckets.RunningMin = double.PositiveInfinity;
+ histogramBuckets.RunningMax = double.NegativeInfinity;
+ }
+
+ for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++)
+ {
+ histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i];
+ if (outputDelta)
+ {
+ histogramBuckets.RunningBucketCounts[i] = 0;
+ }
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.HistogramWithMinMax:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+ histogramBuckets.SnapshotMin = histogramBuckets.RunningMin;
+ histogramBuckets.SnapshotMax = histogramBuckets.RunningMax;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ histogramBuckets.RunningMin = double.PositiveInfinity;
+ histogramBuckets.RunningMax = double.NegativeInfinity;
+ }
+
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogram:
+ {
+ var histogram = this.mpComponents.Base2ExponentialBucketHistogram;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogram.SnapshotSum = histogram.RunningSum;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogram.RunningSum = 0;
+ }
+
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.Base2ExponentialHistogramWithMinMax:
+ {
+ var histogram = this.mpComponents.Base2ExponentialBucketHistogram;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogram.SnapshotSum = histogram.RunningSum;
+ histogram.SnapshotMin = histogram.RunningMin;
+ histogram.SnapshotMax = histogram.RunningMax;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogram.RunningSum = 0;
+ histogram.RunningMin = double.PositiveInfinity;
+ histogram.RunningMax = double.NegativeInfinity;
+ }
+
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+ }
+ }
+
+ internal void TakeSnapshotWithExemplar(bool outputDelta)
+ {
+ switch (this.aggType)
+ {
+ case AggregationType.LongSumIncomingDelta:
+ case AggregationType.LongSumIncomingCumulative:
+ {
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+
+ if (outputDelta)
+ {
+ long initValue = this.runningValue.AsLong;
+ this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong;
+ this.deltaLastValue.AsLong = initValue;
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ }
+ else
+ {
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.DoubleSumIncomingDelta:
+ case AggregationType.DoubleSumIncomingCumulative:
+ {
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+
+ if (outputDelta)
+ {
+ double initValue = this.runningValue.AsDouble;
+ this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble;
+ this.deltaLastValue.AsDouble = initValue;
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ }
+ else
+ {
+ this.snapshotValue.AsDouble = this.runningValue.AsDouble;
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.LongGauge:
+ {
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.DoubleGauge:
+ {
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+
+ this.snapshotValue.AsDouble = this.runningValue.AsDouble;
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+
+ // Release lock
+ Interlocked.Exchange(ref this.mpComponents.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.HistogramWithBuckets:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ }
+
+ for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++)
+ {
+ histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i];
+ if (outputDelta)
+ {
+ histogramBuckets.RunningBucketCounts[i] = 0;
+ }
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.Histogram:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.HistogramWithMinMaxBuckets:
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ this.snapshotValue.AsLong = this.runningValue.AsLong;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+ histogramBuckets.SnapshotMin = histogramBuckets.RunningMin;
+ histogramBuckets.SnapshotMax = histogramBuckets.RunningMax;
+
+ if (outputDelta)
+ {
+ this.runningValue.AsLong = 0;
+ histogramBuckets.RunningSum = 0;
+ histogramBuckets.RunningMin = double.PositiveInfinity;
+ histogramBuckets.RunningMax = double.NegativeInfinity;
+ }
+
+ for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++)
+ {
+ histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i];
+ if (outputDelta)
+ {
+ histogramBuckets.RunningBucketCounts[i] = 0;
+ }
+ }
+
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
+ this.MetricPointStatus = MetricPointStatus.NoCollectPending;
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+
+ break;
+ }
+
+ case AggregationType.HistogramWithMinMax:
{
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
this.snapshotValue.AsLong = this.runningValue.AsLong;
- this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum;
- this.histogramBuckets.SnapshotMin = this.histogramBuckets.RunningMin;
- this.histogramBuckets.SnapshotMax = this.histogramBuckets.RunningMax;
+ histogramBuckets.SnapshotSum = histogramBuckets.RunningSum;
+ histogramBuckets.SnapshotMin = histogramBuckets.RunningMin;
+ histogramBuckets.SnapshotMax = histogramBuckets.RunningMax;
if (outputDelta)
{
this.runningValue.AsLong = 0;
- this.histogramBuckets.RunningSum = 0;
- this.histogramBuckets.RunningMin = double.PositiveInfinity;
- this.histogramBuckets.RunningMax = double.NegativeInfinity;
+ histogramBuckets.RunningSum = 0;
+ histogramBuckets.RunningMin = double.PositiveInfinity;
+ histogramBuckets.RunningMax = double.NegativeInfinity;
}
+ this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta);
this.MetricPointStatus = MetricPointStatus.NoCollectPending;
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
break;
}
@@ -656,22 +1354,90 @@ internal void TakeSnapshot(bool outputDelta)
}
}
- private void UpdateHistogram(double number)
+ private void UpdateHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false)
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ unchecked
+ {
+ this.runningValue.AsLong++;
+ histogramBuckets.RunningSum += number;
+ }
+
+ if (reportExemplar)
+ {
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
+ }
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+ }
+
+ private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false)
+ {
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ var sw = default(SpinWait);
+ while (true)
+ {
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ {
+ // Lock acquired
+ unchecked
+ {
+ this.runningValue.AsLong++;
+ histogramBuckets.RunningSum += number;
+ histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number);
+ histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number);
+ }
+
+ if (reportExemplar)
+ {
+ this.mpComponents.ExemplarReservoir.Offer(number, tags);
+ }
+
+ // Release lock
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
+ break;
+ }
+
+ sw.SpinOnce();
+ }
+ }
+
+ private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool reportExemplar = false)
{
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ int i = histogramBuckets.FindBucketIndex(number);
+
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
unchecked
{
this.runningValue.AsLong++;
- this.histogramBuckets.RunningSum += number;
+ histogramBuckets.RunningSum += number;
+ histogramBuckets.RunningBucketCounts[i]++;
+ if (reportExemplar)
+ {
+ this.mpComponents.ExemplarReservoir.Offer(number, tags, i);
+ }
}
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
break;
}
@@ -679,24 +1445,33 @@ private void UpdateHistogram(double number)
}
}
- private void UpdateHistogramWithMinMax(double number)
+ private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false)
{
+ var histogramBuckets = this.mpComponents.HistogramBuckets;
+ int i = histogramBuckets.FindBucketIndex(number);
+
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
unchecked
{
this.runningValue.AsLong++;
- this.histogramBuckets.RunningSum += number;
- this.histogramBuckets.RunningMin = Math.Min(this.histogramBuckets.RunningMin, number);
- this.histogramBuckets.RunningMax = Math.Max(this.histogramBuckets.RunningMax, number);
+ histogramBuckets.RunningSum += number;
+ histogramBuckets.RunningBucketCounts[i]++;
+ if (reportExemplar)
+ {
+ this.mpComponents.ExemplarReservoir.Offer(number, tags, i);
+ }
+
+ histogramBuckets.RunningMin = Math.Min(histogramBuckets.RunningMin, number);
+ histogramBuckets.RunningMax = Math.Max(histogramBuckets.RunningMax, number);
}
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0);
break;
}
@@ -704,25 +1479,26 @@ private void UpdateHistogramWithMinMax(double number)
}
}
- private void UpdateHistogramWithBuckets(double number)
+#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR
+ private void UpdateBase2ExponentialHistogram(double number, ReadOnlySpan> tags = default, bool reportExemplar = false)
+#pragma warning restore IDE0060 // Remove unused parameter
{
- int i = this.histogramBuckets.FindBucketIndex(number);
+ var histogram = this.mpComponents.Base2ExponentialBucketHistogram;
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
unchecked
{
this.runningValue.AsLong++;
- this.histogramBuckets.RunningSum += number;
- this.histogramBuckets.RunningBucketCounts[i]++;
+ histogram.RunningSum += number;
}
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0);
break;
}
@@ -730,27 +1506,29 @@ private void UpdateHistogramWithBuckets(double number)
}
}
- private void UpdateHistogramWithBucketsAndMinMax(double number)
+#pragma warning disable IDE0060 // Remove unused parameter: Exemplars for exponential histograms will be a follow up PR
+ private void UpdateBase2ExponentialHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false)
+#pragma warning restore IDE0060 // Remove unused parameter
{
- int i = this.histogramBuckets.FindBucketIndex(number);
+ var histogram = this.mpComponents.Base2ExponentialBucketHistogram;
var sw = default(SpinWait);
while (true)
{
- if (Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 1) == 0)
+ if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0)
{
// Lock acquired
unchecked
{
this.runningValue.AsLong++;
- this.histogramBuckets.RunningSum += number;
- this.histogramBuckets.RunningBucketCounts[i]++;
- this.histogramBuckets.RunningMin = Math.Min(this.histogramBuckets.RunningMin, number);
- this.histogramBuckets.RunningMax = Math.Max(this.histogramBuckets.RunningMax, number);
+ histogram.RunningSum += number;
+
+ histogram.RunningMin = Math.Min(histogram.RunningMin, number);
+ histogram.RunningMax = Math.Max(histogram.RunningMax, number);
}
// Release lock
- Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0);
+ Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0);
break;
}
diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs
new file mode 100644
index 00000000000..48251fa2427
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs
@@ -0,0 +1,51 @@
+//
+// 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.
+//
+
+namespace OpenTelemetry.Metrics
+{
+ ///
+ /// Stores optional components of a metric point.
+ /// Histogram, Exemplar are current components.
+ /// ExponentialHistogram is a future component.
+ /// This is done to keep the MetricPoint (struct)
+ /// size in control.
+ ///
+ internal sealed class MetricPointOptionalComponents
+ {
+ public HistogramBuckets HistogramBuckets;
+
+ public Base2ExponentialBucketHistogram Base2ExponentialBucketHistogram;
+
+ public ExemplarReservoir ExemplarReservoir;
+
+ public Exemplar[] Exemplars;
+
+ public int IsCriticalSectionOccupied = 0;
+
+ internal MetricPointOptionalComponents Copy()
+ {
+ MetricPointOptionalComponents copy = new MetricPointOptionalComponents();
+ copy.HistogramBuckets = this.HistogramBuckets.Copy();
+ if (this.Exemplars != null)
+ {
+ Array.Copy(this.Exemplars, copy.Exemplars, this.Exemplars.Length);
+ }
+
+ // TODO: Copy Base2ExponentialBucketHistogram
+ return copy;
+ }
+ }
+}
diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs
index c4710fe7bb5..df2d30e7821 100644
--- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs
+++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs
@@ -34,6 +34,8 @@ public abstract partial class MetricReader
private Metric[] metricsCurrentBatch;
private int metricIndex = -1;
+ private ExemplarFilter exemplarFilter;
+
internal AggregationTemporality GetAggregationTemporality(Type instrumentType)
{
return this.temporalityFunc(instrumentType);
@@ -69,7 +71,7 @@ internal Metric AddMetricWithNoViews(Instrument instrument)
Metric metric = null;
try
{
- metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream);
+ metric = new Metric(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, exemplarFilter: this.exemplarFilter);
}
catch (NotSupportedException nse)
{
@@ -154,7 +156,7 @@ internal List AddMetricsListWithViews(Instrument instrument, List[] keyAndValues;
+ internal readonly KeyValuePair[] KeyAndValues;
internal ReadOnlyTagCollection(KeyValuePair[]? keyAndValues)
{
- this.keyAndValues = keyAndValues ?? Array.Empty>();
+ this.KeyAndValues = keyAndValues ?? Array.Empty>();
}
///
/// Gets the number of tags in the collection.
///
- public int Count => this.keyAndValues.Length;
+ public int Count => this.KeyAndValues.Length;
///
/// Returns an enumerator that iterates through the tags.
@@ -78,7 +78,7 @@ public bool MoveNext()
if (index < this.source.Count)
{
- this.Current = this.source.keyAndValues[index];
+ this.Current = this.source.KeyAndValues[index];
this.index++;
return true;
diff --git a/src/OpenTelemetry/Resources/ResourceBuilder.cs b/src/OpenTelemetry/Resources/ResourceBuilder.cs
index 44657001c9d..c9de4fa77e7 100644
--- a/src/OpenTelemetry/Resources/ResourceBuilder.cs
+++ b/src/OpenTelemetry/Resources/ResourceBuilder.cs
@@ -132,18 +132,24 @@ public ResourceBuilder AddDetector(IResourceDetector resourceDetector)
///
/// Add a to the builder which will be resolved using the application .
///
- ///
- /// Note: The supplied may be
- /// called with a
- /// for detached instances. Factories
- /// should either throw if a cannot be handled,
- /// or return a default when is not available.
- ///
/// Resource detector factory.
/// Supplied for call chaining.
- // Note: This API may be made public if there is a need for it.
- internal ResourceBuilder AddDetector(Func resourceDetectorFactory)
+ public ResourceBuilder AddDetector(Func resourceDetectorFactory)
+ {
+ Guard.ThrowIfNull(resourceDetectorFactory);
+
+ return this.AddDetectorInternal(sp =>
+ {
+ if (sp == null)
+ {
+ throw new NotSupportedException("IResourceDetector factory pattern is not supported when calling ResourceBuilder.Build() directly.");
+ }
+
+ return resourceDetectorFactory(sp);
+ });
+ }
+
+ internal ResourceBuilder AddDetectorInternal(Func resourceDetectorFactory)
{
Guard.ThrowIfNull(resourceDetectorFactory);
diff --git a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs
index 3fcaf4b2b64..8353a6c465b 100644
--- a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs
+++ b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs
@@ -122,8 +122,8 @@ public static ResourceBuilder AddEnvironmentVariableDetector(this ResourceBuilde
Lazy configuration = new Lazy(() => new ConfigurationBuilder().AddEnvironmentVariables().Build());
return resourceBuilder
- .AddDetector(sp => new OtelEnvResourceDetector(sp?.GetService() ?? configuration.Value))
- .AddDetector(sp => new OtelServiceNameEnvVarDetector(sp?.GetService() ?? configuration.Value));
+ .AddDetectorInternal(sp => new OtelEnvResourceDetector(sp?.GetService() ?? configuration.Value))
+ .AddDetectorInternal(sp => new OtelServiceNameEnvVarDetector(sp?.GetService() ?? configuration.Value));
}
private static string GetFileVersion()
diff --git a/test/Benchmarks/Helper/LocalServer.cs b/test/Benchmarks/Helper/LocalServer.cs
deleted file mode 100644
index dcc14e7b7a4..00000000000
--- a/test/Benchmarks/Helper/LocalServer.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// 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.
-//
-
-#if NETCOREAPP
-
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using OpenTelemetry;
-using OpenTelemetry.Trace;
-
-namespace Benchmarks.Helper
-{
- public class LocalServer : IDisposable
- {
- private readonly IWebHost host;
- private TracerProvider tracerProvider;
-
- public LocalServer(string url, bool enableTracerProvider = false)
- {
- void ConfigureTestServices(IServiceCollection services)
- {
- if (enableTracerProvider)
- {
- this.tracerProvider = Sdk.CreateTracerProviderBuilder()
- .AddAspNetCoreInstrumentation()
- .Build();
- }
- }
-
- this.host = new WebHostBuilder()
- .UseKestrel()
- .UseStartup()
- .UseUrls(url)
- .ConfigureServices(configure => ConfigureTestServices(configure))
- .Build();
-
- Task.Run(() => this.host.Run());
- }
-
- public void Dispose()
- {
- try
- {
- this.tracerProvider.Dispose();
- this.host.Dispose();
- }
- catch (Exception)
- {
- // ignored, see https://github.com/aspnet/KestrelHttpServer/issues/1513
- // Kestrel 2.0.0 should have fix it, but it does not seem important for our tests
- }
-
- GC.SuppressFinalize(this);
- }
-
- private class Startup
- {
- public void Configure(IApplicationBuilder app)
- {
- app.Run(async (context) =>
- {
- await context.Response.WriteAsync("Hello World!").ConfigureAwait(false);
- });
- }
- }
- }
-}
-#endif
diff --git a/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs b/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs
index dd3160d723c..021ebc7a227 100644
--- a/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs
+++ b/test/Benchmarks/Instrumentation/AspNetCoreInstrumentationBenchmarks.cs
@@ -15,29 +15,27 @@
//
#if !NETFRAMEWORK
-using System.Net.Http;
-using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
+using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
/*
// * Summary *
-BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.521)
-Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
-.NET SDK=7.0.100-preview.6.22275.1
- [Host] : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2
+BenchmarkDotNet=v0.13.3, OS=Windows 10 (10.0.19045.2604)
+Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
+.NET SDK=7.0.103
+ [Host] : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2
Job=InProcess Toolchain=InProcessEmitToolchain
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|-------------------------------------------- |---------:|--------:|--------:|-------:|----------:|
-| UninstrumentedAspNetCoreApp | 172.3 us | 2.35 us | 2.09 us | 0.9766 | 4.73 KB |
-| InstrumentedAspNetCoreAppWithDefaultOptions | 175.2 us | 2.52 us | 2.10 us | 0.9766 | 4.86 KB |
+| UninstrumentedAspNetCoreApp | 149.4 us | 2.94 us | 2.75 us | 0.4883 | 2.54 KB |
+| InstrumentedAspNetCoreAppWithDefaultOptions | 171.9 us | 2.65 us | 2.48 us | 0.7324 | 3.79 KB |
*/
namespace Benchmarks.Instrumentation
@@ -48,6 +46,7 @@ public class AspNetCoreInstrumentationBenchmarks
private HttpClient httpClient;
private WebApplication app;
private TracerProvider tracerProvider;
+ private MeterProvider meterProvider;
[GlobalSetup(Target = nameof(UninstrumentedAspNetCoreApp))]
public void UninstrumentedAspNetCoreAppGlobalSetup()
@@ -65,6 +64,12 @@ public void InstrumentedAspNetCoreAppWithDefaultOptionsGlobalSetup()
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation()
.Build();
+
+ var exportedItems = new List();
+ this.meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddAspNetCoreInstrumentation()
+ .AddInMemoryExporter(exportedItems)
+ .Build();
}
[GlobalCleanup(Target = nameof(UninstrumentedAspNetCoreApp))]
@@ -80,29 +85,29 @@ public async Task GlobalCleanupInstrumentedAspNetCoreAppWithDefaultOptionsAsync(
this.httpClient.Dispose();
await this.app.DisposeAsync().ConfigureAwait(false);
this.tracerProvider.Dispose();
+ this.meterProvider.Dispose();
}
[Benchmark]
public async Task UninstrumentedAspNetCoreApp()
{
- var httpResponse = await this.httpClient.GetAsync("http://localhost:5000/api/values").ConfigureAwait(false);
+ var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
}
[Benchmark]
public async Task InstrumentedAspNetCoreAppWithDefaultOptions()
{
- var httpResponse = await this.httpClient.GetAsync("http://localhost:5000/api/values").ConfigureAwait(false);
+ var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
}
private void StartWebApplication()
{
var builder = WebApplication.CreateBuilder();
- builder.Services.AddControllers();
builder.Logging.ClearProviders();
var app = builder.Build();
- app.MapControllers();
+ app.MapGet("/", () => $"Hello World!");
app.RunAsync();
this.app = app;
diff --git a/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs b/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs
new file mode 100644
index 00000000000..d49cdec358f
--- /dev/null
+++ b/test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs
@@ -0,0 +1,118 @@
+//
+// 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.
+//
+
+#if !NETFRAMEWORK
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+/*
+// * Summary *
+
+BenchmarkDotNet=v0.13.3, OS=Windows 10 (10.0.19045.2604)
+Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
+.NET SDK=7.0.103
+ [Host] : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2
+
+Job=InProcess Toolchain=InProcessEmitToolchain
+
+
+| Method | Mean | Error | StdDev | Gen0 | Allocated |
+|------------------------- |---------:|--------:|--------:|-------:|----------:|
+| UninstrumentedHttpClient | 153.3 us | 2.95 us | 3.83 us | 0.4883 | 2.54 KB |
+| InstrumentedHttpClient | 170.4 us | 3.37 us | 4.14 us | 0.9766 | 4.51 KB |
+*/
+
+namespace Benchmarks.Instrumentation
+{
+ [InProcess]
+ public class HttpClientInstrumentationBenchmarks
+ {
+ private HttpClient httpClient;
+ private WebApplication app;
+ private TracerProvider tracerProvider;
+ private MeterProvider meterProvider;
+
+ [GlobalSetup(Target = nameof(UninstrumentedHttpClient))]
+ public void UninstrumentedSetup()
+ {
+ this.StartWebApplication();
+ this.httpClient = new HttpClient();
+ }
+
+ [GlobalSetup(Target = nameof(InstrumentedHttpClient))]
+ public void InstrumentedSetup()
+ {
+ this.StartWebApplication();
+ this.httpClient = new HttpClient();
+
+ this.tracerProvider = Sdk.CreateTracerProviderBuilder()
+ .AddHttpClientInstrumentation()
+ .Build();
+
+ var exportedItems = new List();
+ this.meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddHttpClientInstrumentation()
+ .AddInMemoryExporter(exportedItems)
+ .Build();
+ }
+
+ [GlobalCleanup(Target = nameof(UninstrumentedHttpClient))]
+ public async Task UninstrumentedCleanupAsync()
+ {
+ this.httpClient.Dispose();
+ await this.app.DisposeAsync().ConfigureAwait(false);
+ }
+
+ [GlobalCleanup(Target = nameof(InstrumentedHttpClient))]
+ public async Task InstrumentedCleanupAsync()
+ {
+ this.httpClient.Dispose();
+ await this.app.DisposeAsync().ConfigureAwait(false);
+ this.tracerProvider.Dispose();
+ this.meterProvider.Dispose();
+ }
+
+ [Benchmark]
+ public async Task UninstrumentedHttpClient()
+ {
+ var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false);
+ httpResponse.EnsureSuccessStatusCode();
+ }
+
+ [Benchmark]
+ public async Task InstrumentedHttpClient()
+ {
+ var httpResponse = await this.httpClient.GetAsync("http://localhost:5000").ConfigureAwait(false);
+ httpResponse.EnsureSuccessStatusCode();
+ }
+
+ private void StartWebApplication()
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.Logging.ClearProviders();
+ var app = builder.Build();
+ app.MapGet("/", () => $"Hello World!");
+ app.RunAsync();
+
+ this.app = app;
+ }
+ }
+}
+#endif
diff --git a/test/Benchmarks/Instrumentation/InstrumentedAspNetCoreBenchmark.cs b/test/Benchmarks/Instrumentation/InstrumentedAspNetCoreBenchmark.cs
deleted file mode 100644
index 4672a0adef9..00000000000
--- a/test/Benchmarks/Instrumentation/InstrumentedAspNetCoreBenchmark.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// 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.
-//
-
-#if !NETFRAMEWORK
-using System.Net.Http;
-using System.Threading.Tasks;
-using BenchmarkDotNet.Attributes;
-using Benchmarks.Helper;
-
-namespace Benchmarks.Instrumentation
-{
- [InProcess]
- public class InstrumentedAspNetCoreBenchmark
- {
- private const string LocalhostUrl = "http://localhost:5050";
-
- private HttpClient client;
- private LocalServer localServer;
-
- [GlobalSetup]
- public void GlobalSetup()
- {
- this.localServer = new LocalServer(LocalhostUrl, true);
- this.client = new HttpClient();
- }
-
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- this.localServer.Dispose();
- this.client.Dispose();
- }
-
- [Benchmark]
- public async Task InstrumentedAspNetCoreGetPage()
- {
- var httpResponse = await this.client.GetAsync(LocalhostUrl).ConfigureAwait(false);
- httpResponse.EnsureSuccessStatusCode();
- }
- }
-}
-#endif
diff --git a/test/Benchmarks/Instrumentation/InstrumentedHttpClientBenchmark.cs b/test/Benchmarks/Instrumentation/InstrumentedHttpClientBenchmark.cs
deleted file mode 100644
index c313d3af0d7..00000000000
--- a/test/Benchmarks/Instrumentation/InstrumentedHttpClientBenchmark.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-//
-// 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.Diagnostics;
-using System.Net.Http;
-using BenchmarkDotNet.Attributes;
-using OpenTelemetry;
-using OpenTelemetry.Resources;
-using OpenTelemetry.Tests;
-using OpenTelemetry.Trace;
-
-namespace Benchmarks.Instrumentation
-{
- public class InstrumentedHttpClientBenchmark
- {
- private const string ActivityName = "incoming request";
- private const string ServiceName = "http-service-example";
- private const string SourceName = "http-client-test";
-
- private HttpClient httpClient;
- private TracerProvider tracerProvider;
- private IDisposable serverLifeTime;
- private ActivitySource source;
- private string url;
-
- [GlobalSetup]
- public void GlobalSetup()
- {
- this.serverLifeTime = TestHttpServer.RunServer(
- (ctx) =>
- {
- ctx.Response.StatusCode = 200;
- ctx.Response.OutputStream.Close();
- },
- out var host,
- out var port);
-
- this.tracerProvider = Sdk.CreateTracerProviderBuilder()
- .AddHttpClientInstrumentation()
- .ConfigureResource(r => r.AddService(ServiceName))
- .AddSource(SourceName)
- .Build();
-
- this.url = $"http://{host}:{port}/";
- this.httpClient = new HttpClient();
- this.source = new ActivitySource(SourceName);
- }
-
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- this.httpClient.Dispose();
- this.tracerProvider.Dispose();
- this.serverLifeTime.Dispose();
- this.source.Dispose();
- }
-
- [Benchmark]
- public async Task InstrumentedHttpClient()
- {
- var httpResponse = await this.httpClient.GetAsync(this.url).ConfigureAwait(false);
- httpResponse.EnsureSuccessStatusCode();
- }
-
- [Benchmark]
- public async Task InstrumentedHttpClientWithParentActivity()
- {
- using var parent = this.source.StartActivity(ActivityName, ActivityKind.Server);
- var httpResponse = await this.httpClient.GetAsync(this.url).ConfigureAwait(false);
- httpResponse.EnsureSuccessStatusCode();
- }
- }
-}
diff --git a/test/Benchmarks/Instrumentation/UninstrumentedAspNetCoreBenchmark.cs b/test/Benchmarks/Instrumentation/UninstrumentedAspNetCoreBenchmark.cs
deleted file mode 100644
index b207c916019..00000000000
--- a/test/Benchmarks/Instrumentation/UninstrumentedAspNetCoreBenchmark.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-//
-// 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.
-//
-
-#if !NETFRAMEWORK
-using System.Net.Http;
-using System.Threading.Tasks;
-using BenchmarkDotNet.Attributes;
-using Benchmarks.Helper;
-
-namespace Benchmarks.Instrumentation
-{
- [InProcess]
- public class UninstrumentedAspNetCoreBenchmark
- {
- private const string LocalhostUrl = "http://localhost:5050";
-
- private HttpClient client;
- private LocalServer localServer;
-
- [GlobalSetup]
- public void GlobalSetup()
- {
- this.localServer = new LocalServer(LocalhostUrl);
- this.client = new HttpClient();
- }
-
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- this.client.Dispose();
- this.localServer.Dispose();
- }
-
- [Benchmark]
- public async Task SimpleAspNetCoreGetPage()
- {
- var httpResponse = await this.client.GetAsync(LocalhostUrl).ConfigureAwait(false);
- httpResponse.EnsureSuccessStatusCode();
- }
- }
-}
-#endif
diff --git a/test/Benchmarks/Instrumentation/UninstrumentedHttpClientBenchmark.cs b/test/Benchmarks/Instrumentation/UninstrumentedHttpClientBenchmark.cs
deleted file mode 100644
index 31ec0115b5b..00000000000
--- a/test/Benchmarks/Instrumentation/UninstrumentedHttpClientBenchmark.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-//
-// 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.Net.Http;
-using BenchmarkDotNet.Attributes;
-using OpenTelemetry.Tests;
-
-namespace Benchmarks.Instrumentation
-{
- public class UninstrumentedHttpClientBenchmark
- {
- private IDisposable serverLifeTime;
- private string url;
- private HttpClient httpClient;
-
- [GlobalSetup]
- public void GlobalSetup()
- {
- this.serverLifeTime = TestHttpServer.RunServer(
- (ctx) =>
- {
- ctx.Response.StatusCode = 200;
- ctx.Response.OutputStream.Close();
- },
- out var host,
- out var port);
-
- this.url = $"http://{host}:{port}/";
- this.httpClient = new HttpClient();
- }
-
- [GlobalCleanup]
- public void GlobalCleanup()
- {
- this.serverLifeTime.Dispose();
- this.httpClient.Dispose();
- }
-
- [Benchmark]
- public async Task SimpleHttpClient()
- {
- var httpResponse = await this.httpClient.GetAsync(this.url).ConfigureAwait(false);
- httpResponse.EnsureSuccessStatusCode();
- }
- }
-}
diff --git a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs
new file mode 100644
index 00000000000..97be2bc31f4
--- /dev/null
+++ b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs
@@ -0,0 +1,149 @@
+//
+// 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.Diagnostics;
+using System.Diagnostics.Metrics;
+using BenchmarkDotNet.Attributes;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Tests;
+
+/*
+// * Summary *
+BenchmarkDotNet=v0.13.3, OS=Windows 10 (10.0.19045.2604)
+Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
+.NET SDK=7.0.103
+ [Host] : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2
+ DefaultJob : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2
+
+
+| Method | ExemplarFilter | Mean | Error | StdDev |
+|-------------------------- |--------------- |---------:|--------:|--------:|
+| HistogramNoTagReduction | AlwaysOff | 380.7 ns | 5.92 ns | 5.53 ns |
+| HistogramWithTagReduction | AlwaysOff | 356.5 ns | 3.33 ns | 2.95 ns |
+| HistogramNoTagReduction | AlwaysOn | 412.3 ns | 2.11 ns | 1.64 ns |
+| HistogramWithTagReduction | AlwaysOn | 461.0 ns | 4.65 ns | 4.35 ns |
+| HistogramNoTagReduction | HighValueOnly | 378.3 ns | 2.22 ns | 2.08 ns |
+| HistogramWithTagReduction | HighValueOnly | 383.1 ns | 7.48 ns | 7.35 ns |
+
+*/
+
+namespace Benchmarks.Metrics
+{
+ public class ExemplarBenchmarks
+ {
+ private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random());
+ private readonly string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" };
+ private Histogram histogramWithoutTagReduction;
+
+ private Histogram histogramWithTagReduction;
+
+ private MeterProvider provider;
+ private Meter meter;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Test only.")]
+ public enum ExemplarFilterTouse
+ {
+ AlwaysOff,
+ AlwaysOn,
+ HighValueOnly,
+ }
+
+ [Params(ExemplarFilterTouse.AlwaysOn, ExemplarFilterTouse.AlwaysOff, ExemplarFilterTouse.HighValueOnly)]
+ public ExemplarFilterTouse ExemplarFilter { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ this.meter = new Meter(Utils.GetCurrentMethodName());
+ this.histogramWithoutTagReduction = this.meter.CreateHistogram("HistogramWithoutTagReduction");
+ this.histogramWithTagReduction = this.meter.CreateHistogram("HistogramWithTagReduction");
+ var exportedItems = new List();
+
+ ExemplarFilter exemplarFilter = new AlwaysOffExemplarFilter();
+ if (this.ExemplarFilter == ExemplarFilterTouse.AlwaysOn)
+ {
+ exemplarFilter = new AlwaysOnExemplarFilter();
+ }
+ else if (this.ExemplarFilter == ExemplarFilterTouse.HighValueOnly)
+ {
+ exemplarFilter = new HighValueExemplarFilter();
+ }
+
+ this.provider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(this.meter.Name)
+ .SetExemplarFilter(exemplarFilter)
+ .AddView("HistogramWithTagReduction", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } })
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 1000;
+ })
+ .Build();
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ this.meter?.Dispose();
+ this.provider?.Dispose();
+ }
+
+ [Benchmark]
+ public void HistogramNoTagReduction()
+ {
+ var random = ThreadLocalRandom.Value;
+ var tags = new TagList
+ {
+ { "DimName1", this.dimensionValues[random.Next(0, 2)] },
+ { "DimName2", this.dimensionValues[random.Next(0, 2)] },
+ { "DimName3", this.dimensionValues[random.Next(0, 5)] },
+ { "DimName4", this.dimensionValues[random.Next(0, 5)] },
+ { "DimName5", this.dimensionValues[random.Next(0, 10)] },
+ };
+
+ this.histogramWithoutTagReduction.Record(random.Next(1000), tags);
+ }
+
+ [Benchmark]
+ public void HistogramWithTagReduction()
+ {
+ var random = ThreadLocalRandom.Value;
+ var tags = new TagList
+ {
+ { "DimName1", this.dimensionValues[random.Next(0, 2)] },
+ { "DimName2", this.dimensionValues[random.Next(0, 2)] },
+ { "DimName3", this.dimensionValues[random.Next(0, 5)] },
+ { "DimName4", this.dimensionValues[random.Next(0, 5)] },
+ { "DimName5", this.dimensionValues[random.Next(0, 10)] },
+ };
+
+ this.histogramWithTagReduction.Record(random.Next(1000), tags);
+ }
+
+ public class HighValueExemplarFilter : ExemplarFilter
+ {
+ public override bool ShouldSample(long value, ReadOnlySpan> tags)
+ {
+ return value > 800;
+ }
+
+ public override bool ShouldSample(double value, ReadOnlySpan> tags)
+ {
+ return value > 800;
+ }
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
index e8f4e08a4ed..e4e484e062b 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.cs
@@ -157,58 +157,59 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut
Assert.True(enrichWithExceptionCalled);
}
- if (tc.ResponseExpected)
- {
#if NETFRAMEWORK
- Assert.Empty(requestMetrics);
+ Assert.Empty(requestMetrics);
#else
- Assert.Single(requestMetrics);
+ Assert.Single(requestMetrics);
- var metric = requestMetrics[0];
- Assert.NotNull(metric);
- Assert.True(metric.MetricType == MetricType.Histogram);
+ var metric = requestMetrics[0];
+ Assert.NotNull(metric);
+ Assert.True(metric.MetricType == MetricType.Histogram);
- var metricPoints = new List();
- foreach (var p in metric.GetMetricPoints())
- {
- metricPoints.Add(p);
- }
+ var metricPoints = new List();
+ foreach (var p in metric.GetMetricPoints())
+ {
+ metricPoints.Add(p);
+ }
- Assert.Single(metricPoints);
- var metricPoint = metricPoints[0];
+ Assert.Single(metricPoints);
+ var metricPoint = metricPoints[0];
- var count = metricPoint.GetHistogramCount();
- var sum = metricPoint.GetHistogramSum();
+ var count = metricPoint.GetHistogramCount();
+ var sum = metricPoint.GetHistogramSum();
- Assert.Equal(1L, count);
- Assert.Equal(activity.Duration.TotalMilliseconds, sum);
+ Assert.Equal(1L, count);
+ Assert.Equal(activity.Duration.TotalMilliseconds, sum);
- var attributes = new KeyValuePair[metricPoint.Tags.Count];
- int i = 0;
- foreach (var tag in metricPoint.Tags)
- {
- attributes[i++] = tag;
- }
+ var attributes = new KeyValuePair[metricPoint.Tags.Count];
+ int i = 0;
+ foreach (var tag in metricPoint.Tags)
+ {
+ attributes[i++] = tag;
+ }
- var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, tc.Method);
- var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http");
- var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, tc.ResponseCode == 0 ? 200 : tc.ResponseCode);
- var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "2.0");
- var hostName = new KeyValuePair(SemanticConventions.AttributeNetPeerName, host);
- var portNumber = new KeyValuePair(SemanticConventions.AttributeNetPeerPort, port);
- Assert.Contains(method, attributes);
- Assert.Contains(scheme, attributes);
+ var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, tc.Method);
+ var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http");
+ var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, tc.ResponseCode == 0 ? 200 : tc.ResponseCode);
+ var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "2.0");
+ var hostName = new KeyValuePair(SemanticConventions.AttributeNetPeerName, tc.ResponseExpected ? host : "sdlfaldfjalkdfjlkajdflkajlsdjf");
+ var portNumber = new KeyValuePair(SemanticConventions.AttributeNetPeerPort, port);
+ Assert.Contains(hostName, attributes);
+ Assert.Contains(portNumber, attributes);
+ Assert.Contains(method, attributes);
+ Assert.Contains(scheme, attributes);
+ Assert.Contains(flavor, attributes);
+ if (tc.ResponseExpected)
+ {
Assert.Contains(statusCode, attributes);
- Assert.Contains(flavor, attributes);
- Assert.Contains(hostName, attributes);
- Assert.Contains(portNumber, attributes);
Assert.Equal(6, attributes.Length);
-#endif
}
else
{
- Assert.Empty(requestMetrics);
+ Assert.DoesNotContain(statusCode, attributes);
+ Assert.Equal(5, attributes.Length);
}
+#endif
}
[Fact]
diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
index c9f68f4698b..833e26098bb 100644
--- a/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
+++ b/test/OpenTelemetry.Instrumentation.Http.Tests/http-out-test-cases.json
@@ -90,35 +90,37 @@
{
"name": "Call that cannot resolve DNS will be reported as error span",
"method": "GET",
- "url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/",
+ "url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/",
"spanName": "HTTP GET",
"spanStatus": "Error",
"spanStatusHasDescription": true,
"responseExpected": false,
"recordException": false,
"spanAttributes": {
- "http.scheme": "https",
+ "http.scheme": "http",
"http.method": "GET",
- "net.peer.name": "sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com",
+ "net.peer.name": "sdlfaldfjalkdfjlkajdflkajlsdjf",
+ "net.peer.port": "{port}",
"http.flavor": "{flavor}",
- "http.url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"
+ "http.url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/"
}
},
{
"name": "Call that cannot resolve DNS will be reported as error span. And Records exception",
"method": "GET",
- "url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/",
+ "url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/",
"spanName": "HTTP GET",
"spanStatus": "Error",
"spanStatusHasDescription": true,
"responseExpected": false,
"recordException": true,
"spanAttributes": {
- "http.scheme": "https",
+ "http.scheme": "http",
"http.method": "GET",
- "net.peer.name": "sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com",
+ "net.peer.name": "sdlfaldfjalkdfjlkajdflkajlsdjf",
+ "net.peer.port": "{port}",
"http.flavor": "{flavor}",
- "http.url": "https://sdlfaldfjalkdfjlkajdflkajlsdjf.sdlkjafsdjfalfadslkf.com/"
+ "http.url": "http://sdlfaldfjalkdfjlkajdflkajlsdjf:{port}/"
}
},
{
diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs
index 3129dfaa808..9dd7a9872fb 100644
--- a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs
+++ b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs
@@ -25,7 +25,7 @@ public partial class Program
private const int ArraySize = 10;
// Note: Uncomment the below line if you want to run Histogram stress test
- // private const int MaxHistogramMeasurement = 1000;
+ private const int MaxHistogramMeasurement = 1000;
private static readonly Meter TestMeter = new(Utils.GetCurrentMethodName());
private static readonly Counter TestCounter = TestMeter.CreateCounter("TestCounter");
@@ -33,7 +33,7 @@ public partial class Program
private static readonly ThreadLocal ThreadLocalRandom = new(() => new Random());
// Note: Uncomment the below line if you want to run Histogram stress test
- // private static readonly Histogram TestHistogram = TestMeter.CreateHistogram("TestHistogram");
+ private static readonly Histogram TestHistogram = TestMeter.CreateHistogram("TestHistogram");
public static void Main()
{
@@ -44,6 +44,7 @@ public static void Main()
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(TestMeter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
.AddPrometheusHttpListener(
options => options.UriPrefixes = new string[] { $"http://localhost:9185/" })
.Build();
@@ -51,26 +52,27 @@ public static void Main()
Stress(prometheusPort: 9464);
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- protected static void Run()
- {
- var random = ThreadLocalRandom.Value;
- TestCounter.Add(
- 100,
- new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
- new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
- new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
- }
-
- // Note: Uncomment the below lines if you want to run Histogram stress test
+ // Note: Uncomment the below lines if you want to run Counter stress test
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// protected static void Run()
// {
// var random = ThreadLocalRandom.Value;
- // TestHistogram.Record(
- // random.Next(MaxHistogramMeasurement),
+ // TestCounter.Add(
+ // 100,
// new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
// new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
// new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
// }
+
+ // Note: Uncomment the below lines if you want to run Histogram stress test
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected static void Run()
+ {
+ var random = ThreadLocalRandom.Value;
+ TestHistogram.Record(
+ random.Next(MaxHistogramMeasurement),
+ new("DimName1", DimensionValues[random.Next(0, ArraySize)]),
+ new("DimName2", DimensionValues[random.Next(0, ArraySize)]),
+ new("DimName3", DimensionValues[random.Next(0, ArraySize)]));
+ }
}
diff --git a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs
index 56b55b41f02..a28e68c3a2f 100644
--- a/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs
+++ b/test/OpenTelemetry.Tests/Internal/SelfDiagnosticsConfigRefresherTest.cs
@@ -43,7 +43,7 @@ public void SelfDiagnosticsConfigRefresher_OmitAsConfigured()
using var configRefresher = new SelfDiagnosticsConfigRefresher();
// Emitting event of EventLevel.Warning
- OpenTelemetrySdkEventSource.Log.SpanProcessorQueueIsExhausted();
+ OpenTelemetrySdkEventSource.Log.ExporterErrorResult(ExportResult.Success);
int bufferSize = 512;
byte[] actualBytes = ReadFile(bufferSize);
@@ -69,8 +69,8 @@ public void SelfDiagnosticsConfigRefresher_CaptureAsConfigured()
using var configRefresher = new SelfDiagnosticsConfigRefresher();
// Emitting event of EventLevel.Error
- OpenTelemetrySdkEventSource.Log.SpanProcessorException("Event string sample", "Exception string sample");
- string expectedMessage = "Unknown error in SpanProcessor event '{0}': '{1}'.{Event string sample}{Exception string sample}";
+ OpenTelemetrySdkEventSource.Log.TracerProviderException("Event string sample", "Exception string sample");
+ string expectedMessage = "Unknown error in TracerProvider '{0}': '{1}'.{Event string sample}{Exception string sample}";
int bufferSize = 2 * (MessageOnNewFileString.Length + expectedMessage.Length);
byte[] actualBytes = ReadFile(bufferSize);
diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs
index e05cbcda414..f935799cdae 100644
--- a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs
+++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs
@@ -20,12 +20,12 @@ namespace OpenTelemetry.Metrics.Tests
{
public class AggregatorTest
{
- private readonly AggregatorStore aggregatorStore = new("test", AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, Metric.DefaultHistogramBounds);
+ private readonly AggregatorStore aggregatorStore = new("test", AggregationType.HistogramWithBuckets, AggregationTemporality.Cumulative, 1024, Metric.DefaultHistogramBounds, Metric.DefaultExponentialHistogramMaxBuckets);
[Fact]
public void HistogramDistributeToAllBucketsDefault()
{
- var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, Metric.DefaultHistogramBounds);
+ var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, Metric.DefaultHistogramBounds, Metric.DefaultExponentialHistogramMaxBuckets);
histogramPoint.Update(-1);
histogramPoint.Update(0);
histogramPoint.Update(2);
@@ -76,7 +76,7 @@ public void HistogramDistributeToAllBucketsDefault()
public void HistogramDistributeToAllBucketsCustom()
{
var boundaries = new double[] { 10, 20 };
- var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries);
+ var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets);
// 5 recordings <=10
histogramPoint.Update(-10);
@@ -124,7 +124,7 @@ public void HistogramBinaryBucketTest()
boundaries[i] = i;
}
- var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries);
+ var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.HistogramWithBuckets, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets);
// Act
histogramPoint.Update(-1);
@@ -157,7 +157,7 @@ public void HistogramBinaryBucketTest()
public void HistogramWithOnlySumCount()
{
var boundaries = Array.Empty();
- var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries);
+ var histogramPoint = new MetricPoint(this.aggregatorStore, AggregationType.Histogram, null, boundaries, Metric.DefaultExponentialHistogramMaxBuckets);
histogramPoint.Update(-10);
histogramPoint.Update(0);
@@ -182,5 +182,95 @@ public void HistogramWithOnlySumCount()
var enumerator = histogramPoint.GetHistogramBuckets().GetEnumerator();
Assert.False(enumerator.MoveNext());
}
+
+ [Theory]
+ [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative)]
+ [InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Delta)]
+ [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Cumulative)]
+ [InlineData(AggregationType.Base2ExponentialHistogramWithMinMax, AggregationTemporality.Delta)]
+ internal void ExponentialHistogramTests(AggregationType aggregationType, AggregationTemporality aggregationTemporality)
+ {
+ var aggregatorStore = new AggregatorStore(
+ $"{nameof(this.ExponentialHistogramTests)}",
+ aggregationType,
+ aggregationTemporality,
+ maxMetricPoints: 1024,
+ Metric.DefaultHistogramBounds,
+ Metric.DefaultExponentialHistogramMaxBuckets);
+
+ var metricPoint = new MetricPoint(
+ aggregatorStore,
+ aggregationType, // TODO: Why is this here? AggregationType is already declared when AggregatorStore was instantiated.
+ tagKeysAndValues: null,
+ Metric.DefaultHistogramBounds,
+ Metric.DefaultExponentialHistogramMaxBuckets);
+
+ metricPoint.Update(-10);
+ metricPoint.Update(0);
+ metricPoint.Update(1);
+ metricPoint.Update(9);
+ metricPoint.Update(10);
+ metricPoint.Update(11);
+ metricPoint.Update(19);
+
+ metricPoint.TakeSnapshot(aggregationTemporality == AggregationTemporality.Delta); // TODO: Why outputDelta param? The aggregation temporality was declared when instantiateing the AggregatorStore.
+
+ var count = metricPoint.GetHistogramCount();
+ var sum = metricPoint.GetHistogramSum();
+ var hasMinMax = metricPoint.TryGetHistogramMinMaxValues(out var min, out var max);
+
+ Assert.Equal(40, sum);
+ Assert.Equal(7, count);
+
+ if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax)
+ {
+ Assert.True(hasMinMax);
+ Assert.Equal(-10, min);
+ Assert.Equal(19, max);
+ }
+ else
+ {
+ Assert.False(hasMinMax);
+ }
+
+ metricPoint.TakeSnapshot(aggregationTemporality == AggregationTemporality.Delta);
+
+ count = metricPoint.GetHistogramCount();
+ sum = metricPoint.GetHistogramSum();
+ hasMinMax = metricPoint.TryGetHistogramMinMaxValues(out min, out max);
+
+ if (aggregationTemporality == AggregationTemporality.Cumulative)
+ {
+ Assert.Equal(40, sum);
+ Assert.Equal(7, count);
+
+ if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax)
+ {
+ Assert.True(hasMinMax);
+ Assert.Equal(-10, min);
+ Assert.Equal(19, max);
+ }
+ else
+ {
+ Assert.False(hasMinMax);
+ }
+ }
+ else
+ {
+ Assert.Equal(0, sum);
+ Assert.Equal(0, count);
+
+ if (aggregationType == AggregationType.Base2ExponentialHistogramWithMinMax)
+ {
+ Assert.True(hasMinMax);
+ Assert.Equal(double.PositiveInfinity, min);
+ Assert.Equal(double.NegativeInfinity, max);
+ }
+ else
+ {
+ Assert.False(hasMinMax);
+ }
+ }
+ }
}
}
diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs
index f4012b52d11..e3e3857dfd6 100644
--- a/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs
+++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderBuilderExtensionsTests.cs
@@ -108,7 +108,7 @@ public void SetAndConfigureResourceTest()
Assert.Empty(builder.ResourceDetectors);
- builder.AddDetector(sp =>
+ builder.AddDetectorInternal(sp =>
{
serviceProviderTestExecuted = true;
Assert.NotNull(sp);
diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs
new file mode 100644
index 00000000000..5595fc05cf4
--- /dev/null
+++ b/test/OpenTelemetry.Tests/Metrics/MetricExemplarTests.cs
@@ -0,0 +1,214 @@
+//
+// 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.Diagnostics;
+using System.Diagnostics.Metrics;
+using OpenTelemetry.Tests;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace OpenTelemetry.Metrics.Tests
+{
+ public class MetricExemplarTests : MetricTestsBase
+ {
+ private const int MaxTimeToAllowForFlush = 10000;
+ private readonly ITestOutputHelper output;
+
+ public MetricExemplarTests(ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ [Fact]
+ public void TestExemplarsCounter()
+ {
+ DateTime testStartTime = DateTime.UtcNow;
+ var exportedItems = new List();
+
+ using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
+ var counter = meter.CreateCounter("testCounter");
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(meter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
+ })
+ .Build();
+
+ var measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ counter.Add(value);
+ }
+
+ meterProvider.ForceFlush(MaxTimeToAllowForFlush);
+ var metricPoint = GetFirstMetricPoint(exportedItems);
+ Assert.NotNull(metricPoint);
+ Assert.True(metricPoint.Value.StartTime >= testStartTime);
+ Assert.True(metricPoint.Value.EndTime != default);
+ var exemplars = GetExemplars(metricPoint.Value);
+ ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false);
+
+ exportedItems.Clear();
+
+#if NETFRAMEWORK
+ Thread.Sleep(10); // Compensates for low resolution timing in netfx.
+#endif
+
+ measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ var act = new Activity("test").Start();
+ counter.Add(value);
+ act.Stop();
+ }
+
+ meterProvider.ForceFlush(MaxTimeToAllowForFlush);
+ metricPoint = GetFirstMetricPoint(exportedItems);
+ Assert.NotNull(metricPoint);
+ Assert.True(metricPoint.Value.StartTime >= testStartTime);
+ Assert.True(metricPoint.Value.EndTime != default);
+ exemplars = GetExemplars(metricPoint.Value);
+ ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true);
+ }
+
+ [Fact]
+ public void TestExemplarsHistogram()
+ {
+ DateTime testStartTime = DateTime.UtcNow;
+ var exportedItems = new List();
+
+ using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
+ var histogram = meter.CreateHistogram("testHistogram");
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(meter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
+ })
+ .Build();
+
+ var measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ histogram.Record(value);
+ }
+
+ meterProvider.ForceFlush(MaxTimeToAllowForFlush);
+ var metricPoint = GetFirstMetricPoint(exportedItems);
+ Assert.NotNull(metricPoint);
+ Assert.True(metricPoint.Value.StartTime >= testStartTime);
+ Assert.True(metricPoint.Value.EndTime != default);
+ var exemplars = GetExemplars(metricPoint.Value);
+ ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, false);
+
+ exportedItems.Clear();
+
+#if NETFRAMEWORK
+ Thread.Sleep(10); // Compensates for low resolution timing in netfx.
+#endif
+
+ measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ using var act = new Activity("test").Start();
+ histogram.Record(value);
+ }
+
+ meterProvider.ForceFlush(MaxTimeToAllowForFlush);
+ metricPoint = GetFirstMetricPoint(exportedItems);
+ Assert.NotNull(metricPoint);
+ Assert.True(metricPoint.Value.StartTime >= testStartTime);
+ Assert.True(metricPoint.Value.EndTime != default);
+ exemplars = GetExemplars(metricPoint.Value);
+ ValidateExemplars(exemplars, metricPoint.Value.StartTime, metricPoint.Value.EndTime, measurementValues, true);
+ }
+
+ [Fact]
+ public void TestExemplarsFilterTags()
+ {
+ DateTime testStartTime = DateTime.UtcNow;
+ var exportedItems = new List();
+
+ using var meter = new Meter($"{Utils.GetCurrentMethodName()}");
+ var histogram = meter.CreateHistogram("testHistogram");
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddMeter(meter.Name)
+ .SetExemplarFilter(new AlwaysOnExemplarFilter())
+ .AddView(histogram.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "key1" } })
+ .AddInMemoryExporter(exportedItems, metricReaderOptions =>
+ {
+ metricReaderOptions.TemporalityPreference = MetricReaderTemporalityPreference.Delta;
+ })
+ .Build();
+
+ var measurementValues = GenerateRandomValues(10);
+ foreach (var value in measurementValues)
+ {
+ histogram.Record(value, new("key1", "value1"), new("key2", "value1"), new("key3", "value1"));
+ }
+
+ meterProvider.ForceFlush(MaxTimeToAllowForFlush);
+ var metricPoint = GetFirstMetricPoint(exportedItems);
+ Assert.NotNull(metricPoint);
+ Assert.True(metricPoint.Value.StartTime >= testStartTime);
+ Assert.True(metricPoint.Value.EndTime != default);
+ var exemplars = GetExemplars(metricPoint.Value);
+ Assert.NotNull(exemplars);
+ foreach (var exemplar in exemplars)
+ {
+ Assert.NotNull(exemplar.FilteredTags);
+ Assert.Contains(new("key2", "value1"), exemplar.FilteredTags);
+ Assert.Contains(new("key3", "value1"), exemplar.FilteredTags);
+ }
+ }
+
+ private static double[] GenerateRandomValues(int count)
+ {
+ var random = new Random();
+ var values = new double[count];
+ for (int i = 0; i < count; i++)
+ {
+ values[i] = random.NextDouble();
+ }
+
+ return values;
+ }
+
+ private static void ValidateExemplars(Exemplar[] exemplars, DateTimeOffset startTime, DateTimeOffset endTime, double[] measurementValues, bool traceContextExists)
+ {
+ Assert.NotNull(exemplars);
+ foreach (var exemplar in exemplars)
+ {
+ Assert.True(exemplar.Timestamp >= startTime && exemplar.Timestamp <= endTime, $"{startTime} < {exemplar.Timestamp} < {endTime}");
+ Assert.Contains(exemplar.DoubleValue, measurementValues);
+ Assert.Null(exemplar.FilteredTags);
+ if (traceContextExists)
+ {
+ Assert.NotEqual(default, exemplar.TraceId);
+ Assert.NotEqual(default, exemplar.SpanId);
+ }
+ else
+ {
+ Assert.Equal(default, exemplar.TraceId);
+ Assert.Equal(default, exemplar.SpanId);
+ }
+ }
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs
index f1d6b228d58..3b3312c3057 100644
--- a/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs
+++ b/test/OpenTelemetry.Tests/Metrics/MetricTestsBase.cs
@@ -122,4 +122,9 @@ public static void CheckTagsForNthMetricPoint(List metrics, List exemplar.Timestamp != default).ToArray();
+ }
}
diff --git a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs b/test/OpenTelemetry.Tests/Resources/ResourceTest.cs
index 419f78281a8..c1ab60fe0c6 100644
--- a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs
+++ b/test/OpenTelemetry.Tests/Resources/ResourceTest.cs
@@ -498,13 +498,39 @@ public void GetResource_WithServiceNameSetWithTwoEnvVarsAndCode()
}
[Fact]
- public void ResourceBuilder_ServiceProvider_Available()
+ public void ResourceBuilder_AddDetector_Test()
+ {
+ bool factoryExecuted = false;
+
+ var builder = ResourceBuilder.CreateDefault();
+
+ builder.AddDetector(sp =>
+ {
+ factoryExecuted = true;
+ return new NoopResourceDetector();
+ });
+
+ Assert.Throws(() => builder.Build());
+ Assert.False(factoryExecuted);
+
+ var serviceCollection = new ServiceCollection();
+ using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+ builder.ServiceProvider = serviceProvider;
+
+ var resource = builder.Build();
+
+ Assert.True(factoryExecuted);
+ }
+
+ [Fact]
+ public void ResourceBuilder_AddDetectorInternal_Test()
{
var builder = ResourceBuilder.CreateDefault();
bool nullTestRun = false;
- builder.AddDetector(sp =>
+ builder.AddDetectorInternal(sp =>
{
nullTestRun = true;
Assert.Null(sp);
@@ -524,7 +550,7 @@ public void ResourceBuilder_ServiceProvider_Available()
builder.ServiceProvider = serviceProvider;
- builder.AddDetector(sp =>
+ builder.AddDetectorInternal(sp =>
{
validTestRun = true;
Assert.NotNull(sp);
diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs
index 8b042d488ae..252f749f4bb 100644
--- a/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs
+++ b/test/OpenTelemetry.Tests/Trace/TracerProviderBuilderExtensionsTest.cs
@@ -196,7 +196,7 @@ public void SetAndConfigureResourceTest()
Assert.Empty(builder.ResourceDetectors);
- builder.AddDetector(sp =>
+ builder.AddDetectorInternal(sp =>
{
serviceProviderTestExecuted = true;
Assert.NotNull(sp);