Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Exporter.Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Notes](../../RELEASENOTES.md).
* The library is now marked as trim and AOT compatible.
([#7441](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7441))

* The `Resource` Schema URL is now printed alongside the resource attributes.
([#7472](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7472))

## 1.16.0

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ public override ExportResult Export(in Batch<Activity> batch)
this.WriteLine($" {result.Key}: {result.Value}");
}
}

if (!string.IsNullOrEmpty(resource.SchemaUrl))
{
this.WriteLine($" Schema URL: {resource.SchemaUrl}");
}
}

this.WriteLine(string.Empty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ void ProcessScope(LogRecordScope scope, ConsoleLogRecordExporter exporter)
this.WriteLine($"{result.Key}: {result.Value}");
}
}

if (!string.IsNullOrEmpty(resource.SchemaUrl))
{
this.WriteLine($"Schema URL: {resource.SchemaUrl}");
}
}

this.WriteLine(string.Empty);
Expand Down
5 changes: 5 additions & 0 deletions src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public override ExportResult Export(in Batch<Metric> batch)
this.WriteLine($"\t{result.Key}: {result.Value}");
}
}

if (!string.IsNullOrEmpty(resource.SchemaUrl))
{
this.WriteLine($"\tSchema URL: {resource.SchemaUrl}");
}
}

foreach (var metric in batch)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Notes](../../RELEASENOTES.md).
* Fixed the OTLP/gRPC exporter logging incorrectly when an export timed out.
([#7455](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7455))

* The `Resource` Schema URL is now exported on the OTLP `ResourceSpans`,
`ResourceMetrics`, and `ResourceLogs` messages.
([#7472](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7472))

## 1.16.0

Released 2026-Jun-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ internal static int WriteResourceLogs(byte[] buffer, int writePosition, SdkLimit
{
writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource);
writePosition = WriteScopeLogs(buffer, writePosition, sdkLimitOptions, experimentalOptions, scopeLogs);

if (resource?.SchemaUrl is { Length: > 0 } schemaUrl)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpLogFieldNumberConstants.ResourceLogs_Schema_Url, schemaUrl);
}

return writePosition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ private static int WriteResourceMetrics(byte[] buffer, int writePosition, Resour
writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource);
writePosition = WriteScopeMetrics(buffer, writePosition, scopeMetrics);

if (resource?.SchemaUrl is { Length: > 0 } schemaUrl)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ResourceMetrics_Schema_Url, schemaUrl);
}

return writePosition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ internal static int WriteResourceSpans(byte[] buffer, int writePosition, SdkLimi
writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource);
writePosition = WriteScopeSpans(buffer, writePosition, sdkLimitOptions);

if (resource?.SchemaUrl is { Length: > 0 } schemaUrl)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpTraceFieldNumberConstants.ResourceSpans_Schema_Url, schemaUrl);
}

return writePosition;
}

Expand Down
3 changes: 3 additions & 0 deletions src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OpenTelemetry.Resources.Resource.Resource(System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object!>>! attributes, string? schemaUrl) -> void
OpenTelemetry.Resources.Resource.SchemaUrl.get -> string?
Comment thread
alanwest marked this conversation as resolved.
static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder! resourceBuilder, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object!>>! attributes, string? schemaUrl) -> OpenTelemetry.Resources.ResourceBuilder!
Comment thread
alanwest marked this conversation as resolved.
3 changes: 3 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Notes](../../RELEASENOTES.md).
recorded.
([#7427](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7427))

* Added support for a Schema URL on `Resource` instances.
([#7472](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7472))

## 1.16.0

Released 2026-Jun-10
Expand Down
4 changes: 4 additions & 0 deletions src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ public void ExemplarReservoirException(string ex)
public void ActivityDroppedDueToUnsampledLocalParent(string activityName, string activitySourceName)
=> this.WriteEvent(58, activityName, activitySourceName);

[Event(59, Message = "Resource merge encountered conflicting Schema URLs: old = '{0}', updating = '{1}'. No Schema URL will be used for the merged resource.", Level = EventLevel.Warning)]
public void ResourceSchemaUrlMergeConflict(string oldSchemaUrl, string updatingSchemaUrl)
=> this.WriteEvent(59, oldSchemaUrl, updatingSchemaUrl);

void IConfigurationExtensionsLogger.LogInvalidConfigurationValue(string key, string value)
=> this.InvalidConfigurationValue(key, value);

Expand Down
84 changes: 83 additions & 1 deletion src/OpenTelemetry/Resources/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,27 @@ public class Resource
/// </summary>
/// <param name="attributes">An <see cref="IEnumerable{T}"/> of attributes that describe the resource.</param>
public Resource(IEnumerable<KeyValuePair<string, object>> attributes)
: this(attributes, schemaUrl: null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="Resource"/> class.
/// </summary>
/// <param name="attributes">An <see cref="IEnumerable{T}"/> of attributes that describe the resource.</param>
/// <param name="schemaUrl">The Schema URL (semantic conventions URL) that applies to the resource, or <see langword="null"/> if the resource has no Schema URL.</param>
#pragma warning disable CA1054 // Change the type of parameter from 'string' to 'System.Uri'
public Resource(IEnumerable<KeyValuePair<string, object>> attributes, string? schemaUrl)
#pragma warning restore CA1054 // Change the type of parameter from 'string' to 'System.Uri'
: this(attributes, schemaUrl, schemaUrlConflict: false)
{
}

private Resource(IEnumerable<KeyValuePair<string, object>> attributes, string? schemaUrl, bool schemaUrlConflict)
{
this.SchemaUrl = string.IsNullOrEmpty(schemaUrl) ? null : schemaUrl;
this.HasSchemaUrlConflict = schemaUrlConflict;

if (attributes == null)
{
OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attributes", "are null");
Expand All @@ -42,6 +62,23 @@ public Resource(IEnumerable<KeyValuePair<string, object>> attributes)
/// </summary>
public IEnumerable<KeyValuePair<string, object>> Attributes { get; }

#pragma warning disable CA1056 // Change the type of property from 'string' to 'System.Uri'
/// <summary>
/// Gets the Schema URL (semantic conventions URL) that applies to the resource, or <see langword="null"/> if the resource has no Schema URL.
/// </summary>
public string? SchemaUrl { get; }
#pragma warning restore CA1056 // Change the type of property from 'string' to 'System.Uri'

/// <summary>
/// Gets a value indicating whether a Schema URL conflict has occurred during a chain of merges.
/// </summary>
/// <remarks>
/// Once a conflict occurs the merged resource keeps an empty Schema URL, and the conflict is
/// "sticky" so that subsequent merges cannot recover a Schema URL. This keeps Build() deterministic
/// regardless of the number and order of detectors that contribute conflicting Schema URLs.
/// </remarks>
internal bool HasSchemaUrlConflict { get; }

/// <summary>
/// Returns a new, merged <see cref="Resource"/> by merging the old <see cref="Resource"/> with the
/// <c>other</c> <see cref="Resource"/>. In case of a collision the other <see cref="Resource"/> takes precedence.
Expand Down Expand Up @@ -71,7 +108,52 @@ public Resource Merge(Resource other)
}
}

return new Resource(newAttributes);
// A Schema URL conflict is sticky: if either resource in the chain already had a conflict the
// merged resource keeps an empty Schema URL, regardless of the other resource's Schema URL.
var conflict = this.HasSchemaUrlConflict || (other?.HasSchemaUrlConflict ?? false);

var mergedSchemaUrl = MergeSchemaUrl(this.SchemaUrl, other?.SchemaUrl, ref conflict);

return new Resource(newAttributes, mergedSchemaUrl, conflict);
}

// Implements the Schema URL merge logic from
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#merge
// where "this" is the old resource and "other" is the updating resource.
private static string? MergeSchemaUrl(string? oldSchemaUrl, string? updatingSchemaUrl, ref bool conflict)
{
// If a previous merge in the chain already conflicted then the conflict is sticky: the merged
// resource keeps an empty Schema URL and no further Schema URL can be recovered.
if (conflict)
{
return null;
}

// If the old resource's Schema URL is empty then the result is the updating resource's Schema URL.
if (oldSchemaUrl is not { Length: > 0 })
{
return updatingSchemaUrl;
}

// Else if the updating resource's Schema URL is empty then the result is the old resource's Schema URL.
if (updatingSchemaUrl is not { Length: > 0 })
{
return oldSchemaUrl;
}

// Else if the Schema URLs are the same then that is the result.
if (string.Equals(oldSchemaUrl, updatingSchemaUrl, StringComparison.Ordinal))
Comment thread
Kielek marked this conversation as resolved.
{
return oldSchemaUrl;
}

// Else this is a merging error: the Schema URLs are both non-empty and different. The spec leaves the
// result undefined; we log a warning and return null consistent with the Go, Java, JavaScript, and Rust SDKs.
OpenTelemetrySdkEventSource.Log.ResourceSchemaUrlMergeConflict(oldSchemaUrl, updatingSchemaUrl);

conflict = true;

return null;
}

private static KeyValuePair<string, object> SanitizeAttribute(KeyValuePair<string, object> attribute)
Expand Down
19 changes: 18 additions & 1 deletion src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,24 @@ public static ResourceBuilder AddAttributes(this ResourceBuilder resourceBuilder
}

/// <summary>
/// Adds resource attributes parsed from OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME environment variables
/// Adds attributes and a Schema URL to a <see cref="ResourceBuilder"/>.
/// </summary>
/// <param name="resourceBuilder"><see cref="ResourceBuilder"/>.</param>
/// <param name="attributes">An <see cref="IEnumerable{T}"/> of attributes that describe the resource.</param>
/// <param name="schemaUrl">The Schema URL (semantic conventions URL) that applies to the resource, or <see langword="null"/> if the resource has no Schema URL. See <see cref="Resource.Merge(Resource)"/> for how Schema URLs are combined.</param>
/// <returns>Returns <see cref="ResourceBuilder"/> for chaining.</returns>
#pragma warning disable CA1054 // Change the type of parameter from 'string' to 'System.Uri'
public static ResourceBuilder AddAttributes(this ResourceBuilder resourceBuilder, IEnumerable<KeyValuePair<string, object>> attributes, string? schemaUrl)
#pragma warning restore CA1054 // Change the type of parameter from 'string' to 'System.Uri'
{
Guard.ThrowIfNull(resourceBuilder);
#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1
return resourceBuilder.AddResource(new Resource(attributes, schemaUrl));
#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1
}

/// <summary>
/// Adds resource attributes parsed from the <c>OTEL_RESOURCE_ATTRIBUTES</c> and <c>OTEL_SERVICE_NAME</c> environment variables
/// to a <see cref="ResourceBuilder"/> following the <a
/// href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable">Resource
/// SDK</a>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ internal static class Generators
LogRecordSeverity.Warn,
]);

private static readonly Gen<string?> SchemaUrls = Gen.Elements(
[
null,
string.Empty,
"https://opentelemetry.io/schemas/1.0.0",
"https://opentelemetry.io/schemas/1.36.0",
]);

public static Arbitrary<SdkLimitOptions> SdkLimitOptionsArbitrary()
{
var gen = from spanAttributesLimit in Gen.Choose(0, 1000).Select(x => (int?)x)
Expand Down Expand Up @@ -132,7 +140,9 @@ public static Arbitrary<Resource> ResourceArbitrary()
attributes[$"resource.attr.{i}"] = $"value_{i}";
}

return Gen.Constant(Resource.Empty.Merge(new Resource(attributes)));
var schemaUrl = SchemaUrls.Sample(size, 1).First();

return Gen.Constant(Resource.Empty.Merge(new Resource(attributes, schemaUrl)));
});

return gen.ToArbitrary();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,60 @@ public Property WriteLogsDataHandlesInvalidSeverities() => Prop.ForAll(
}
});

[Property(MaxTest = 100)]
public Property ResourceSchemaUrlRoundTrips() => Prop.ForAll(
Generators.SdkLimitOptionsArbitrary(),
Generators.ResourceArbitrary(),
Generators.LogRecordSeverityArbitrary(),
(sdkLimits, resource, severity) =>
{
var logRecords = CreateLogRecords(severity);

try
{
var buffer = new byte[10 * 1024 * 1024];
var batch = new Batch<LogRecord>(logRecords, logRecords.Length);
var experimentalOptions = new ExperimentalOptions();

var writePos = ProtobufOtlpLogSerializer.WriteLogsData(
ref buffer,
0,
sdkLimits,
experimentalOptions,
resource,
batch);

if (writePos <= 0)
{
return true;
}

using var stream = new MemoryStream(buffer, 0, writePos);
var request = OtlpCollector.ExportLogsServiceRequest.Parser.ParseFrom(stream);

if (request == null || request.ResourceLogs.Count == 0)
{
return true;
}

var expected = resource.SchemaUrl ?? string.Empty;

foreach (var resourceLogs in request.ResourceLogs)
{
if (resourceLogs.SchemaUrl != expected)
{
return false;
}
}

return true;
}
catch (Exception ex) when (IsAllowedException(ex))
{
return true;
}
});

private static LogRecord[] CreateLogRecords(LogRecordSeverity severity)
=> Generators.LogRecordArbitrary(severity).Generator.ArrayOf().Sample(1, 10).First();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,53 @@ public Property SerializedOutputCanBeDeserialized() => Prop.ForAll(
}
});

[Property(MaxTest = 100)]
public Property ResourceSchemaUrlRoundTrips() => Prop.ForAll(
Generators.BatchMetricArbitrary(),
Generators.ResourceArbitrary(),
(metrics, resource) =>
{
try
{
var buffer = new byte[10 * 1024 * 1024];

var writePos = ProtobufOtlpMetricSerializer.WriteMetricsData(
ref buffer,
0,
resource,
metrics);

if (writePos <= 0)
{
return true;
}

using var stream = new MemoryStream(buffer, 0, writePos);
var request = ExportMetricsServiceRequest.Parser.ParseFrom(stream);

if (request == null || request.ResourceMetrics.Count == 0)
{
return true;
}

var expected = resource.SchemaUrl ?? string.Empty;

foreach (var resourceMetrics in request.ResourceMetrics)
{
if (resourceMetrics.SchemaUrl != expected)
{
return false;
}
}

return true;
}
catch (Exception ex) when (IsAllowedException(ex))
{
return true;
}
});

private static bool IsAllowedException(Exception ex)
=> ex is IndexOutOfRangeException or ArgumentException;
}
Loading