From 9dd54d7124ab964fe8a1cf8f1458846d510847de Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Mon, 27 Feb 2023 15:57:58 -0800 Subject: [PATCH] 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 b6d3c01b7..af4092798 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 90119d36d..872830ccb 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 4162afa3d..b671d7c70 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 000000000..0c66303fa --- /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 000000000..87cd7a6c6 --- /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 000000000..467975584 --- /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 000000000..bcf0cb5d6 --- /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 000000000..4d6c5d18a --- /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 000000000..0d46d4718 --- /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 6018cad60..8835f260f 100644 --- a/examples/AspNetCore/Program.cs +++ b/examples/AspNetCore/Program.cs @@ -106,6 +106,7 @@ appBuilder.Services.AddOpenTelemetry() // 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 b6756e818..19f051327 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 db5ac7dcd..f66a34ba7 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -167,6 +167,38 @@ namespace OpenTelemetry.Exporter } } + 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 @@ namespace OpenTelemetry.Exporter 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 31e3438d8..9208eaa3e 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 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation } } + 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 e69de29bb..8162e6986 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 e69de29bb..8162e6986 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 e69de29bb..8162e6986 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 e69de29bb..8162e6986 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 5b8909ef8..558691fa6 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -41,6 +41,7 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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); + } + else + { + this.metricPoints[index].Update(value); + } } catch (Exception) { @@ -355,7 +382,15 @@ namespace OpenTelemetry.Metrics 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) { @@ -378,7 +413,15 @@ namespace OpenTelemetry.Metrics 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); + } + else + { + this.metricPoints[index].Update(value); + } } catch (Exception) { diff --git a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs index 12f46b182..9a200177b 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderExtensions.cs @@ -303,6 +303,26 @@ namespace OpenTelemetry.Metrics return meterProviderBuilder; } + /// + /// Sets the to be used for this provider. + /// This is applied to all the metrics from this provider. + /// + /// . + /// ExemplarFilter to use. + /// The supplied for chaining. + public static MeterProviderBuilder SetExemplarFilter(this MeterProviderBuilder meterProviderBuilder, ExemplarFilter exemplarFilter) + { + 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 ba53a2946..da4571184 100644 --- a/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Metrics/Builder/MeterProviderBuilderSdk.cs @@ -54,6 +54,8 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 000000000..81ac7bbce --- /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 000000000..2d3f3cab8 --- /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 000000000..79adb9eeb --- /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 000000000..aa9fb938e --- /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 000000000..9e79570ce --- /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 000000000..9531caeab --- /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 000000000..69c4c29e9 --- /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 f0cf39382..651fac346 100644 --- a/src/OpenTelemetry/Metrics/HistogramBuckets.cs +++ b/src/OpenTelemetry/Metrics/HistogramBuckets.cs @@ -32,6 +32,8 @@ namespace OpenTelemetry.Metrics internal readonly long[] RunningBucketCounts; internal readonly long[] SnapshotBucketCounts; + internal readonly ExemplarReservoir ExemplarReservoir; + internal double RunningSum; internal double SnapshotSum; @@ -43,11 +45,13 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 d02c34865..cffcf80a4 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -80,6 +80,7 @@ namespace OpenTelemetry.Metrics 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 63338d81c..779183f23 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -33,7 +33,8 @@ namespace OpenTelemetry.Metrics int maxMetricPointsPerMetricStream, double[] histogramBounds = null, string[] tagKeysInteresting = null, - bool histogramRecordMinMax = true) + bool histogramRecordMinMax = true, + ExemplarFilter exemplarFilter = null) { this.InstrumentIdentity = instrumentIdentity; @@ -126,7 +127,7 @@ namespace OpenTelemetry.Metrics 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 8846ed86f..474bd236c 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -28,7 +28,7 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum)); } - return this.histogramBuckets.SnapshotSum; + return this.mpComponents.HistogramBuckets.SnapshotSum; } /// @@ -235,7 +237,7 @@ namespace OpenTelemetry.Metrics this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets)); } - return this.histogramBuckets; + return this.mpComponents.HistogramBuckets; } /// @@ -250,10 +252,10 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics } } - 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 @@ namespace OpenTelemetry.Metrics } } - 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 000000000..bba755801 --- /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 c4710fe7b..df2d30e78 100644 --- a/src/OpenTelemetry/Metrics/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -34,6 +34,8 @@ namespace OpenTelemetry.Metrics private Metric[] metricsCurrentBatch; private int metricIndex = -1; + private ExemplarFilter exemplarFilter; + internal AggregationTemporality GetAggregationTemporality(Type instrumentType) { return this.temporalityFunc(instrumentType); @@ -69,7 +71,7 @@ namespace OpenTelemetry.Metrics 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 @@ namespace OpenTelemetry.Metrics } else { - Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, metricStreamIdentity.HistogramBucketBounds, metricStreamIdentity.TagKeys, metricStreamIdentity.HistogramRecordMinMax); + Metric metric = new(metricStreamIdentity, this.GetAggregationTemporality(metricStreamIdentity.InstrumentType), this.maxMetricPointsPerMetricStream, metricStreamIdentity.HistogramBucketBounds, metricStreamIdentity.TagKeys, metricStreamIdentity.HistogramRecordMinMax, this.exemplarFilter); this.instrumentIdentityToMetric[metricStreamIdentity] = metric; this.metrics[index] = metric; @@ -223,6 +225,11 @@ namespace OpenTelemetry.Metrics this.metricsCurrentBatch = new Metric[maxMetricStreams]; } + internal void SetExemplarFilter(ExemplarFilter exemplarFilter) + { + this.exemplarFilter = exemplarFilter; + } + internal void SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream) { this.maxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream; diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs index ab451626e..1ff8bbfd4 100644 --- a/src/OpenTelemetry/ReadOnlyTagCollection.cs +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -25,17 +25,17 @@ namespace OpenTelemetry // prevent accidental boxing. public readonly struct ReadOnlyTagCollection { - private readonly KeyValuePair[] 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 @@ namespace OpenTelemetry 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 000000000..1e2d4f337 --- /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); + } + } +}