Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow duplicate instrument registration #2916

Merged
merged 30 commits into from
Mar 5, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ac0e766
Allow duplicate instrument registration
alanwest Feb 18, 2022
7fb51d0
Merge branch 'main' into alanwest/metric-identity
alanwest Feb 18, 2022
27b595a
Merge branch 'main' into alanwest/metric-identity
cijothomas Feb 23, 2022
ceacd5f
Merge branch 'main' into alanwest/metric-identity
alanwest Feb 23, 2022
d4a45d0
Rename MetricIdentity to InstrumentIdentity
alanwest Feb 23, 2022
0423b57
Handle duplicate instrument registration on views code path
alanwest Feb 25, 2022
56e5313
Test identical instruments with no views
alanwest Feb 25, 2022
024e947
Merge branch 'main' into alanwest/metric-identity
alanwest Feb 25, 2022
134fc1b
Merge branch 'alanwest/metric-identity' of github.com:alanwest/opente…
alanwest Feb 25, 2022
dd19e28
Allow distinct metric streams with same name
alanwest Mar 1, 2022
d399bee
Add more tests
alanwest Mar 1, 2022
445a3da
Merge branch 'main' into alanwest/metric-identity
alanwest Mar 1, 2022
8062437
Update warning message
alanwest Mar 1, 2022
37526c0
Merge branch 'main' into alanwest/metric-identity
alanwest Mar 1, 2022
16b9b63
Fixx spelling
alanwest Mar 1, 2022
1bfafc6
Update src/OpenTelemetry/Metrics/MetricReaderExt.cs
cijothomas Mar 2, 2022
3642a3a
Update changelog
alanwest Mar 3, 2022
c69ed26
Merge remote-tracking branch 'upstream/main' into alanwest/metric-ide…
alanwest Mar 3, 2022
8eec320
Hashtags: For more than just social media
alanwest Mar 3, 2022
2dd0058
Fix view use case
alanwest Mar 3, 2022
03a245e
Handle instruments with same name from meters with same name differen…
alanwest Mar 4, 2022
0ff1d22
Clear InstrumentIdenity map when meter is disposed
alanwest Mar 4, 2022
63bfe85
Merge remote-tracking branch 'upstream/main' into alanwest/metric-ide…
alanwest Mar 4, 2022
e52051d
Update public API
alanwest Mar 4, 2022
90ced8c
Update changelog
alanwest Mar 4, 2022
28aa3b3
Add additional clarification for duplicate instrument warning
alanwest Mar 4, 2022
247b0d7
Private set
alanwest Mar 4, 2022
683182e
Instrument name from InstrumentIdentity
alanwest Mar 4, 2022
ef836a7
Compute hash in constructor
alanwest Mar 5, 2022
012f080
Merge branch 'main' into alanwest/metric-identity
utpilla Mar 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ public void ProviderDisposed(string providerName)
this.WriteEvent(37, providerName);
}

[Event(38, Message = "Duplicate Instrument '{0}', Meter '{1}' encountered. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)]
public void DuplicateMetricInstrument(string instrumentName, string meterName, string reason, string fix)
{
this.WriteEvent(38, instrumentName, meterName, reason, fix);
}

#if DEBUG
public class OpenTelemetryEventListener : EventListener
{
Expand Down
74 changes: 74 additions & 0 deletions src/OpenTelemetry/Metrics/InstrumentIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// <copyright file="InstrumentIdentity.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;

namespace OpenTelemetry.Metrics
{
internal readonly struct InstrumentIdentity : IEquatable<InstrumentIdentity>
{
public InstrumentIdentity(string meterName, string instrumentName, string unit, string description, Type instrumentType)
{
this.MeterName = meterName;
this.InstrumentName = instrumentName;
this.Unit = unit;
this.Description = description;
this.InstrumentType = instrumentType;
}

public readonly string MeterName { get; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not quite right. The entire meter - that is, name, version, and schema url (when we eventually support it) should be a component of the identity. Fixing this...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 03a245e

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not quite right. The entire meter - that is, name, version, and schema url (when we eventually support it) should be a component of the identity. Fixing this...

Wouldn't it be better to just have Meter as the only public property in that case? We would only need to update the Equals() check like below:

                ...
                && this.Meter.Name == other.Meter.Name
                && this.Meter.Version == other.Meter.Version
                && this.Meter.SchemaUrl == other.Meter.SchemaUrl 
                ...

This way if schema url gets added to Meter later on, we don't have to add a new public property dedicated just for that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an internal struct, it wouldn't matter as much if we have to add more properties later on, but I think we could just use Meter here as it provides the best encapsulation for everything Meter related.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean keep a handle on the Meter itself? Same concern here #2916 (comment) would apply. Meter might be disposed so safer to not keep a handle on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it'd be less of a concern to keep a handle on the meter since InstrumentIdentity is internal?

Copy link
Contributor

@utpilla utpilla Mar 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was suggesting to have a handle to Meter itself as all of this is happening in the InstrumentPublished callback so the Meter would not be disposed in this path. But I overlooked the fact that in case of dictionary lookup collisions, the Equals check might fail as the dictionary might still have entries to instruments whose Meters are disposed. This would only work if we can ensure that the dictionary would never have any instrument whose Meter is disposed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay to park this into an issue and come back to this ? (so that we can merge and do a release.)


public readonly string InstrumentName { get; }

public readonly string Unit { get; }

public readonly string Description { get; }

public readonly Type InstrumentType { get; }

public static bool operator ==(InstrumentIdentity metricIdentity1, InstrumentIdentity metricIdentity2) => metricIdentity1.Equals(metricIdentity2);

public static bool operator !=(InstrumentIdentity metricIdentity1, InstrumentIdentity metricIdentity2) => !metricIdentity1.Equals(metricIdentity2);

public readonly override bool Equals(object obj)
{
return obj is InstrumentIdentity other && this.Equals(other);
}

public bool Equals(InstrumentIdentity other)
{
return this.InstrumentType == other.InstrumentType
&& this.MeterName == other.MeterName
&& this.InstrumentName == other.InstrumentName
&& this.Unit == other.Unit
&& this.Description == other.Description;
}

public readonly override int GetHashCode()
{
unchecked
{
int hash = 17;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: would it help if the hash is calculated and stored during ctor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting thought. Done.

hash = (hash * 31) + this.InstrumentType.GetHashCode();
hash = (hash * 31) + this.MeterName.GetHashCode();
hash = (hash * 31) + this.InstrumentName.GetHashCode();
hash = this.Unit == null ? hash : (hash * 31) + this.Unit.GetHashCode();
hash = this.Description == null ? hash : (hash * 31) + this.Description.GetHashCode();
return hash;
}
}
}
}
23 changes: 18 additions & 5 deletions src/OpenTelemetry/Metrics/MetricReaderExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace OpenTelemetry.Metrics
public abstract partial class MetricReader
{
private readonly HashSet<string> metricStreamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<InstrumentIdentity, Metric> instrumentIdentityToMetric = new Dictionary<InstrumentIdentity, Metric>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to clear an entry, upon instrument dispose as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out there's a bit more to just freeing up an entry in this dictionary. I hadn't fully followed the dispose path, so I didn't notice that when an instrument is disposed the metric is freed up. Now that multiple instruments can modify a single metric we need to free up the metric only when no more instruments reference it.

Working on this...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, my initial understanding was incorrect. Instruments don't get disposed, meters do, so I think is less complicated than I had thought... 0ff1d22

private readonly object instrumentCreationLock = new object();
private int maxMetricStreams;
private int maxMetricPointsPerMetricStream;
Expand All @@ -39,12 +40,17 @@ internal Metric AddMetricWithNoViews(Instrument instrument)
var meterName = instrument.Meter.Name;
var metricName = instrument.Name;
var metricStreamName = $"{meterName}.{metricName}";
var instrumentIdentity = new InstrumentIdentity(meterName, metricName, instrument.Unit, instrument.Description, instrument.GetType());
lock (this.instrumentCreationLock)
{
if (this.instrumentIdentityToMetric.TryGetValue(instrumentIdentity, out var existingMetric))
{
return existingMetric;
}

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;
OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument(metricName, instrument.Meter.Name, "Metric instrument has the same name as an existing one but differs by description, unit, or instrument type.", "Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict.");
cijothomas marked this conversation as resolved.
Show resolved Hide resolved
}

var index = ++this.metricIndex;
Expand All @@ -56,6 +62,7 @@ internal Metric AddMetricWithNoViews(Instrument instrument)
else
{
var metric = new Metric(instrument, this.Temporality, metricName, instrument.Description, this.maxMetricPointsPerMetricStream);
this.instrumentIdentityToMetric[instrumentIdentity] = metric;
this.metrics[index] = metric;
this.metricStreamNames.Add(metricStreamName);
return metric;
Expand Down Expand Up @@ -90,6 +97,8 @@ internal List<Metric> AddMetricsListWithViews(Instrument instrument, List<Metric
var meterName = instrument.Meter.Name;
var metricName = metricStreamConfig?.Name ?? instrument.Name;
var metricStreamName = $"{meterName}.{metricName}";
var metricDescription = metricStreamConfig?.Description ?? instrument.Description;
var instrumentIdentity = new InstrumentIdentity(meterName, metricName, instrument.Unit, metricDescription, instrument.GetType());

if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricName))
{
Expand All @@ -102,12 +111,16 @@ internal List<Metric> AddMetricsListWithViews(Instrument instrument, List<Metric
continue;
}

if (this.metricStreamNames.Contains(metricStreamName))
if (this.instrumentIdentityToMetric.TryGetValue(instrumentIdentity, out var existingMetric))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to add existingMetric to the metrics.Add(existingMetric) as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider example:
instr1 has view, which says produce 2 streams named inst1_a, inst1_b.
instr2 has view, which says produce 2 streams named inst1_a, inst1_c.

instr1 creation will return List{M1,M2}.
instr2 creation should return List {M1,M3} --> this PR currently only returns {M3}.

^ we should add unittest to cover this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. Views hurt my mind 🤕. Fixed 2dd0058

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yet, everyone loves a room with a view! 😄

{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using MeterProviderBuilder.AddView.");
continue;
}

if (this.metricStreamNames.Contains(metricStreamName))
{
OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument(metricName, instrument.Meter.Name, "Metric instrument has the same name as an existing one but differs by description, unit, or instrument type.", "Either change the name of the instrument or use MeterProviderBuilder.AddView to resolve the conflict.");
}

if (metricStreamConfig?.Aggregation == Aggregation.Drop)
{
OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "View configuration asks to drop this instrument.", "Modify view configuration to allow this instrument, if desired.");
Expand All @@ -122,12 +135,12 @@ internal List<Metric> AddMetricsListWithViews(Instrument instrument, List<Metric
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.Temporality, metricName, metricDescription, this.maxMetricPointsPerMetricStream, histogramBucketBounds, tagKeysInteresting);

this.instrumentIdentityToMetric[instrumentIdentity] = metric;
this.metrics[index] = metric;
metrics.Add(metric);
this.metricStreamNames.Add(metricStreamName);
Expand Down
Loading