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

Conversation

alanwest
Copy link
Member

@alanwest alanwest commented Feb 18, 2022

Fixes #2925

See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument

When more than one Instrument of the same name is created for identical Meters, denoted duplicate instrument registration, the implementation MUST create a valid Instrument in every case.


Duplicate instrument registration

Identical instruments

Instantiating multiple metric instruments with the same name and also identical in all other respects - same type, description, and unit - result in a single metric stream aggregating measurements from all the identical instruments.

For example, measurements from the following instruments will be aggregated together. No warning is produced by the SDK.

var instrument = meter.CreateCounter<long>(name: "my.counter", unit: "seconds", description: "counts seconds");
var duplicateInstrument = meter.CreateCounter<long>("my.counter", "seconds", "counts seconds");

Distinct instruments

Instantiating multiple metric instruments with the same name but differ in some respect - different type, description, or unit - will result in a separate metric stream for each distinct instrument.

For example, measurements from the following instruments will result in three distinct metric streams. The second one differs by unit/description and the third one differs by type. A warning is logged by the SDK in the event of distinct duplicate instrument registrations.

var instrument = meter.CreateCounter<long>(name: "my.counter", unit: "seconds", description: "counts seconds");
var duplicateInstrument = meter.CreateCounter<long>("my.counter", "milliseconds", "counts milliseconds");
var anotherDuplicateInstrument = meter.CreateCounter<double>("my.counter", "seconds", "counts seconds");

@codecov
Copy link

codecov bot commented Feb 25, 2022

Codecov Report

Merging #2916 (012f080) into main (af0c9b4) will increase coverage by 0.02%.
The diff coverage is 95.87%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2916      +/-   ##
==========================================
+ Coverage   84.01%   84.03%   +0.02%     
==========================================
  Files         254      255       +1     
  Lines        8950     8996      +46     
==========================================
+ Hits         7519     7560      +41     
- Misses       1431     1436       +5     
Impacted Files Coverage Δ
src/OpenTelemetry/Metrics/InstrumentIdentity.cs 90.32% <90.32%> (ø)
src/OpenTelemetry/Metrics/Metric.cs 94.20% <97.05%> (-0.25%) ⬇️
...tryProtocol/Implementation/MetricItemExtensions.cs 93.52% <100.00%> (ø)
...nTelemetry/Internal/OpenTelemetrySdkEventSource.cs 72.72% <100.00%> (+0.50%) ⬆️
src/OpenTelemetry/Metrics/MetricReaderExt.cs 90.51% <100.00%> (+1.62%) ⬆️
...ZPages/Implementation/ZPagesExporterEventSource.cs 56.25% <0.00%> (-6.25%) ⬇️
...ter.ZPages/Implementation/ZPagesActivityTracker.cs 97.14% <0.00%> (-2.86%) ⬇️
src/OpenTelemetry/Metrics/MetricReader.cs 85.07% <0.00%> (-0.22%) ⬇️

@alanwest alanwest marked this pull request as ready for review March 1, 2022 20:25
@alanwest alanwest requested a review from a team March 1, 2022 20:25
Copy link
Member

@reyang reyang left a comment

Choose a reason for hiding this comment

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

LGTM.

@@ -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! 😄

Copy link
Member

@cijothomas cijothomas left a comment

Choose a reason for hiding this comment

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

See this comment : https://github.com/open-telemetry/opentelemetry-dotnet/pull/2916/files#r818252181

I think we are missing a case, when Views are involved.

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, meterName, "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.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we update the message? The user doesn't really need to resolve any conflict here, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason for this warning is to highlight a duplicate instrument registration by name that is not an identical match - i.e., differs by description, unit, or instrument type.

For example:

var instrument = meter.CreateCounter<double>("MyCounter", unit: "seconds");

var duplicateButNotIdenticalInstrument = meter.CreateCounter<int>("MyCounter", unit: "milliseconds");

Per the spec, SDKs should no longer prevent this, but in practice, the differences in this example imply semantic differences between the two instruments which are likely not intended. Furthermore, the spec indicates that consumers of these two metric streams may do what they see fit including simply dropping one of them. So this is why the spec still requires SDKs to emit diagnostics that can help folks identify potential problems.

Copy link
Member

Choose a reason for hiding this comment

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

I think we can also mention in the log that "SDK will still exporter duplicates". Else it is not clear if SDK is going to drop it or pass it to exporters.

@@ -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

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.)

Comment on lines -66 to +67
OpenTelemetry.Metrics.Metric.Meter.get -> System.Diagnostics.Metrics.Meter
OpenTelemetry.Metrics.Metric.MeterName.get -> string
OpenTelemetry.Metrics.Metric.MeterVersion.get -> string
Copy link
Member Author

@alanwest alanwest Mar 4, 2022

Choose a reason for hiding this comment

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

What do folks think about this change? In part I was thinking since Meter can be disposed, maybe it's safer to not keep a handle on it from the metric. Another option would be to create a new struct to encapsulate these fields.

namespace OpenTelemetry.Api;

public struct InstrumentationScope
{
    string Name { get; }
    string Version { get; }
    // Eventually
    string SchemaUrl { get; }
}

Copy link
Member

Choose a reason for hiding this comment

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

This is a good improvement. Don't know if we need to introduce new Struct for this.

Copy link
Member

Choose a reason for hiding this comment

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

Need to add this to changelog as this is breaking any existing exporters.

Copy link
Member

@cijothomas cijothomas left a comment

Choose a reason for hiding this comment

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

LGTM.
Please modify changelog to warn export authors about breaking change from previous version.
Also a improved log message to indicate SDK is not dropping the metric.

@cijothomas
Copy link
Member

@reyang @utpilla Could use one more eyes to this PR. (It has changed since previous review.)

(Goal is to include this and do a rc3 release today)

{
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.

@cijothomas cijothomas merged commit ffffe5c into open-telemetry:main Mar 5, 2022
@alanwest alanwest deleted the alanwest/metric-identity branch March 5, 2022 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support identical instrument registration and handle conflicts
4 participants