Allow duplicate instrument registration#2916
Conversation
…lemetry-dotnet into alanwest/metric-identity
Codecov Report
@@ 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
|
| } | ||
|
|
||
| if (this.metricStreamNames.Contains(metricStreamName)) | ||
| if (this.instrumentIdentityToMetric.TryGetValue(instrumentIdentity, out var existingMetric)) |
There was a problem hiding this comment.
we need to add existingMetric to the metrics.Add(existingMetric) as well.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
You are correct. Views hurt my mind 🤕. Fixed 2dd0058
There was a problem hiding this comment.
And yet, everyone loves a room with a view! 😄
cijothomas
left a comment
There was a problem hiding this comment.
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.
| { | ||
| 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."); |
There was a problem hiding this comment.
Should we update the message? The user doesn't really need to resolve any conflict here, right?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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>(); |
There was a problem hiding this comment.
We need to clear an entry, upon instrument dispose as well.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Maybe it'd be less of a concern to keep a handle on the meter since InstrumentIdentity is internal?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Okay to park this into an issue and come back to this ? (so that we can merge and do a release.)
| OpenTelemetry.Metrics.Metric.Meter.get -> System.Diagnostics.Metrics.Meter | ||
| OpenTelemetry.Metrics.Metric.MeterName.get -> string | ||
| OpenTelemetry.Metrics.Metric.MeterVersion.get -> string |
There was a problem hiding this comment.
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; }
}There was a problem hiding this comment.
This is a good improvement. Don't know if we need to introduce new Struct for this.
There was a problem hiding this comment.
Need to add this to changelog as this is breaking any existing exporters.
cijothomas
left a comment
There was a problem hiding this comment.
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.
| { | ||
| unchecked | ||
| { | ||
| int hash = 17; |
There was a problem hiding this comment.
Minor: would it help if the hash is calculated and stored during ctor?
There was a problem hiding this comment.
Interesting thought. Done.
## Changes Update the compliance matrix for .NET as of 1.16.0 of the .NET SDK. I got Copilot to do most of the work of researching what was missing that had been implementing and finding the relevant PRs that implemented then. I cross checked the PRs _looked_ correct, but some of it might be incorrect, particularly things that seem to have been implemented for a while and predate my contributions to OTel. I then had Claude review the changes and cross-reference, which fixed some of the entries, but again there might still be some mistakes. ~~Some of the entries might only be available as experimental features, so maybe they shouldn't be updated?~~ Experimental entries removed, now marked as ~~strikethrough~~ below. Entries updated with relevant PR links where appropriate below. /cc @open-telemetry/dotnet-maintainers ### Traces - Get a Tracer with schema_url: open-telemetry/opentelemetry-dotnet#6736 - Get a Tracer with scope attributes: open-telemetry/opentelemetry-dotnet#6137 - Associate Tracer with InstrumentationScope: open-telemetry/opentelemetry-dotnet#239 - Fetch InstrumentationScope from ReadableSpan: open-telemetry/opentelemetry-dotnet#679 ### Metrics - Associate Meter with InstrumentationScope: open-telemetry/opentelemetry-dotnet#6714 - Gauge instrument is supported: open-telemetry/opentelemetry-dotnet#5867 - Valid instrument created / warning for duplicate name: open-telemetry/opentelemetry-dotnet#2916 - Duplicate instrument registration resolved by first-seen: open-telemetry/opentelemetry-dotnet#2916 - Instrument names conform to specified syntax: open-telemetry/opentelemetry-dotnet#3821 - Instrument supports advisory ExplicitBucketBoundaries: open-telemetry/opentelemetry-dotnet#5854 - Instrument supports advisory Attributes: Not implemented - name/version/schema_url create InstrumentationScope: open-telemetry/opentelemetry-dotnet#6714 - ~~View allows configuring name, description, attributes, aggregation: open-telemetry/opentelemetry-dotnet#2396 - ~~View allows configuring exemplar reservoir: open-telemetry/opentelemetry-dotnet#5558 - Metrics Reader supports default aggregation config: open-telemetry/opentelemetry-dotnet#6778 - Metrics Reader supports default temporality config: open-telemetry/opentelemetry-dotnet#4667 - ~~ExemplarReservoir interface/extension point: open-telemetry/opentelemetry-dotnet#5542 - ~~ExemplarReservoir has offer method: open-telemetry/opentelemetry-dotnet#5542 - SimpleFixedSizeExemplarReservoir: open-telemetry/opentelemetry-dotnet#4256 - AlignedHistogramBucketExemplarReservoir: open-telemetry/opentelemetry-dotnet#4119 ### Logs - ~~LoggerProvider.GetLogger: open-telemetry/opentelemetry-dotnet#4422 - LoggerProvider.GetLogger accepts attributes: Not implemented - LoggerProvider.Shutdown: open-telemetry/opentelemetry-dotnet#5648 - LoggerProvider.ForceFlush: open-telemetry/opentelemetry-dotnet#5648 - Logger.Emit(LogRecord) with Exception parameter: Not implemented - ~~LogRecord.Set EventName: open-telemetry/opentelemetry-dotnet#6306 - Logger.Enabled: Not implemented - Ergonomic API (ILogger bridge): open-telemetry/opentelemetry-dotnet#3489 - SimpleLogRecordProcessor: open-telemetry/opentelemetry-dotnet#1622 - BatchLogRecordProcessor: open-telemetry/opentelemetry-dotnet#1622 - Can plug custom LogRecordProcessor: open-telemetry/opentelemetry-dotnet#4916 - LogRecordProcessor.Enabled: Not implemented - Can plug custom LogRecordExporter: open-telemetry/opentelemetry-dotnet#1622 ### Context Propagation - Jaeger Propagator: open-telemetry/opentelemetry-dotnet#3309 - TextMapPropagator: open-telemetry/opentelemetry-dotnet#1427 ### Environment Variables - OTEL_SDK_DISABLED: open-telemetry/opentelemetry-dotnet#6568 - OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT: open-telemetry/opentelemetry-dotnet#4887 - OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: open-telemetry/opentelemetry-dotnet#4887 - OTEL_TRACES_SAMPLER: open-telemetry/opentelemetry-dotnet#5448 - OTEL_TRACES_SAMPLER_ARG: open-telemetry/opentelemetry-dotnet#5448 - OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION: open-telemetry/opentelemetry-dotnet#6778 ### Exporters — OTLP - OTLP/HTTP gzip Content-Encoding support: open-telemetry/opentelemetry-dotnet#7055 - ~~Honors retryable responses with backoff: open-telemetry/opentelemetry-dotnet#5495 - ~~Honors non-retryable responses: open-telemetry/opentelemetry-dotnet#5495 - ~~Honors throttling response: open-telemetry/opentelemetry-dotnet#5495 - Metric Exporter configurable temporality preference: open-telemetry/opentelemetry-dotnet#4667 - Metric Exporter configurable default aggregation: open-telemetry/opentelemetry-dotnet#6778 ### Exporters — Zipkin - InstrumentationScope mapping: open-telemetry/opentelemetry-dotnet#5473 ### Exporters — Prometheus - Metadata Deduplication: open-telemetry/opentelemetry-dotnet#7237 - otel_scope_name and otel_scope_version labels: open-telemetry/opentelemetry-dotnet#7237 - otel_scope_[attribute] labels: open-telemetry/opentelemetry-dotnet#7237 - Prometheus Counters have _total suffix by default: open-telemetry/opentelemetry-dotnet#5305 - Prometheus Counters _total suffixing can be disabled: open-telemetry/opentelemetry-dotnet#5305 - Colliding sanitized attribute keys are merged: open-telemetry/opentelemetry-dotnet#7239 - Exemplars for Histograms and Monotonic sums: open-telemetry/opentelemetry-dotnet#7222 - target_info metric from Resource: open-telemetry/opentelemetry-dotnet#5407 ### OpenTracing Compatibility - Create OpenTracing Shim: open-telemetry/opentelemetry-dotnet#197 - Tracer shim: open-telemetry/opentelemetry-dotnet#197 - Span shim: open-telemetry/opentelemetry-dotnet#197 - SpanContext shim: open-telemetry/opentelemetry-dotnet#197 - ScopeManager shim: open-telemetry/opentelemetry-dotnet#197 - Error mapping for attributes/events: open-telemetry/opentelemetry-dotnet#197
Fixes #2925
See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument
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.
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.