Add support for multiple readers (#2596)

This commit is contained in:
Utkarsh Umesan Pillai 2021-11-19 07:49:47 -08:00 committed by GitHub
parent 03313e7b02
commit ca291422ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 726 additions and 250 deletions

View File

@ -121,7 +121,6 @@ static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Met
static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool
static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool
static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool
static OpenTelemetry.ProviderExtensions.GetMetricCollect(this OpenTelemetry.BaseProvider baseProvider) -> System.Func<OpenTelemetry.Batch<OpenTelemetry.Metrics.Metric>>
static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder
static readonly OpenTelemetry.Metrics.MetricStreamConfiguration.Drop -> OpenTelemetry.Metrics.MetricStreamConfiguration
virtual OpenTelemetry.BaseExporter<T>.OnForceFlush(int timeoutMilliseconds) -> bool

View File

@ -121,7 +121,6 @@ static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Met
static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool
static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool
static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool
static OpenTelemetry.ProviderExtensions.GetMetricCollect(this OpenTelemetry.BaseProvider baseProvider) -> System.Func<OpenTelemetry.Batch<OpenTelemetry.Metrics.Metric>>
static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder
static readonly OpenTelemetry.Metrics.MetricStreamConfiguration.Drop -> OpenTelemetry.Metrics.MetricStreamConfiguration
virtual OpenTelemetry.BaseExporter<T>.OnForceFlush(int timeoutMilliseconds) -> bool

View File

@ -26,30 +26,33 @@
([#2542](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2542))
* Added wildcard support for AddMeter.
([#2459](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2459))
([#2459](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2459))
* Add support for multiple Metric readers
([#2596](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2596))
## 1.2.0-beta1
Released 2021-Oct-08
* Exception from Observable instrument callbacks does not
result in entire metrics being lost.
* Exception from Observable instrument callbacks does not result in entire
metrics being lost.
* SDK is allocation-free on recording of measurements with
upto 8 tags.
* SDK is allocation-free on recording of measurements with upto 8 tags.
* TracerProviderBuilder.AddLegacySource now supports wildcard activity names.
([#2183](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2183))
* Instrument and View names are validated
[according with the spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument).
* Instrument and View names are validated [according with the
spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument).
([#2470](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2470))
## 1.2.0-alpha4
Released 2021-Sep-23
* `BatchExportProcessor.OnShutdown` will now log the count of dropped telemetry items.
* `BatchExportProcessor.OnShutdown` will now log the count of dropped telemetry
items.
([#2331](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2331))
* Changed `CompositeProcessor<T>.OnForceFlush` to meet with the spec
requirement. Now the SDK will invoke `ForceFlush` on all registered
@ -60,14 +63,14 @@ Released 2021-Sep-23
Released 2021-Sep-13
* Metrics perf improvements, bug fixes.
Replace MetricProcessor with MetricReader.
* Metrics perf improvements, bug fixes. Replace MetricProcessor with
MetricReader.
([#2306](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2306))
* Add `BatchExportActivityProcessorOptions` which supports field value overriding
using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`,
`OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
envionmental variables as defined in the
* Add `BatchExportActivityProcessorOptions` which supports field value
overriding using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`,
`OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` envionmental
variables as defined in the
[specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor).
([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219))
@ -75,21 +78,24 @@ Released 2021-Sep-13
Released 2021-Aug-24
* More Metrics features. All instrument types, push/pull
exporters, Delta/Cumulative temporality supported.
* More Metrics features. All instrument types, push/pull exporters,
Delta/Cumulative temporality supported.
* `ResourceBuilder.CreateDefault` has detectors for
`OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME` environment variables
so that explicit `AddEnvironmentVariableDetector` call is not needed. ([#2247](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2247))
* `ResourceBuilder.CreateDefault` has detectors for `OTEL_RESOURCE_ATTRIBUTES`,
`OTEL_SERVICE_NAME` environment variables so that explicit
`AddEnvironmentVariableDetector` call is not needed.
([#2247](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2247))
* `ResourceBuilder.AddEnvironmentVariableDetector` handles `OTEL_SERVICE_NAME`
environmental variable. ([#2209](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2209))
environmental variable.
([#2209](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2209))
* Removes upper constraint for Microsoft.Extensions.Logging
dependencies. ([#2179](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2179))
* Removes upper constraint for Microsoft.Extensions.Logging dependencies.
([#2179](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2179))
* OpenTelemetryLogger modified to not throw, when the
formatter supplied in ILogger.Log call is null. ([#2200](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2200))
* OpenTelemetryLogger modified to not throw, when the formatter supplied in
ILogger.Log call is null.
([#2200](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2200))
## 1.2.0-alpha1
@ -100,7 +106,8 @@ Released 2021-Jul-23
([#2174](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2174))
* Removes .NET Framework 4.5.2, .NET 4.6 support. The minimum .NET Framework
version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138))
version supported is .NET 4.6.1.
([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138))
## 1.1.0
@ -137,8 +144,8 @@ Released 2021-May-11
Released 2021-Apr-23
* Use `AssemblyFileVersionAttribute` instead of `FileVersionInfo.GetVersionInfo`
to get the SDK version attribute to ensure that it works when the assembly
is not loaded directly from a file on disk
to get the SDK version attribute to ensure that it works when the assembly is
not loaded directly from a file on disk
([#1908](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1908))
## 1.1.0-beta1

View File

@ -22,11 +22,15 @@ using OpenTelemetry.Internal;
namespace OpenTelemetry.Metrics
{
internal sealed class CompositeMetricReader : MetricReader
/// <summary>
/// CompositeMetricReader that does not deal with adding metrics and recording measurements.
/// </summary>
internal sealed partial class CompositeMetricReader : MetricReader
{
private readonly DoublyLinkedListNode head;
private DoublyLinkedListNode tail;
private bool disposed;
private int count;
public CompositeMetricReader(IEnumerable<MetricReader> readers)
{
@ -40,6 +44,7 @@ namespace OpenTelemetry.Metrics
this.head = new DoublyLinkedListNode(iter.Current);
this.tail = this.head;
this.count++;
while (iter.MoveNext())
{
@ -57,6 +62,7 @@ namespace OpenTelemetry.Metrics
};
this.tail.Next = node;
this.tail = node;
this.count++;
return this;
}

View File

@ -0,0 +1,149 @@
// <copyright file="CompositeMetricReaderExt.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
namespace OpenTelemetry.Metrics
{
/// <summary>
/// CompositeMetricReader that deals with adding metrics and recording measurements.
/// </summary>
internal sealed partial class CompositeMetricReader
{
internal List<Metric> AddMetricsWithNoViews(Instrument instrument)
{
var metrics = new List<Metric>(this.count);
for (var cur = this.head; cur != null; cur = cur.Next)
{
var metric = cur.Value.AddMetricWithNoViews(instrument);
metrics.Add(metric);
}
return metrics;
}
internal void RecordSingleStreamLongMeasurements(List<Metric> metrics, long value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers.");
int index = 0;
for (var cur = this.head; cur != null; cur = cur.Next)
{
if (metrics[index] != null)
{
cur.Value.RecordSingleStreamLongMeasurement(metrics[index], value, tags);
}
index++;
}
}
internal void RecordSingleStreamDoubleMeasurements(List<Metric> metrics, double value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers.");
int index = 0;
for (var cur = this.head; cur != null; cur = cur.Next)
{
if (metrics[index] != null)
{
cur.Value.RecordSingleStreamDoubleMeasurement(metrics[index], value, tags);
}
index++;
}
}
internal List<List<Metric>> AddMetricsSuperListWithViews(Instrument instrument, List<MetricStreamConfiguration> metricStreamConfigs)
{
var metricsSuperList = new List<List<Metric>>(this.count);
for (var cur = this.head; cur != null; cur = cur.Next)
{
var metrics = cur.Value.AddMetricsListWithViews(instrument, metricStreamConfigs);
metricsSuperList.Add(metrics);
}
return metricsSuperList;
}
internal void RecordLongMeasurements(List<List<Metric>> metricsSuperList, long value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers.");
int index = 0;
for (var cur = this.head; cur != null; cur = cur.Next)
{
if (metricsSuperList[index].Count > 0)
{
cur.Value.RecordLongMeasurement(metricsSuperList[index], value, tags);
}
index++;
}
}
internal void RecordDoubleMeasurements(List<List<Metric>> metricsSuperList, double value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers.");
int index = 0;
for (var cur = this.head; cur != null; cur = cur.Next)
{
if (metricsSuperList[index].Count > 0)
{
cur.Value.RecordDoubleMeasurement(metricsSuperList[index], value, tags);
}
index++;
}
}
internal void CompleteSingleStreamMeasurements(List<Metric> metrics)
{
Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers.");
int index = 0;
for (var cur = this.head; cur != null; cur = cur.Next)
{
if (metrics[index] != null)
{
cur.Value.CompleteSingleStreamMeasurement(metrics[index]);
}
index++;
}
}
internal void CompleteMesaurements(List<List<Metric>> metricsSuperList)
{
Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers.");
int index = 0;
for (var cur = this.head; cur != null; cur = cur.Next)
{
if (metricsSuperList[index].Count > 0)
{
cur.Value.CompleteMeasurement(metricsSuperList[index]);
}
index++;
}
}
}
}

View File

@ -71,11 +71,6 @@ namespace OpenTelemetry.Metrics
internal MeterProviderBuilder AddReader(MetricReader reader)
{
if (this.MetricReaders.Count >= 1)
{
throw new InvalidOperationException("Only one Metricreader is allowed.");
}
this.MetricReaders.Add(reader);
return this;
}

View File

@ -27,18 +27,13 @@ namespace OpenTelemetry.Metrics
{
internal sealed class MeterProviderSdk : MeterProvider
{
internal const int MaxMetrics = 1000;
internal int ShutdownCount;
private readonly Metric[] metrics;
private readonly Metric[] metricsCurrentBatch;
private readonly List<object> instrumentations = new List<object>();
private readonly List<Func<Instrument, MetricStreamConfiguration>> viewConfigs;
private readonly object collectLock = new object();
private readonly object instrumentCreationLock = new object();
private readonly HashSet<string> metricStreamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly MeterListener listener;
private readonly MetricReader reader;
private int metricIndex = -1;
private readonly CompositeMetricReader compositeMetricReader;
private bool disposed;
internal MeterProviderSdk(
@ -50,10 +45,6 @@ namespace OpenTelemetry.Metrics
{
this.Resource = resource;
this.viewConfigs = viewConfigs;
this.metrics = new Metric[MaxMetrics];
this.metricsCurrentBatch = new Metric[MaxMetrics];
AggregationTemporality temporality = AggregationTemporality.Cumulative;
foreach (var reader in readers)
{
@ -61,10 +52,6 @@ namespace OpenTelemetry.Metrics
reader.SetParentProvider(this);
// TODO: Actually support multiple readers.
// Currently the last reader's temporality wins.
temporality = reader.PreferredAggregationTemporality;
if (this.reader == null)
{
this.reader = reader;
@ -79,6 +66,8 @@ namespace OpenTelemetry.Metrics
}
}
this.compositeMetricReader = this.reader as CompositeMetricReader;
if (instrumentationFactories.Any())
{
foreach (var instrumentationFactory in instrumentationFactories)
@ -107,6 +96,9 @@ namespace OpenTelemetry.Metrics
this.listener = new MeterListener();
var viewConfigCount = this.viewConfigs.Count;
// We expect that all the readers to be added are provided before MeterProviderSdk is built.
// If there are no readers added, we do not enable measurements for the instruments.
if (viewConfigCount > 0)
{
this.listener.InstrumentPublished = (instrument, listener) =>
@ -141,73 +133,23 @@ namespace OpenTelemetry.Metrics
metricStreamConfigs.Add(null);
}
var maxCountMetricsToBeCreated = metricStreamConfigs.Count;
// Create list with initial capacity as the max metric count.
// Due to duplicate/max limit, we may not end up using them
// all, and that memory is wasted until Meter disposed.
// TODO: Revisit to see if we need to do metrics.TrimExcess()
var metrics = new List<Metric>(maxCountMetricsToBeCreated);
lock (this.instrumentCreationLock)
if (this.reader != null)
{
for (int i = 0; i < maxCountMetricsToBeCreated; i++)
if (this.compositeMetricReader == null)
{
var metricStreamConfig = metricStreamConfigs[i];
var meterName = instrument.Meter.Name;
var metricName = metricStreamConfig?.Name ?? instrument.Name;
var metricStreamName = $"{meterName}.{metricName}";
if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricName))
var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs);
if (metrics.Count > 0)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
metricName,
instrument.Meter.Name,
"Metric name is invalid.",
"The name must comply with the OpenTelemetry specification.");
continue;
}
if (this.metricStreamNames.Contains(metricStreamName))
{
// TODO: Log that instrument is ignored
// as the resulting Metric name is conflicting
// with existing name.
continue;
}
if (metricStreamConfig?.Aggregation == Aggregation.Drop)
{
// TODO: Log that instrument is ignored
// as user explicitly asked to drop it
// with View.
continue;
}
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
// TODO: Log that instrument is ignored
// as max number of Metrics have reached.
}
else
{
Metric metric;
var metricDescription = metricStreamConfig?.Description ?? instrument.Description;
string[] tagKeysInteresting = metricStreamConfig?.TagKeys;
double[] histogramBucketBounds = (metricStreamConfig is ExplicitBucketHistogramConfiguration histogramConfig
&& histogramConfig.Boundaries != null) ? histogramConfig.Boundaries : null;
metric = new Metric(instrument, temporality, metricName, metricDescription, histogramBucketBounds, tagKeysInteresting);
this.metrics[index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamName);
listener.EnableMeasurementEvents(instrument, metrics);
}
}
if (metrics.Count > 0)
else
{
listener.EnableMeasurementEvents(instrument, metrics);
var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs);
if (metricsSuperList.Any(metrics => metrics.Count > 0))
{
listener.EnableMeasurementEvents(instrument, metricsSuperList);
}
}
}
};
@ -247,33 +189,25 @@ namespace OpenTelemetry.Metrics
return;
}
var meterName = instrument.Meter.Name;
var metricName = instrument.Name;
var metricStreamName = $"{meterName}.{metricName}";
Metric metric = null;
lock (this.instrumentCreationLock)
if (this.reader != null)
{
if (this.metricStreamNames.Contains(metricStreamName))
if (this.compositeMetricReader == null)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using View.");
return;
}
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Maximum allowed Metrics for the provider exceeded.", "Use views to drop unused instruments. Or configure Provider to allow higher limit.");
return;
var metric = this.reader.AddMetricWithNoViews(instrument);
if (metric != null)
{
listener.EnableMeasurementEvents(instrument, metric);
}
}
else
{
metric = new Metric(instrument, temporality, metricName, instrument.Description);
this.metrics[index] = metric;
this.metricStreamNames.Add(metricStreamName);
var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument);
if (metrics.Any(metric => metric != null))
{
listener.EnableMeasurementEvents(instrument, metrics);
}
}
}
listener.EnableMeasurementEvents(instrument, metric);
}
catch (Exception)
{
@ -311,168 +245,175 @@ namespace OpenTelemetry.Metrics
internal void MeasurementsCompletedSingleStream(Instrument instrument, object state)
{
var metric = state as Metric;
if (metric == null)
{
// TODO: log
return;
}
Debug.Assert(instrument != null, "instrument must be non-null.");
metric.InstrumentDisposed = true;
if (this.compositeMetricReader == null)
{
if (state is not Metric metric)
{
// TODO: log
return;
}
this.reader.CompleteSingleStreamMeasurement(metric);
}
else
{
if (state is not List<Metric> metrics)
{
// TODO: log
return;
}
this.compositeMetricReader.CompleteSingleStreamMeasurements(metrics);
}
}
internal void MeasurementsCompleted(Instrument instrument, object state)
{
var metrics = state as List<Metric>;
if (metrics == null)
{
// TODO: log
return;
}
Debug.Assert(instrument != null, "instrument must be non-null.");
foreach (var metric in metrics)
if (this.compositeMetricReader == null)
{
metric.InstrumentDisposed = true;
if (state is not List<Metric> metrics)
{
// TODO: log
return;
}
this.reader.CompleteMeasurement(metrics);
}
else
{
if (state is not List<List<Metric>> metricsSuperList)
{
// TODO: log
return;
}
this.compositeMetricReader.CompleteMesaurements(metricsSuperList);
}
}
internal void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan<KeyValuePair<string, object>> tagsRos, object state)
{
// Get Instrument State
var metrics = state as List<Metric>;
Debug.Assert(instrument != null, "instrument must be non-null.");
if (metrics == null)
{
// TODO: log
return;
}
if (metrics.Count == 1)
if (this.compositeMetricReader == null)
{
// special casing the common path
// as this is faster than the
// foreach, when count is 1.
metrics[0].UpdateDouble(value, tagsRos);
if (state is not List<Metric> metrics)
{
// TODO: log
return;
}
this.reader.RecordDoubleMeasurement(metrics, value, tagsRos);
}
else
{
foreach (var metric in metrics)
if (state is not List<List<Metric>> metricsSuperList)
{
metric.UpdateDouble(value, tagsRos);
// TODO: log
return;
}
this.compositeMetricReader.RecordDoubleMeasurements(metricsSuperList, value, tagsRos);
}
}
internal void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan<KeyValuePair<string, object>> tagsRos, object state)
{
// Get Instrument State
var metrics = state as List<Metric>;
Debug.Assert(instrument != null, "instrument must be non-null.");
if (metrics == null)
{
// TODO: log
return;
}
if (metrics.Count == 1)
if (this.compositeMetricReader == null)
{
// special casing the common path
// as this is faster than the
// foreach, when count is 1.
metrics[0].UpdateLong(value, tagsRos);
if (state is not List<Metric> metrics)
{
// TODO: log
return;
}
this.reader.RecordLongMeasurement(metrics, value, tagsRos);
}
else
{
foreach (var metric in metrics)
if (state is not List<List<Metric>> metricsSuperList)
{
metric.UpdateLong(value, tagsRos);
// TODO: log
return;
}
this.compositeMetricReader.RecordLongMeasurements(metricsSuperList, value, tagsRos);
}
}
internal void MeasurementRecordedLongSingleStream(Instrument instrument, long value, ReadOnlySpan<KeyValuePair<string, object>> tagsRos, object state)
{
// Get Instrument State
var metric = state as Metric;
Debug.Assert(instrument != null, "instrument must be non-null.");
if (metric == null)
{
// TODO: log
return;
}
metric.UpdateLong(value, tagsRos);
if (this.compositeMetricReader == null)
{
if (state is not Metric metric)
{
// TODO: log
return;
}
this.reader.RecordSingleStreamLongMeasurement(metric, value, tagsRos);
}
else
{
if (state is not List<Metric> metrics)
{
// TODO: log
return;
}
this.compositeMetricReader.RecordSingleStreamLongMeasurements(metrics, value, tagsRos);
}
}
internal void MeasurementRecordedDoubleSingleStream(Instrument instrument, double value, ReadOnlySpan<KeyValuePair<string, object>> tagsRos, object state)
{
// Get Instrument State
var metric = state as Metric;
Debug.Assert(instrument != null, "instrument must be non-null.");
if (metric == null)
{
// TODO: log
return;
}
metric.UpdateDouble(value, tagsRos);
if (this.compositeMetricReader == null)
{
if (state is not Metric metric)
{
// TODO: log
return;
}
this.reader.RecordSingleStreamDoubleMeasurement(metric, value, tagsRos);
}
else
{
if (state is not List<Metric> metrics)
{
// TODO: log
return;
}
this.compositeMetricReader.RecordSingleStreamDoubleMeasurements(metrics, value, tagsRos);
}
}
internal Batch<Metric> Collect()
internal void CollectObservableInstruments()
{
lock (this.collectLock)
{
// Record all observable instruments
try
{
// Record all observable instruments
try
{
this.listener.RecordObservableInstruments();
}
catch (Exception exception)
{
// TODO:
// It doesn't looks like we can find which instrument callback
// threw.
OpenTelemetrySdkEventSource.Log.MetricObserverCallbackException(exception);
}
var indexSnapshot = Math.Min(this.metricIndex, MaxMetrics - 1);
var target = indexSnapshot + 1;
int metricCountCurrentBatch = 0;
for (int i = 0; i < target; i++)
{
var metric = this.metrics[i];
int metricPointSize = 0;
if (metric != null)
{
if (metric.InstrumentDisposed)
{
metricPointSize = metric.Snapshot();
this.metrics[i] = null;
}
else
{
metricPointSize = metric.Snapshot();
}
if (metricPointSize > 0)
{
this.metricsCurrentBatch[metricCountCurrentBatch++] = metric;
}
}
}
return (metricCountCurrentBatch > 0) ? new Batch<Metric>(this.metricsCurrentBatch, metricCountCurrentBatch) : default;
this.listener.RecordObservableInstruments();
}
catch (Exception)
catch (Exception exception)
{
// TODO: Log
return default;
// TODO:
// It doesn't looks like we can find which instrument callback
// threw.
OpenTelemetrySdkEventSource.Log.MetricObserverCallbackException(exception);
}
}
}
@ -538,6 +479,7 @@ namespace OpenTelemetry.Metrics
// Wait for up to 5 seconds grace period
this.reader?.Shutdown(5000);
this.reader?.Dispose();
this.compositeMetricReader?.Dispose();
this.listener.Dispose();
}

View File

@ -22,7 +22,10 @@ using OpenTelemetry.Internal;
namespace OpenTelemetry.Metrics
{
public abstract class MetricReader : IDisposable
/// <summary>
/// MetricReader which does not deal with individual metrics.
/// </summary>
public abstract partial class MetricReader : IDisposable
{
private const AggregationTemporality CumulativeAndDelta = AggregationTemporality.Cumulative | AggregationTemporality.Delta;
private readonly object newTaskLock = new object();
@ -211,8 +214,10 @@ namespace OpenTelemetry.Metrics
? null
: Stopwatch.StartNew();
var collectMetric = this.ParentProvider.GetMetricCollect();
var metrics = collectMetric();
var collectObservableInstruments = this.ParentProvider.GetObservableInstrumentCollectCallback();
collectObservableInstruments();
var metrics = this.GetMetricsBatch();
if (sw == null)
{

View File

@ -0,0 +1,233 @@
// <copyright file="MetricReaderExt.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Metrics
{
/// <summary>
/// MetricReader which processes individual metrics.
/// </summary>
public abstract partial class MetricReader
{
internal const int MaxMetrics = 1000;
private readonly Metric[] metrics = new Metric[MaxMetrics];
private readonly Metric[] metricsCurrentBatch = new Metric[MaxMetrics];
private readonly HashSet<string> metricStreamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly object instrumentCreationLock = new object();
private int metricIndex = -1;
internal Metric AddMetricWithNoViews(Instrument instrument)
{
var meterName = instrument.Meter.Name;
var metricName = instrument.Name;
var metricStreamName = $"{meterName}.{metricName}";
lock (this.instrumentCreationLock)
{
if (this.metricStreamNames.Contains(metricStreamName))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using View.");
return null;
}
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Maximum allowed Metrics for the provider exceeded.", "Use views to drop unused instruments. Or configure Provider to allow higher limit.");
return null;
}
else
{
var metric = new Metric(instrument, this.preferredAggregationTemporality, metricName, instrument.Description);
this.metrics[index] = metric;
this.metricStreamNames.Add(metricStreamName);
return metric;
}
}
}
internal void RecordSingleStreamLongMeasurement(Metric metric, long value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
metric.UpdateLong(value, tags);
}
internal void RecordSingleStreamDoubleMeasurement(Metric metric, double value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
metric.UpdateDouble(value, tags);
}
internal List<Metric> AddMetricsListWithViews(Instrument instrument, List<MetricStreamConfiguration> metricStreamConfigs)
{
var maxCountMetricsToBeCreated = metricStreamConfigs.Count;
// Create list with initial capacity as the max metric count.
// Due to duplicate/max limit, we may not end up using them
// all, and that memory is wasted until Meter disposed.
// TODO: Revisit to see if we need to do metrics.TrimExcess()
var metrics = new List<Metric>(maxCountMetricsToBeCreated);
lock (this.instrumentCreationLock)
{
for (int i = 0; i < maxCountMetricsToBeCreated; i++)
{
var metricStreamConfig = metricStreamConfigs[i];
var meterName = instrument.Meter.Name;
var metricName = metricStreamConfig?.Name ?? instrument.Name;
var metricStreamName = $"{meterName}.{metricName}";
if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricName))
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(
metricName,
instrument.Meter.Name,
"Metric name is invalid.",
"The name must comply with the OpenTelemetry specification.");
continue;
}
if (this.metricStreamNames.Contains(metricStreamName))
{
// TODO: Log that instrument is ignored
// as the resulting Metric name is conflicting
// with existing name.
continue;
}
if (metricStreamConfig?.Aggregation == Aggregation.Drop)
{
// TODO: Log that instrument is ignored
// as user explicitly asked to drop it
// with View.
continue;
}
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
// TODO: Log that instrument is ignored
// as max number of Metrics have reached.
}
else
{
Metric metric;
var metricDescription = metricStreamConfig?.Description ?? instrument.Description;
string[] tagKeysInteresting = metricStreamConfig?.TagKeys;
double[] histogramBucketBounds = (metricStreamConfig is ExplicitBucketHistogramConfiguration histogramConfig
&& histogramConfig.Boundaries != null) ? histogramConfig.Boundaries : null;
metric = new Metric(instrument, this.preferredAggregationTemporality, metricName, metricDescription, histogramBucketBounds, tagKeysInteresting);
this.metrics[index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamName);
}
}
return metrics;
}
}
internal void RecordLongMeasurement(List<Metric> metrics, long value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
if (metrics.Count == 1)
{
// special casing the common path
// as this is faster than the
// foreach, when count is 1.
metrics[0].UpdateLong(value, tags);
}
else
{
foreach (var metric in metrics)
{
metric.UpdateLong(value, tags);
}
}
}
internal void RecordDoubleMeasurement(List<Metric> metrics, double value, ReadOnlySpan<KeyValuePair<string, object>> tags)
{
if (metrics.Count == 1)
{
// special casing the common path
// as this is faster than the
// foreach, when count is 1.
metrics[0].UpdateDouble(value, tags);
}
else
{
foreach (var metric in metrics)
{
metric.UpdateDouble(value, tags);
}
}
}
internal void CompleteSingleStreamMeasurement(Metric metric)
{
metric.InstrumentDisposed = true;
}
internal void CompleteMeasurement(List<Metric> metrics)
{
foreach (var metric in metrics)
{
metric.InstrumentDisposed = true;
}
}
private Batch<Metric> GetMetricsBatch()
{
try
{
var indexSnapshot = Math.Min(this.metricIndex, MaxMetrics - 1);
var target = indexSnapshot + 1;
int metricCountCurrentBatch = 0;
for (int i = 0; i < target; i++)
{
var metric = this.metrics[i];
int metricPointSize = 0;
if (metric != null)
{
if (metric.InstrumentDisposed)
{
metricPointSize = metric.Snapshot();
this.metrics[i] = null;
}
else
{
metricPointSize = metric.Snapshot();
}
if (metricPointSize > 0)
{
this.metricsCurrentBatch[metricCountCurrentBatch++] = metric;
}
}
}
return (metricCountCurrentBatch > 0) ? new Batch<Metric>(this.metricsCurrentBatch, metricCountCurrentBatch) : default;
}
catch (Exception)
{
// TODO: Log
return default;
}
}
}
}

View File

@ -50,16 +50,6 @@ namespace OpenTelemetry
return Resource.Empty;
}
public static Func<Batch<Metric>> GetMetricCollect(this BaseProvider baseProvider)
{
if (baseProvider is MeterProviderSdk meterProviderSdk)
{
return meterProviderSdk.Collect;
}
return null;
}
/// <summary>
/// Gets the <see cref="Resource"/> associated with the <see cref="BaseProvider"/>.
/// </summary>
@ -69,5 +59,15 @@ namespace OpenTelemetry
{
return ResourceBuilder.CreateDefault().Build();
}
internal static Action GetObservableInstrumentCollectCallback(this BaseProvider baseProvider)
{
if (baseProvider is MeterProviderSdk meterProviderSdk)
{
return meterProviderSdk.CollectObservableInstruments;
}
return null;
}
}
}

View File

@ -690,6 +690,28 @@ namespace OpenTelemetry.Metrics.Tests
Assert.Equal(name, metric.Name);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void SetupSdkProviderWithNoReader(bool hasViews)
{
// This test ensures that MeterProviderSdk can be set up without any reader
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}");
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name);
if (hasViews)
{
meterProviderBuilder.AddView("counter", "renamedCounter");
}
using var meterProvider = meterProviderBuilder.Build();
var counter = meter.CreateCounter<long>("counter");
counter.Add(10, new KeyValuePair<string, object>("key", "value"));
}
private static long GetLongSum(List<Metric> metrics)
{
long sum = 0;

View File

@ -0,0 +1,119 @@
// <copyright file="MultipleReadersTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using OpenTelemetry.Exporter;
using OpenTelemetry.Tests;
using Xunit;
namespace OpenTelemetry.Metrics.Tests
{
public class MultipleReadersTests
{
[Theory]
[InlineData(AggregationTemporality.Delta, false)]
[InlineData(AggregationTemporality.Delta, true)]
[InlineData(AggregationTemporality.Cumulative, false)]
[InlineData(AggregationTemporality.Cumulative, true)]
public void SdkSupportsMultipleReaders(AggregationTemporality aggregationTemporality, bool hasViews)
{
var exporterdMetricItems1 = new List<Metric>();
using var deltaMetricExporter1 = new InMemoryExporter<Metric>(exporterdMetricItems1);
using var deltaMetricReader1 = new BaseExportingMetricReader(deltaMetricExporter1)
{
PreferredAggregationTemporality = AggregationTemporality.Delta,
};
var exporterdMetricItems2 = new List<Metric>();
using var deltaMetricExporter2 = new InMemoryExporter<Metric>(exporterdMetricItems2);
using var deltaMetricReader2 = new BaseExportingMetricReader(deltaMetricExporter2)
{
PreferredAggregationTemporality = aggregationTemporality,
};
using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{aggregationTemporality}.{hasViews}");
var counter = meter.CreateCounter<long>("counter");
int index = 0;
var values = new long[] { 100, 200, 300, 400 };
long GetValue() => values[index++];
var gauge = meter.CreateObservableGauge("gauge", () => GetValue());
var meterProviderBuilder = Sdk.CreateMeterProviderBuilder()
.AddMeter(meter.Name)
.AddReader(deltaMetricReader1)
.AddReader(deltaMetricReader2);
if (hasViews)
{
meterProviderBuilder.AddView("counter", "renamedCounter");
}
using var meterProvider = meterProviderBuilder.Build();
counter.Add(10, new KeyValuePair<string, object>("key", "value"));
meterProvider.ForceFlush();
Assert.Equal(2, exporterdMetricItems1.Count);
Assert.Equal(2, exporterdMetricItems2.Count);
// Check value exported for Counter
this.AssertLongSumValueForMetric(exporterdMetricItems1[0], 10);
this.AssertLongSumValueForMetric(exporterdMetricItems2[0], 10);
// Check value exported for Gauge
this.AssertLongSumValueForMetric(exporterdMetricItems1[1], 100);
this.AssertLongSumValueForMetric(exporterdMetricItems2[1], 200);
exporterdMetricItems1.Clear();
exporterdMetricItems2.Clear();
counter.Add(15, new KeyValuePair<string, object>("key", "value"));
meterProvider.ForceFlush();
Assert.Equal(2, exporterdMetricItems1.Count);
Assert.Equal(2, exporterdMetricItems2.Count);
// Check value exported for Counter
this.AssertLongSumValueForMetric(exporterdMetricItems1[0], 15);
if (aggregationTemporality == AggregationTemporality.Delta)
{
this.AssertLongSumValueForMetric(exporterdMetricItems2[0], 15);
}
else
{
this.AssertLongSumValueForMetric(exporterdMetricItems2[0], 25);
}
// Check value exported for Gauge
this.AssertLongSumValueForMetric(exporterdMetricItems1[1], 300);
this.AssertLongSumValueForMetric(exporterdMetricItems2[1], 400);
}
private void AssertLongSumValueForMetric(Metric metric, long value)
{
using var metricPoints = metric.GetMetricPoints();
var metricPointsEnumerator = metricPoints.GetEnumerator();
Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric
ref var metricPointForFirstExport = ref metricPointsEnumerator.Current;
Assert.Equal(value, metricPointForFirstExport.LongValue);
}
}
}