From 9dd54d7124ab964fe8a1cf8f1458846d510847de Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Mon, 27 Feb 2023 15:57:58 -0800 Subject: [PATCH 01/16] Merge main-metrics to main (#4217) Co-authored-by: Yun-Ting Lin --- .gitignore | 3 + build/Common.prod.props | 1 + docs/metrics/customizing-the-sdk/README.md | 55 ++++ docs/metrics/exemplars/README.md | 99 ++++++ docs/metrics/exemplars/docker-compose.yaml | 51 +++ .../exemplars/grafana-datasources.yaml | 33 ++ docs/metrics/exemplars/otel-collector.yaml | 30 ++ docs/metrics/exemplars/prometheus.yaml | 8 + docs/metrics/exemplars/tempo.yaml | 17 + examples/AspNetCore/Program.cs | 1 + examples/AspNetCore/appsettings.json | 10 + .../ConsoleMetricExporter.cs | 40 +++ .../Implementation/MetricItemExtensions.cs | 35 +++ .../.publicApi/net462/PublicAPI.Unshipped.txt | 25 ++ .../.publicApi/net6.0/PublicAPI.Unshipped.txt | 25 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 25 ++ .../netstandard2.1/PublicAPI.Unshipped.txt | 25 ++ src/OpenTelemetry/Metrics/AggregatorStore.cs | 53 +++- .../Builder/MeterProviderBuilderExtensions.cs | 20 ++ .../Builder/MeterProviderBuilderSdk.cs | 11 + ...AlignedHistogramBucketExemplarReservoir.cs | 112 +++++++ .../Exemplar/AlwaysOffExemplarFilter.cs | 34 ++ .../Exemplar/AlwaysOnExemplarFilter.cs | 33 ++ .../Metrics/Exemplar/Exemplar.cs | 54 ++++ .../Metrics/Exemplar/ExemplarFilter.cs | 58 ++++ .../Metrics/Exemplar/ExemplarReservoir.cs | 42 +++ .../Exemplar/TraceBasedExemplarFilter.cs | 36 +++ src/OpenTelemetry/Metrics/HistogramBuckets.cs | 10 +- src/OpenTelemetry/Metrics/MeterProviderSdk.cs | 1 + src/OpenTelemetry/Metrics/Metric.cs | 5 +- src/OpenTelemetry/Metrics/MetricPoint.cs | 297 ++++++++++++++---- .../Metrics/MetricPointOptionalComponents.cs | 38 +++ src/OpenTelemetry/Metrics/MetricReaderExt.cs | 11 +- src/OpenTelemetry/ReadOnlyTagCollection.cs | 8 +- test/Benchmarks/Metrics/ExemplarBenchmarks.cs | 116 +++++++ 35 files changed, 1346 insertions(+), 76 deletions(-) create mode 100644 docs/metrics/exemplars/README.md create mode 100644 docs/metrics/exemplars/docker-compose.yaml create mode 100644 docs/metrics/exemplars/grafana-datasources.yaml create mode 100644 docs/metrics/exemplars/otel-collector.yaml create mode 100644 docs/metrics/exemplars/prometheus.yaml create mode 100644 docs/metrics/exemplars/tempo.yaml create mode 100644 src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/AlwaysOffExemplarFilter.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ExemplarFilter.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs create mode 100644 src/OpenTelemetry/Metrics/Exemplar/TraceBasedExemplarFilter.cs create mode 100644 src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs create mode 100644 test/Benchmarks/Metrics/ExemplarBenchmarks.cs 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/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..b671d7c7070 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -403,6 +403,61 @@ 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. + +#### 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. Use this to turn-off Exemplar feature. +* `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. It can be turned off by using `AlwaysOffExemplarFilter`. + +#### ExemplarReservoir + +`ExemplarReservoir` receives the measurements sampled in by the `ExemplarFilter` +and is responsible for storing Exemplars. +`AlignedHistogramBucketExemplarReservoir` is the default reservoir used for +Histograms with buckets, and it stores one exemplar per histogram bucket. The +exemplar stored is the last measurement recorded - i.e. any new measurement +overwrites the previous one. + +Currently there is no ability to change the Reservoir used. + ### 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/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.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.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/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt index e69de29bb2d..8162e6986d8 100644 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,25 @@ +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.DateTime +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 diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt index e69de29bb2d..8162e6986d8 100644 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1,25 @@ +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.DateTime +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 diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..8162e6986d8 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,25 @@ +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.DateTime +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 diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index e69de29bb2d..8162e6986d8 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -0,0 +1,25 @@ +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.DateTime +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 diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 5b8909ef836..558691fa6c8 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -41,6 +41,7 @@ internal sealed class AggregatorStore 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 +53,8 @@ internal AggregatorStore( AggregationTemporality temporality, int maxMetricPoints, double[] histogramBounds, - string[] tagKeysInteresting = null) + string[] tagKeysInteresting = null, + ExemplarFilter exemplarFilter = null) { this.name = name; this.maxMetricPoints = maxMetricPoints; @@ -63,6 +65,8 @@ internal AggregatorStore( this.outputDelta = temporality == AggregationTemporality.Delta; this.histogramBounds = histogramBounds; this.StartTimeExclusive = DateTimeOffset.UtcNow; + + this.exemplarFilter = exemplarFilter ?? new AlwaysOffExemplarFilter(); if (tagKeysInteresting == null) { this.updateLongCallback = this.UpdateLong; @@ -86,6 +90,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); @@ -309,7 +320,15 @@ 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.exemplarFilter.ShouldSample(value, tags)) + { + this.metricPoints[index].UpdateWithExemplar(value, tags: default); + } + else + { + this.metricPoints[index].Update(value); + } } catch (Exception) { @@ -332,7 +351,15 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan + /// 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) + { + 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..81ac7bbcec6 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs @@ -0,0 +1,112 @@ +// +// 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 = DateTime.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. + 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/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs new file mode 100644 index 00000000000..79adb9eeba3 --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/AlwaysOnExemplarFilter.cs @@ -0,0 +1,33 @@ +// +// 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 all measurements eligible for being an Exemplar. +/// +public sealed class AlwaysOnExemplarFilter : ExemplarFilter +{ + public override bool ShouldSample(long value, ReadOnlySpan> tags) + { + return true; + } + + public override bool ShouldSample(double value, ReadOnlySpan> tags) + { + return true; + } +} diff --git a/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs b/src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs new file mode 100644 index 00000000000..aa9fb938e48 --- /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. + /// + public DateTime 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..9531caeab0f --- /dev/null +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -0,0 +1,42 @@ +// +// 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); + + public abstract Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset); +} 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/HistogramBuckets.cs b/src/OpenTelemetry/Metrics/HistogramBuckets.cs index f0cf3938299..651fac3464d 100644 --- a/src/OpenTelemetry/Metrics/HistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/HistogramBuckets.cs @@ -32,6 +32,8 @@ public class HistogramBuckets internal readonly long[] RunningBucketCounts; internal readonly long[] SnapshotBucketCounts; + internal readonly ExemplarReservoir ExemplarReservoir; + internal double RunningSum; internal double SnapshotSum; @@ -43,11 +45,13 @@ public class HistogramBuckets internal int IsCriticalSectionOccupied = 0; + internal Exemplar[] Exemplars; + private readonly BucketLookupNode bucketLookupTreeRoot; private readonly Func findHistogramBucketIndex; - internal HistogramBuckets(double[] explicitBounds) + internal HistogramBuckets(double[] explicitBounds, bool enableExemplar = false) { this.ExplicitBounds = explicitBounds; this.findHistogramBucketIndex = this.FindBucketIndexLinear; @@ -77,6 +81,10 @@ static BucketLookupNode ConstructBalancedBST(double[] values, int min, int max) this.RunningBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : null; this.SnapshotBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : new long[0]; + if (explicitBounds != null && enableExemplar) + { + this.ExemplarReservoir = new AlignedHistogramBucketExemplarReservoir(explicitBounds.Length); + } } public Enumerator GetEnumerator() => new(this); 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..779183f230e 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -33,7 +33,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 +127,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, tagKeysInteresting, exemplarFilter); this.Temporality = temporality; this.InstrumentDisposed = false; } diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 8846ed86f3a..474bd236c54 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -28,7 +28,7 @@ public struct MetricPoint 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; @@ -57,16 +57,18 @@ internal MetricPoint( if (this.aggType == AggregationType.HistogramWithBuckets || this.aggType == AggregationType.HistogramWithMinMaxBuckets) { - this.histogramBuckets = new HistogramBuckets(histogramExplicitBounds); + this.mpComponents = new MetricPointOptionalComponents(); + this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds, aggregatorStore.IsExemplarEnabled()); } 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 { - this.histogramBuckets = null; + this.mpComponents = null; } // Note: Intentionally set last because this is used to detect valid MetricPoints. @@ -214,7 +216,7 @@ public readonly double GetHistogramSum() this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum)); } - return this.histogramBuckets.SnapshotSum; + return this.mpComponents.HistogramBuckets.SnapshotSum; } /// @@ -235,7 +237,7 @@ public readonly HistogramBuckets GetHistogramBuckets() this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets)); } - return this.histogramBuckets; + return this.mpComponents.HistogramBuckets; } /// @@ -250,10 +252,10 @@ 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; } @@ -262,10 +264,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.HistogramBuckets?.Exemplars ?? Array.Empty(); + } + internal readonly MetricPoint Copy() { MetricPoint copy = this; - copy.histogramBuckets = this.histogramBuckets?.Copy(); + copy.mpComponents = this.mpComponents?.Copy(); return copy; } @@ -330,6 +343,67 @@ internal void Update(long number) this.MetricPointStatus = MetricPointStatus.CollectPending; } + internal void UpdateWithExemplar(long number, ReadOnlySpan> tags) + { + switch (this.aggType) + { + case AggregationType.LongSumIncomingDelta: + { + Interlocked.Add(ref this.runningValue.AsLong, number); + break; + } + + case AggregationType.LongSumIncomingCumulative: + { + Interlocked.Exchange(ref this.runningValue.AsLong, number); + break; + } + + case AggregationType.LongGauge: + { + Interlocked.Exchange(ref this.runningValue.AsLong, number); + break; + } + + case AggregationType.Histogram: + { + this.UpdateHistogram((double)number); + break; + } + + case AggregationType.HistogramWithMinMax: + { + this.UpdateHistogramWithMinMax((double)number); + break; + } + + case AggregationType.HistogramWithBuckets: + { + this.UpdateHistogramWithBuckets((double)number, tags, true); + break; + } + + case AggregationType.HistogramWithMinMaxBuckets: + { + this.UpdateHistogramWithBucketsAndMinMax((double)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 Update(double number) { switch (this.aggType) @@ -409,6 +483,85 @@ internal void Update(double number) this.MetricPointStatus = MetricPointStatus.CollectPending; } + internal void UpdateWithExemplar(double number, ReadOnlySpan> tags) + { + switch (this.aggType) + { + case AggregationType.DoubleSumIncomingDelta: + { + double initValue, newValue; + var sw = default(SpinWait); + while (true) + { + initValue = this.runningValue.AsDouble; + + unchecked + { + newValue = initValue + number; + } + + if (initValue == Interlocked.CompareExchange(ref this.runningValue.AsDouble, newValue, initValue)) + { + break; + } + + sw.SpinOnce(); + } + + break; + } + + case AggregationType.DoubleSumIncomingCumulative: + { + Interlocked.Exchange(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.DoubleGauge: + { + Interlocked.Exchange(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.Histogram: + { + this.UpdateHistogram(number); + break; + } + + case AggregationType.HistogramWithMinMax: + { + this.UpdateHistogramWithMinMax(number); + break; + } + + case AggregationType.HistogramWithBuckets: + { + this.UpdateHistogramWithBuckets(number, tags, true); + break; + } + + case AggregationType.HistogramWithMinMaxBuckets: + { + this.UpdateHistogramWithBucketsAndMinMax(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) @@ -510,34 +663,37 @@ internal void TakeSnapshot(bool outputDelta) case AggregationType.HistogramWithBuckets: { + 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; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; if (outputDelta) { this.runningValue.AsLong = 0; - this.histogramBuckets.RunningSum = 0; + histogramBuckets.RunningSum = 0; } - for (int i = 0; i < this.histogramBuckets.RunningBucketCounts.Length; i++) + for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++) { - this.histogramBuckets.SnapshotBucketCounts[i] = this.histogramBuckets.RunningBucketCounts[i]; + histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i]; if (outputDelta) { - this.histogramBuckets.RunningBucketCounts[i] = 0; + histogramBuckets.RunningBucketCounts[i] = 0; } } + histogramBuckets.Exemplars = histogramBuckets.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; } @@ -549,25 +705,26 @@ internal void TakeSnapshot(bool outputDelta) case AggregationType.Histogram: { + 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; + histogramBuckets.SnapshotSum = histogramBuckets.RunningSum; if (outputDelta) { this.runningValue.AsLong = 0; - this.histogramBuckets.RunningSum = 0; + histogramBuckets.RunningSum = 0; } this.MetricPointStatus = MetricPointStatus.NoCollectPending; // Release lock - Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); break; } @@ -579,38 +736,40 @@ internal void TakeSnapshot(bool outputDelta) case AggregationType.HistogramWithMinMaxBuckets: { + 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; } - for (int i = 0; i < this.histogramBuckets.RunningBucketCounts.Length; i++) + for (int i = 0; i < histogramBuckets.RunningBucketCounts.Length; i++) { - this.histogramBuckets.SnapshotBucketCounts[i] = this.histogramBuckets.RunningBucketCounts[i]; + histogramBuckets.SnapshotBucketCounts[i] = histogramBuckets.RunningBucketCounts[i]; if (outputDelta) { - this.histogramBuckets.RunningBucketCounts[i] = 0; + histogramBuckets.RunningBucketCounts[i] = 0; } } + histogramBuckets.Exemplars = histogramBuckets.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; } @@ -622,29 +781,30 @@ internal void TakeSnapshot(bool outputDelta) 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.MetricPointStatus = MetricPointStatus.NoCollectPending; // Release lock - Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); break; } @@ -658,20 +818,21 @@ internal void TakeSnapshot(bool outputDelta) private void UpdateHistogram(double number) { + 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 unchecked { this.runningValue.AsLong++; - this.histogramBuckets.RunningSum += number; + histogramBuckets.RunningSum += number; } // Release lock - Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); break; } @@ -681,22 +842,23 @@ private void UpdateHistogram(double number) private void UpdateHistogramWithMinMax(double number) { + 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 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.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 +866,30 @@ private void UpdateHistogramWithMinMax(double number) } } - private void UpdateHistogramWithBuckets(double number) + private void UpdateHistogramWithBuckets(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) { - int i = this.histogramBuckets.FindBucketIndex(number); + 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.RunningBucketCounts[i]++; + histogramBuckets.RunningSum += number; + histogramBuckets.RunningBucketCounts[i]++; + if (reportExemplar) + { + histogramBuckets.ExemplarReservoir.Offer(number, tags, i); + } } // Release lock - Interlocked.Exchange(ref this.histogramBuckets.IsCriticalSectionOccupied, 0); + Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); break; } @@ -730,27 +897,33 @@ private void UpdateHistogramWithBuckets(double number) } } - private void UpdateHistogramWithBucketsAndMinMax(double number) + private void UpdateHistogramWithBucketsAndMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) { - int i = this.histogramBuckets.FindBucketIndex(number); + 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.RunningBucketCounts[i]++; - 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) + { + histogramBuckets.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; } diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs new file mode 100644 index 00000000000..bba755801d3 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -0,0 +1,38 @@ +// +// 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; + + internal MetricPointOptionalComponents Copy() + { + MetricPointOptionalComponents copy = new MetricPointOptionalComponents(); + copy.HistogramBuckets = this.HistogramBuckets.Copy(); + + 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/test/Benchmarks/Metrics/ExemplarBenchmarks.cs b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs new file mode 100644 index 00000000000..1e2d4f337be --- /dev/null +++ b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs @@ -0,0 +1,116 @@ +// +// 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 11 (10.0.22621.1265) +11th Gen Intel Core i7-1185G7 3.00GHz, 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 | EnableExemplar | Mean | Error | StdDev | Allocated | +|-------------------------- |--------------- |---------:|--------:|--------:|----------:| +| HistogramNoTagReduction | False | 256.5 ns | 4.84 ns | 4.53 ns | - | +| HistogramWithTagReduction | False | 246.6 ns | 4.90 ns | 4.81 ns | - | +| HistogramNoTagReduction | True | 286.4 ns | 5.30 ns | 7.25 ns | - | +| HistogramWithTagReduction | True | 293.6 ns | 5.77 ns | 7.09 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; + + [Params(true, false)] + public bool EnableExemplar { 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(); + + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .SetExemplarFilter(this.EnableExemplar ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .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); + } + } +} From afd677f67900fe58a460579534c9534a79fae6e8 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Tue, 28 Feb 2023 10:49:34 -0800 Subject: [PATCH 02/16] Minor Exemplar fixes (#4244) --- .../.publicApi/net462/PublicAPI.Unshipped.txt | 2 +- .../.publicApi/net6.0/PublicAPI.Unshipped.txt | 2 +- .../.publicApi/netstandard2.0/PublicAPI.Unshipped.txt | 2 +- .../.publicApi/netstandard2.1/PublicAPI.Unshipped.txt | 2 +- src/OpenTelemetry/CHANGELOG.md | 3 +++ .../Metrics/Builder/MeterProviderBuilderExtensions.cs | 2 ++ .../AlignedHistogramBucketExemplarReservoir.cs | 4 +++- src/OpenTelemetry/Metrics/Exemplar/Exemplar.cs | 4 ++-- .../Metrics/Exemplar/ExemplarReservoir.cs | 10 ++++++++++ src/OpenTelemetry/Metrics/MetricPoint.cs | 4 ++-- 10 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt index 8162e6986d8..c9041c2b48c 100644 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt @@ -6,7 +6,7 @@ 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.DateTime +OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void diff --git a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt index 8162e6986d8..c9041c2b48c 100644 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -6,7 +6,7 @@ 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.DateTime +OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index 8162e6986d8..c9041c2b48c 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -6,7 +6,7 @@ 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.DateTime +OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void diff --git a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index 8162e6986d8..c9041c2b48c 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -6,7 +6,7 @@ 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.DateTime +OpenTelemetry.Metrics.Exemplar.Timestamp.get -> System.DateTimeOffset OpenTelemetry.Metrics.Exemplar.TraceId.get -> System.Diagnostics.ActivityTraceId? OpenTelemetry.Metrics.ExemplarFilter OpenTelemetry.Metrics.ExemplarFilter.ExemplarFilter() -> void diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index a213f19fa68..a7535102bb7 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Added Exemplar support. + ([#4217](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4217)) + ## 1.4.0 Released 2023-Feb-24 diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index 9a200177bb8..21763741efe 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -312,6 +312,8 @@ public static MeterProviderBuilder ConfigureResource(this MeterProviderBuilder m /// 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) diff --git a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs index 81ac7bbcec6..73090ce9949 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/AlignedHistogramBucketExemplarReservoir.cs @@ -74,7 +74,7 @@ public override Exemplar[] Collect(ReadOnlyTagCollection actualTags, bool reset) private void OfferAtBoundary(double value, ReadOnlySpan> tags, int index) { ref var exemplar = ref this.runningExemplars[index]; - exemplar.Timestamp = DateTime.UtcNow; + exemplar.Timestamp = DateTimeOffset.UtcNow; exemplar.DoubleValue = value; exemplar.TraceId = Activity.Current?.TraceId; exemplar.SpanId = Activity.Current?.SpanId; @@ -104,6 +104,8 @@ private void OfferAtBoundary(double value, ReadOnlySpan - /// Gets the timestamp. + /// Gets the timestamp (UTC). /// - public DateTime Timestamp { get; internal set; } + public DateTimeOffset Timestamp { get; internal set; } /// /// Gets the TraceId. diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index 9531caeab0f..a92cb1b9e1b 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -38,5 +38,15 @@ internal abstract class ExemplarReservoir /// 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 + /// 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/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 474bd236c54..fa7d985abe5 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -85,12 +85,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; From 19a281e54170878e0b4340d059356ad228b1d524 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Tue, 28 Feb 2023 13:30:21 -0800 Subject: [PATCH 03/16] Add benchmarks to show impact of ExemplarFilter (#4245) --- .../Metrics/Exemplar/ExemplarReservoir.cs | 2 +- test/Benchmarks/Metrics/ExemplarBenchmarks.cs | 55 +++++++++++++++---- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs index a92cb1b9e1b..59b530e9d56 100644 --- a/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs +++ b/src/OpenTelemetry/Metrics/Exemplar/ExemplarReservoir.cs @@ -43,7 +43,7 @@ internal abstract class ExemplarReservoir /// /// The actual tags that are part of the metric. Exemplars are /// only expected to contain any filtered tags, so this will allow the reservoir - /// prepare the filtered tags from all the tags it is given by doing the + /// 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. diff --git a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs index 1e2d4f337be..97be2bc31f4 100644 --- a/test/Benchmarks/Metrics/ExemplarBenchmarks.cs +++ b/test/Benchmarks/Metrics/ExemplarBenchmarks.cs @@ -23,19 +23,21 @@ /* // * Summary * -BenchmarkDotNet=v0.13.3, OS=Windows 11 (10.0.22621.1265) -11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores +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 | EnableExemplar | Mean | Error | StdDev | Allocated | -|-------------------------- |--------------- |---------:|--------:|--------:|----------:| -| HistogramNoTagReduction | False | 256.5 ns | 4.84 ns | 4.53 ns | - | -| HistogramWithTagReduction | False | 246.6 ns | 4.90 ns | 4.81 ns | - | -| HistogramNoTagReduction | True | 286.4 ns | 5.30 ns | 7.25 ns | - | -| HistogramWithTagReduction | True | 293.6 ns | 5.77 ns | 7.09 ns | - | +| 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 | */ @@ -52,8 +54,16 @@ public class ExemplarBenchmarks private MeterProvider provider; private Meter meter; - [Params(true, false)] - public bool EnableExemplar { get; set; } + [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() @@ -63,9 +73,19 @@ public void Setup() 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(this.EnableExemplar ? new AlwaysOnExemplarFilter() : new AlwaysOffExemplarFilter()) + .SetExemplarFilter(exemplarFilter) .AddView("HistogramWithTagReduction", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) .AddInMemoryExporter(exportedItems, metricReaderOptions => { @@ -112,5 +132,18 @@ public void HistogramWithTagReduction() 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; + } + } } } From 690f7e5bbcf2cbcaba47b595b16446ac628377b9 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Tue, 28 Feb 2023 21:05:50 -0800 Subject: [PATCH 04/16] Add basic ExemplarFilter doc. (#4246) Co-authored-by: Utkarsh Umesan Pillai --- docs/metrics/customizing-the-sdk/README.md | 9 +++++ docs/metrics/extending-the-sdk/README.md | 46 +++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md index b671d7c7070..5608ee51687 100644 --- a/docs/metrics/customizing-the-sdk/README.md +++ b/docs/metrics/customizing-the-sdk/README.md @@ -447,6 +447,15 @@ using var meterProvider = Sdk.CreateMeterProviderBuilder() > As of today, there is no separate toggle for enable/disable Exemplar feature. It can be turned off by using `AlwaysOffExemplarFilter`. +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` 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. From 869dccee150547cb8545eca38e29100b115382d4 Mon Sep 17 00:00:00 2001 From: Vishwesh Bankwar Date: Wed, 1 Mar 2023 11:34:25 -0800 Subject: [PATCH 05/16] [HttpClient] Fix missing metric during network failures (#4098) Co-authored-by: Cijo Thomas --- .../CHANGELOG.md | 4 + .../HttpHandlerMetricsDiagnosticListener.cs | 11 ++- .../HttpClientTests.cs | 75 ++++++++++--------- .../http-out-test-cases.json | 18 +++-- 4 files changed, 59 insertions(+), 49 deletions(-) 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/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}/" } }, { From 882006e753b24a7090d016d4becb648af3aa6e0a Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Wed, 1 Mar 2023 16:16:29 -0800 Subject: [PATCH 06/16] Move exemplarreservoir to outside of Histogram (#4250) --- src/OpenTelemetry/Metrics/HistogramBuckets.cs | 10 +----- src/OpenTelemetry/Metrics/MetricPoint.cs | 16 +++++---- .../Metrics/MetricPointOptionalComponents.cs | 4 +++ .../Program.cs | 34 ++++++++++--------- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/OpenTelemetry/Metrics/HistogramBuckets.cs b/src/OpenTelemetry/Metrics/HistogramBuckets.cs index 651fac3464d..f0cf3938299 100644 --- a/src/OpenTelemetry/Metrics/HistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/HistogramBuckets.cs @@ -32,8 +32,6 @@ public class HistogramBuckets internal readonly long[] RunningBucketCounts; internal readonly long[] SnapshotBucketCounts; - internal readonly ExemplarReservoir ExemplarReservoir; - internal double RunningSum; internal double SnapshotSum; @@ -45,13 +43,11 @@ public class HistogramBuckets internal int IsCriticalSectionOccupied = 0; - internal Exemplar[] Exemplars; - private readonly BucketLookupNode bucketLookupTreeRoot; private readonly Func findHistogramBucketIndex; - internal HistogramBuckets(double[] explicitBounds, bool enableExemplar = false) + internal HistogramBuckets(double[] explicitBounds) { this.ExplicitBounds = explicitBounds; this.findHistogramBucketIndex = this.FindBucketIndexLinear; @@ -81,10 +77,6 @@ static BucketLookupNode ConstructBalancedBST(double[] values, int min, int max) this.RunningBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : null; this.SnapshotBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : new long[0]; - if (explicitBounds != null && enableExemplar) - { - this.ExemplarReservoir = new AlignedHistogramBucketExemplarReservoir(explicitBounds.Length); - } } public Enumerator GetEnumerator() => new(this); diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index fa7d985abe5..16c915aca08 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -58,7 +58,11 @@ internal MetricPoint( this.aggType == AggregationType.HistogramWithMinMaxBuckets) { this.mpComponents = new MetricPointOptionalComponents(); - this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds, aggregatorStore.IsExemplarEnabled()); + this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds); + if (aggregatorStore.IsExemplarEnabled()) + { + this.mpComponents.ExemplarReservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds.Length); + } } else if (this.aggType == AggregationType.Histogram || this.aggType == AggregationType.HistogramWithMinMax) @@ -272,7 +276,7 @@ public bool TryGetHistogramMinMaxValues(out double min, out double max) public readonly Exemplar[] GetExemplars() { // TODO: Do not expose Exemplar data structure (array now) - return this.mpComponents.HistogramBuckets?.Exemplars ?? Array.Empty(); + return this.mpComponents.Exemplars ?? Array.Empty(); } internal readonly MetricPoint Copy() @@ -688,7 +692,7 @@ internal void TakeSnapshot(bool outputDelta) } } - histogramBuckets.Exemplars = histogramBuckets.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); this.MetricPointStatus = MetricPointStatus.NoCollectPending; @@ -765,7 +769,7 @@ internal void TakeSnapshot(bool outputDelta) } } - histogramBuckets.Exemplars = histogramBuckets.ExemplarReservoir?.Collect(this.Tags, outputDelta); + this.mpComponents.Exemplars = this.mpComponents.ExemplarReservoir?.Collect(this.Tags, outputDelta); this.MetricPointStatus = MetricPointStatus.NoCollectPending; // Release lock @@ -884,7 +888,7 @@ private void UpdateHistogramWithBuckets(double number, ReadOnlySpan 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)])); + } } From c9ec50314ed078ff348b36783207027aae7f8f06 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Wed, 1 Mar 2023 18:08:59 -0800 Subject: [PATCH 07/16] Remove unused eventsource methods (#4221) Co-authored-by: Utkarsh Umesan Pillai --- .../Internal/OpenTelemetryApiEventSource.cs | 6 - .../Internal/OpenTelemetrySdkEventSource.cs | 128 ++---------------- .../SelfDiagnosticsConfigRefresherTest.cs | 6 +- 3 files changed, 11 insertions(+), 129 deletions(-) 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/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/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); From 1b1e4e53dce2a9e2e82bee7d5b81b0ec54731e82 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Thu, 2 Mar 2023 18:32:35 -0800 Subject: [PATCH 08/16] Enabling Exemplars for all types of metrics (#4256) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 20 +- .../Exemplar/SimpleExemplarReservoir.cs | 149 ++++++ src/OpenTelemetry/Metrics/MetricPoint.cs | 444 +++++++++++++++++- .../Metrics/MetricPointOptionalComponents.cs | 2 + 4 files changed, 592 insertions(+), 23 deletions(-) create mode 100644 src/OpenTelemetry/Metrics/Exemplar/SimpleExemplarReservoir.cs diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 558691fa6c8..07a035813ff 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -134,7 +134,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++; } @@ -155,7 +163,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++; } 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/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs index 16c915aca08..54bf087ff6e 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -24,6 +24,9 @@ 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; @@ -54,6 +57,7 @@ internal MetricPoint( this.deltaLastValue = default; this.MetricPointStatus = MetricPointStatus.NoCollectPending; + ExemplarReservoir reservoir = null; if (this.aggType == AggregationType.HistogramWithBuckets || this.aggType == AggregationType.HistogramWithMinMaxBuckets) { @@ -61,7 +65,7 @@ internal MetricPoint( this.mpComponents.HistogramBuckets = new HistogramBuckets(histogramExplicitBounds); if (aggregatorStore.IsExemplarEnabled()) { - this.mpComponents.ExemplarReservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds.Length); + reservoir = new AlignedHistogramBucketExemplarReservoir(histogramExplicitBounds.Length); } } else if (this.aggType == AggregationType.Histogram || @@ -75,6 +79,21 @@ internal MetricPoint( 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. this.aggregatorStore = aggregatorStore; } @@ -276,7 +295,7 @@ public bool TryGetHistogramMinMaxValues(out double min, out double max) public readonly Exemplar[] GetExemplars() { // TODO: Do not expose Exemplar data structure (array now) - return this.mpComponents.Exemplars ?? Array.Empty(); + return this.mpComponents?.Exemplars ?? Array.Empty(); } internal readonly MetricPoint Copy() @@ -353,31 +372,83 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan> tags = default, bool reportExemplar = false) { var histogramBuckets = this.mpComponents.HistogramBuckets; var sw = default(SpinWait); @@ -835,6 +1227,11 @@ private void UpdateHistogram(double number) histogramBuckets.RunningSum += number; } + if (reportExemplar) + { + this.mpComponents.ExemplarReservoir.Offer(number, tags); + } + // Release lock Interlocked.Exchange(ref histogramBuckets.IsCriticalSectionOccupied, 0); break; @@ -844,7 +1241,7 @@ private void UpdateHistogram(double number) } } - private void UpdateHistogramWithMinMax(double number) + private void UpdateHistogramWithMinMax(double number, ReadOnlySpan> tags = default, bool reportExemplar = false) { var histogramBuckets = this.mpComponents.HistogramBuckets; var sw = default(SpinWait); @@ -861,6 +1258,11 @@ private void UpdateHistogramWithMinMax(double 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; diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index 422ab4348e7..94beb8c3631 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -31,6 +31,8 @@ internal sealed class MetricPointOptionalComponents public Exemplar[] Exemplars; + public int IsCriticalSectionOccupied = 0; + internal MetricPointOptionalComponents Copy() { MetricPointOptionalComponents copy = new MetricPointOptionalComponents(); From 3a89cb38b0161183121ce973837c92a6f7f670c6 Mon Sep 17 00:00:00 2001 From: Alan West <3676547+alanwest@users.noreply.github.com> Date: Thu, 2 Mar 2023 18:51:47 -0800 Subject: [PATCH 09/16] Aggregate count, sum, min, and max for exponential histograms (#4060) --- src/OpenTelemetry/Metrics/AggregationType.cs | 10 + src/OpenTelemetry/Metrics/AggregatorStore.cs | 9 +- .../Base2ExponentialBucketHistogram.cs | 11 + src/OpenTelemetry/Metrics/Metric.cs | 4 +- src/OpenTelemetry/Metrics/MetricPoint.cs | 201 +++++++++++++++++- .../Metrics/MetricPointOptionalComponents.cs | 2 + .../Metrics/AggregatorTest.cs | 100 ++++++++- 7 files changed, 324 insertions(+), 13 deletions(-) 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 07a035813ff..1d5785c4212 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -38,6 +38,7 @@ 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; @@ -53,6 +54,7 @@ internal AggregatorStore( AggregationTemporality temporality, int maxMetricPoints, double[] histogramBounds, + int exponentialHistogramMaxBuckets, string[] tagKeysInteresting = null, ExemplarFilter exemplarFilter = null) { @@ -64,6 +66,7 @@ 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(); @@ -189,7 +192,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; } } @@ -256,7 +259,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 @@ -305,7 +308,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 diff --git a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs index e951afcb0e7..1d721864c21 100644 --- a/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs +++ b/src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs @@ -27,6 +27,17 @@ namespace OpenTelemetry.Metrics; /// 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/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index 779183f230e..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; @@ -127,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, exemplarFilter); + 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 54bf087ff6e..0409b377270 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -45,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."); @@ -74,6 +75,12 @@ internal MetricPoint( 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.mpComponents = null; @@ -213,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)); } @@ -234,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.mpComponents.HistogramBuckets.SnapshotSum; + return this.mpComponents.HistogramBuckets != null + ? this.mpComponents.HistogramBuckets.SnapshotSum + : this.mpComponents.Base2ExponentialBucketHistogram.SnapshotSum; } /// @@ -282,6 +295,15 @@ public bool TryGetHistogramMinMaxValues(out double min, out double max) 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; + } + min = 0; max = 0; return false; @@ -350,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: @@ -463,6 +497,18 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan> tags = default, bool reportExemplar = false) +#pragma warning restore IDE0060 // Remove unused parameter + { + var histogram = this.mpComponents.Base2ExponentialBucketHistogram; + + var sw = default(SpinWait); + while (true) + { + if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) + { + // Lock acquired + unchecked + { + this.runningValue.AsLong++; + histogram.RunningSum += number; + } + + // Release lock + Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); + break; + } + + sw.SpinOnce(); + } + } + +#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 + { + var histogram = this.mpComponents.Base2ExponentialBucketHistogram; + + var sw = default(SpinWait); + while (true) + { + if (Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 1) == 0) + { + // Lock acquired + unchecked + { + this.runningValue.AsLong++; + histogram.RunningSum += number; + + histogram.RunningMin = Math.Min(histogram.RunningMin, number); + histogram.RunningMax = Math.Max(histogram.RunningMax, number); + } + + // Release lock + Interlocked.Exchange(ref histogram.IsCriticalSectionOccupied, 0); + break; + } + + sw.SpinOnce(); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] private readonly void ThrowNotSupportedMetricTypeException(string methodName) { diff --git a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs index 94beb8c3631..6c7e648d7ff 100644 --- a/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs +++ b/src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs @@ -27,6 +27,8 @@ internal sealed class MetricPointOptionalComponents { public HistogramBuckets HistogramBuckets; + public Base2ExponentialBucketHistogram Base2ExponentialBucketHistogram; + public ExemplarReservoir ExemplarReservoir; public Exemplar[] Exemplars; 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); + } + } + } } } From 77cd1b03ee86e6b5dba7590081a0083fb85e91a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Fri, 3 Mar 2023 17:46:03 +0100 Subject: [PATCH 10/16] [Instrumentation.SqlClient] Document not supported version (#4258) --- src/OpenTelemetry.Instrumentation.SqlClient/README.md | 5 +++++ 1 file changed, 5 insertions(+) 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). From 735dd0d04c755af7cc1ddfe7565c53757f36d5ab Mon Sep 17 00:00:00 2001 From: "J. Kalyana Sundaram" Date: Fri, 3 Mar 2023 09:45:51 -0800 Subject: [PATCH 11/16] An example / proof of concept for stratified sampling in OpenTelemetry.NET (#4208) Co-authored-by: Timothy Mothra Co-authored-by: Cijo Thomas --- OpenTelemetry.sln | 7 ++ .../stratified-sampling-example/Program.cs | 72 ++++++++++++ .../stratified-sampling-example/README.md | 105 ++++++++++++++++++ .../StratifiedSampler.cs | 80 +++++++++++++ .../stratified-sampling-example.csproj | 14 +++ 5 files changed, 278 insertions(+) create mode 100644 docs/trace/advanced/stratified-sampling-example/Program.cs create mode 100644 docs/trace/advanced/stratified-sampling-example/README.md create mode 100644 docs/trace/advanced/stratified-sampling-example/StratifiedSampler.cs create mode 100644 docs/trace/advanced/stratified-sampling-example/stratified-sampling-example.csproj 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/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 + + + + + + + + From 4425ce52fcabdc72414daa6d57496905d58bc3a3 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Fri, 3 Mar 2023 13:49:44 -0800 Subject: [PATCH 12/16] Fix thread safety for exemplars (#4263) --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 21 +++++++++++--------- src/OpenTelemetry/Metrics/MetricPoint.cs | 14 +++++++++---- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index 1d5785c4212..1eb976cc983 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -68,7 +68,6 @@ internal AggregatorStore( this.histogramBounds = histogramBounds; this.exponentialHistogramMaxBuckets = exponentialHistogramMaxBuckets; this.StartTimeExclusive = DateTimeOffset.UtcNow; - this.exemplarFilter = exemplarFilter ?? new AlwaysOffExemplarFilter(); if (tagKeysInteresting == null) { @@ -340,9 +339,10 @@ private void UpdateLong(long value, ReadOnlySpan> t } // TODO: can special case built-in filters to be bit faster. - if (this.exemplarFilter.ShouldSample(value, tags)) + if (this.IsExemplarEnabled()) { - this.metricPoints[index].UpdateWithExemplar(value, tags: default); + var shouldSample = this.exemplarFilter.ShouldSample(value, tags); + this.metricPoints[index].UpdateWithExemplar(value, tags: default, shouldSample); } else { @@ -371,9 +371,10 @@ private void UpdateLongCustomTags(long value, ReadOnlySpan> tags) + internal void UpdateWithExemplar(long number, ReadOnlySpan> tags, bool isSampled) { switch (this.aggType) { @@ -417,7 +417,10 @@ internal void UpdateWithExemplar(long number, ReadOnlySpan> tags) + internal void UpdateWithExemplar(double number, ReadOnlySpan> tags, bool isSampled) { switch (this.aggType) { @@ -633,7 +636,10 @@ internal void UpdateWithExemplar(double number, ReadOnlySpan Date: Fri, 3 Mar 2023 16:44:22 -0800 Subject: [PATCH 13/16] Cleanup aspnetcore benchmarks and add metrics (#4266) --- test/Benchmarks/Helper/LocalServer.cs | 85 ------------------- test/Benchmarks/Helper/ValuesController.cs | 34 -------- .../AspNetCoreInstrumentationBenchmarks.cs | 31 ++++--- .../InstrumentedAspNetCoreBenchmark.cs | 55 ------------ .../UninstrumentedAspNetCoreBenchmark.cs | 55 ------------ 5 files changed, 18 insertions(+), 242 deletions(-) delete mode 100644 test/Benchmarks/Helper/LocalServer.cs delete mode 100644 test/Benchmarks/Helper/ValuesController.cs delete mode 100644 test/Benchmarks/Instrumentation/InstrumentedAspNetCoreBenchmark.cs delete mode 100644 test/Benchmarks/Instrumentation/UninstrumentedAspNetCoreBenchmark.cs 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/Helper/ValuesController.cs b/test/Benchmarks/Helper/ValuesController.cs deleted file mode 100644 index 41da05856f1..00000000000 --- a/test/Benchmarks/Helper/ValuesController.cs +++ /dev/null @@ -1,34 +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.Collections.Generic; -using Microsoft.AspNetCore.Mvc; - -namespace Benchmark.Helper -{ - [Route("api/[controller]")] - public class ValuesController : Controller - { - // GET api/values - [HttpGet] - public IEnumerable Get() - { - return new string[] { "value1", "value2" }; - } - } -} -#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/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/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 From 8cd0ef5642adf8e3af04c6fd7b341e46d4e20510 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Sat, 4 Mar 2023 22:54:53 -0800 Subject: [PATCH 14/16] HttpClient benchmark updates (#4269) --- .../HttpClientInstrumentationBenchmarks.cs | 118 ++++++++++++++++++ .../InstrumentedHttpClientBenchmark.cs | 86 ------------- .../UninstrumentedHttpClientBenchmark.cs | 59 --------- 3 files changed, 118 insertions(+), 145 deletions(-) create mode 100644 test/Benchmarks/Instrumentation/HttpClientInstrumentationBenchmarks.cs delete mode 100644 test/Benchmarks/Instrumentation/InstrumentedHttpClientBenchmark.cs delete mode 100644 test/Benchmarks/Instrumentation/UninstrumentedHttpClientBenchmark.cs 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/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/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(); - } - } -} From 1a30646546342c1e5eccdbede90de3fa72b0983d Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Sun, 5 Mar 2023 21:28:59 -0800 Subject: [PATCH 15/16] Update Jaeger readme to indicate future deprecation (#4270) --- CONTRIBUTING.md | 4 ---- src/OpenTelemetry.Exporter.Jaeger/README.md | 10 ++++++---- src/OpenTelemetry.Extensions.Hosting/README.md | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) 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/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.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 From a3181306177db537d95b7c6562f0d84fb7b1d2d3 Mon Sep 17 00:00:00 2001 From: Mikel Blanchard Date: Mon, 6 Mar 2023 08:42:55 -0800 Subject: [PATCH 16/16] [sdk] Make ResourceBuilder.AddDetector factory pattern public (attempt 2) (#4261) Co-authored-by: Cijo Thomas --- .../.publicApi/net462/PublicAPI.Unshipped.txt | 1 + .../.publicApi/net6.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.0/PublicAPI.Unshipped.txt | 1 + .../netstandard2.1/PublicAPI.Unshipped.txt | 1 + src/OpenTelemetry/CHANGELOG.md | 3 ++ .../Logs/OpenTelemetryLoggerProvider.cs | 4 +++ src/OpenTelemetry/ProviderExtensions.cs | 4 +++ .../Resources/ResourceBuilder.cs | 26 +++++++++------ .../Resources/ResourceBuilderExtensions.cs | 4 +-- .../MeterProviderBuilderExtensionsTests.cs | 2 +- .../Resources/ResourceTest.cs | 32 +++++++++++++++++-- .../TracerProviderBuilderExtensionsTest.cs | 2 +- 12 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt index c9041c2b48c..955d808767a 100644 --- a/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net462/PublicAPI.Unshipped.txt @@ -23,3 +23,4 @@ static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(th ~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 c9041c2b48c..b334cfe1664 100644 --- a/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -23,3 +23,4 @@ static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(th ~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 c9041c2b48c..b334cfe1664 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -23,3 +23,4 @@ static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(th ~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 c9041c2b48c..b334cfe1664 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -23,3 +23,4 @@ static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetExemplarFilter(th ~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 a7535102bb7..4036a76749a 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -5,6 +5,9 @@ * Added Exemplar support. ([#4217](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4217)) +* 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/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/ProviderExtensions.cs b/src/OpenTelemetry/ProviderExtensions.cs index c83127fdc51..b25753e71ef 100644 --- a/src/OpenTelemetry/ProviderExtensions.cs +++ b/src/OpenTelemetry/ProviderExtensions.cs @@ -73,6 +73,10 @@ public static Resource GetDefaultResource(this BaseProvider baseProvider) { return meterProviderSdk.ServiceProvider; } + else if (baseProvider is OpenTelemetryLoggerProvider openTelemetryLoggerProvider) + { + return openTelemetryLoggerProvider.ServiceProvider; + } return null; } 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/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/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);