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);