Support View part1 - rename an instrument (#2421)

This commit is contained in:
Cijo Thomas 2021-09-29 13:03:32 -07:00 committed by GitHub
parent d716bb17cd
commit a6e1e98ec2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 297 additions and 29 deletions

View File

@ -60,7 +60,7 @@ public class Program
// turn off the above default. i.e any
// instrument which does not match any views
// gets dropped.
// .AddView(instrumentName: "*", new DropAggregationConfig())
// .AddView(instrumentName: "*", new MetricStreamConfiguration() { Aggregation = Aggregation.Drop })
.AddConsoleExporter()
.Build();

View File

@ -17,6 +17,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Text.RegularExpressions;
using OpenTelemetry.Resources;
namespace OpenTelemetry.Metrics
@ -88,19 +89,26 @@ namespace OpenTelemetry.Metrics
internal MeterProviderBuilder AddView(string instrumentName, string name)
{
// TODO: Actually implement view.
return this;
return this.AddView(instrumentName, new MetricStreamConfiguration() { Name = name });
}
internal MeterProviderBuilder AddView(string instrumentName, MetricStreamConfiguration aggregationConfig)
internal MeterProviderBuilder AddView(string instrumentName, MetricStreamConfiguration metricStreamConfiguration)
{
// TODO: Actually implement view.
return this;
if (instrumentName.IndexOf('*') != -1)
{
var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*");
var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
return this.AddView(instrument => regex.IsMatch(instrument.Name) ? metricStreamConfiguration : null);
}
else
{
return this.AddView(instrument => instrument.Name.Equals(instrumentName, StringComparison.OrdinalIgnoreCase) ? metricStreamConfiguration : null);
}
}
internal MeterProviderBuilder AddView(Func<Instrument, MetricStreamConfiguration> viewConfig)
{
// TODO: Actually implement view.
this.viewConfigs.Add(viewConfig);
return this;
}

View File

@ -18,7 +18,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Threading;
using OpenTelemetry.Resources;
namespace OpenTelemetry.Metrics
@ -92,39 +91,122 @@ namespace OpenTelemetry.Metrics
meterSourcesToSubscribe[name] = true;
}
this.listener = new MeterListener()
this.listener = new MeterListener();
var viewConfigCount = this.viewConfigs.Count;
if (viewConfigCount > 0)
{
InstrumentPublished = (instrument, listener) =>
this.listener.InstrumentPublished = (instrument, listener) =>
{
if (meterSourcesToSubscribe.ContainsKey(instrument.Meter.Name))
{
// Creating list with initial capacity as the maximum
// possible size, to avoid any array resize/copy internally.
// There may be excess space wasted, but it'll eligible for
// GC right after this method.
var metricStreamConfigs = new List<MetricStreamConfiguration>(viewConfigCount);
foreach (var viewConfig in this.viewConfigs)
{
var metricStreamConfig = viewConfig(instrument);
if (metricStreamConfig != null)
{
metricStreamConfigs.Add(metricStreamConfig);
}
}
if (metricStreamConfigs.Count == 0)
{
// No views matched. Add null
// which will apply defaults.
// Users can turn off this default
// by adding a view like below as the last view.
// .AddView(instrumentName: "*", new MetricStreamConfiguration() { Aggregation = Aggregation.Drop })
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)
{
// TODO: This is where view config will be looked up
// and zero, one or more Metric streams will be created.
if (this.metricStreamNames.ContainsKey(instrument.Name))
for (int i = 0; i < maxCountMetricsToBeCreated; i++)
{
// log and ignore this instrument.
var metricStreamName = metricStreamConfigs[i]?.Name ?? instrument.Name;
if (this.metricStreamNames.ContainsKey(metricStreamName))
{
// TODO: Log that instrument is ignored
// as the resulting Metric name is conflicting
// with existing name.
continue;
}
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
// TODO: Log that instrument is ignored
// as max number of Metrics have reached.
}
else
{
var metric = new Metric(instrument, temporality, metricStreamName);
this.metrics[index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamName, true);
}
}
}
if (metrics.Count > 0)
{
listener.EnableMeasurementEvents(instrument, metrics);
}
}
};
}
else
{
this.listener.InstrumentPublished = (instrument, listener) =>
{
if (meterSourcesToSubscribe.ContainsKey(instrument.Meter.Name))
{
var metricName = instrument.Name;
List<Metric> metrics = null;
lock (this.instrumentCreationLock)
{
if (this.metricStreamNames.ContainsKey(metricName))
{
// TODO: Log that instrument is ignored
// as the resulting Metric name is conflicting
// with existing name.
return;
}
var index = Interlocked.Increment(ref this.metricIndex);
var index = ++this.metricIndex;
if (index >= MaxMetrics)
{
// Log that all measurements are dropped from this instrument.
// TODO: Log that instrument is ignored
// as max number of Metrics have reached.
return;
}
else
{
metrics = new List<Metric>(1);
var metric = new Metric(instrument, temporality);
this.metrics[index] = metric;
this.metricStreamNames.Add(instrument.Name, true);
listener.EnableMeasurementEvents(instrument, metric);
metrics.Add(metric);
this.metricStreamNames.Add(metricName, true);
}
}
listener.EnableMeasurementEvents(instrument, metrics);
}
},
MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state),
};
};
}
this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state);
// Everything double
this.listener.SetMeasurementEventCallback<double>((instrument, value, tags, state) => this.MeasurementRecordedDouble(instrument, value, tags, state));
@ -153,29 +235,41 @@ namespace OpenTelemetry.Metrics
internal void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan<KeyValuePair<string, object>> tagsRos, object state)
{
// Get Instrument State
var metric = state as Metric;
// TODO: Benchmark and see if it makes
// sense to use a different state
// when there are no views registered.
// In that case, storing Metric as state
// might be faster than storing List<Metric>
// of size one as state.
var metrics = state as List<Metric>;
if (instrument == null || metric == null)
if (instrument == null || metrics == null)
{
// TODO: log
return;
}
metric.UpdateDouble(value, tagsRos);
foreach (var metric in metrics)
{
metric.UpdateDouble(value, tagsRos);
}
}
internal void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan<KeyValuePair<string, object>> tagsRos, object state)
{
// Get Instrument State
var metric = state as Metric;
var metrics = state as List<Metric>;
if (instrument == null || metric == null)
if (instrument == null || metrics == null)
{
// TODO: log
return;
}
metric.UpdateLong(value, tagsRos);
foreach (var metric in metrics)
{
metric.UpdateLong(value, tagsRos);
}
}
internal Batch<Metric> Collect()

View File

@ -24,9 +24,9 @@ namespace OpenTelemetry.Metrics
{
private AggregatorStore aggStore;
internal Metric(Instrument instrument, AggregationTemporality temporality)
internal Metric(Instrument instrument, AggregationTemporality temporality, string metricName = "")
{
this.Name = instrument.Name;
this.Name = string.IsNullOrWhiteSpace(metricName) ? instrument.Name : metricName;
this.Description = instrument.Description;
this.Unit = instrument.Unit;
this.Meter = instrument.Meter;

View File

@ -0,0 +1,166 @@
// <copyright file="MetricViewTests.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 Xunit;
using Xunit.Abstractions;
namespace OpenTelemetry.Metrics.Tests
{
public class MetricViewTests
{
private const int MaxTimeToAllowForFlush = 10000;
private readonly ITestOutputHelper output;
public MetricViewTests(ITestOutputHelper output)
{
this.output = output;
}
[Fact]
public void ViewToRenameMetric()
{
using var meter1 = new Meter("ViewToRenameMetricTest");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddSource(meter1.Name)
.AddView("name1", "renamed")
.AddInMemoryExporter(exportedItems)
.Build();
// Expecting one metric stream.
var counterLong = meter1.CreateCounter<long>("name1");
counterLong.Add(10);
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Single(exportedItems);
var metric = exportedItems[0];
Assert.Equal("renamed", metric.Name);
}
[Fact]
public void ViewToRenameMetricConditionally()
{
using var meter1 = new Meter("ViewToRenameMetricConditionallyTest");
using var meter2 = new Meter("ViewToRenameMetricConditionallyTest2");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddSource(meter1.Name)
.AddSource(meter2.Name)
.AddView((instrument) =>
{
if (instrument.Meter.Name.Equals(meter2.Name, StringComparison.OrdinalIgnoreCase)
&& instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase))
{
return new MetricStreamConfiguration() { Name = "name1_Renamed" };
}
else
{
return null;
}
})
.AddInMemoryExporter(exportedItems)
.Build();
// Without views only 1 stream would be
// exported (the 2nd one gets dropped due to
// name conflict). Due to renaming with Views,
// we expect 2 metric streams here.
var counter1 = meter1.CreateCounter<long>("name1");
var counter2 = meter2.CreateCounter<long>("name1");
counter1.Add(10);
counter2.Add(10);
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Equal(2, exportedItems.Count);
Assert.Equal("name1", exportedItems[0].Name);
Assert.Equal("name1_Renamed", exportedItems[1].Name);
}
[Fact]
public void ViewToRenameMetricWildCardMatch()
{
using var meter1 = new Meter("ViewToRenameMetricWildCardMatchTest");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddSource(meter1.Name)
.AddView("counter*", "renamed")
.AddInMemoryExporter(exportedItems)
.Build();
// Expecting one metric stream.
var counter1 = meter1.CreateCounter<long>("counterA");
counter1.Add(10);
var counter2 = meter1.CreateCounter<long>("counterB");
counter2.Add(10);
var counter3 = meter1.CreateCounter<long>("counterC");
counter3.Add(10);
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
// counter* matches all 3 instruments which all
// becomes "renamed" and only 1st one is exported.
Assert.Single(exportedItems);
var metric = exportedItems[0];
Assert.Equal("renamed", metric.Name);
}
[Fact]
public void ViewToProduceMultipleStreamsFromInstrument()
{
using var meter1 = new Meter("ViewToProduceMultipleStreamsFromInstrumentTest");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddSource(meter1.Name)
.AddView("name1", "renamedStream1")
.AddView("name1", "renamedStream2")
.AddInMemoryExporter(exportedItems)
.Build();
// Expecting two metric stream.
var counterLong = meter1.CreateCounter<long>("name1");
counterLong.Add(10);
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Equal(2, exportedItems.Count);
Assert.Equal("renamedStream1", exportedItems[0].Name);
Assert.Equal("renamedStream2", exportedItems[1].Name);
}
[Fact]
public void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument()
{
using var meter1 = new Meter("ViewToProduceMultipleStreamsWithDuplicatesFromInstrumentTest");
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddSource(meter1.Name)
.AddView("name1", "renamedStream1")
.AddView("name1", "renamedStream2")
.AddView("name1", "renamedStream2")
.AddInMemoryExporter(exportedItems)
.Build();
// Expecting two metric stream.
// the .AddView("name1", "renamedStream2")
// won't produce new Metric as the name
// conflicts.
var counterLong = meter1.CreateCounter<long>("name1");
counterLong.Add(10);
meterProvider.ForceFlush(MaxTimeToAllowForFlush);
Assert.Equal(2, exportedItems.Count);
Assert.Equal("renamedStream1", exportedItems[0].Name);
Assert.Equal("renamedStream2", exportedItems[1].Name);
}
}
}