Skip to content

Allow duplicate instrument registration#2916

Merged
cijothomas merged 30 commits into
open-telemetry:mainfrom
alanwest:alanwest/metric-identity
Mar 5, 2022
Merged

Allow duplicate instrument registration#2916
cijothomas merged 30 commits into
open-telemetry:mainfrom
alanwest:alanwest/metric-identity

Conversation

@alanwest

@alanwest alanwest commented Feb 18, 2022

Copy link
Copy Markdown
Member

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

codecov Bot commented Feb 25, 2022

Copy link
Copy Markdown

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

@reyang reyang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM.

Comment thread src/OpenTelemetry/Metrics/MetricReaderExt.cs Outdated
}

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

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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! 😄

@cijothomas cijothomas left a comment

Copy link
Copy Markdown
Member

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.

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

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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?

@utpilla utpilla Mar 5, 2022

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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

@alanwest alanwest Mar 4, 2022

Copy link
Copy Markdown
Member Author

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
Copy Markdown
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
Copy Markdown
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.

@cijothomas cijothomas left a comment

Copy link
Copy Markdown
Member

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/OpenTelemetry/Metrics/Metric.cs Outdated
Comment thread src/OpenTelemetry/Metrics/Metric.cs Outdated
@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
AsherBond pushed a commit to OTEL-California/opentelemetry-specification that referenced this pull request Jun 29, 2026
## 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
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