Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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}'. The updating Schema URL will be used for the merged resource.", Level = EventLevel.Warning)]
Comment thread
Kielek marked this conversation as resolved.
Outdated
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
54 changes: 53 additions & 1 deletion src/OpenTelemetry/Resources/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,21 @@ 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.SchemaUrl = string.IsNullOrEmpty(schemaUrl) ? null : schemaUrl;

if (attributes == null)
{
OpenTelemetrySdkEventSource.Log.InvalidArgument("Create resource", "attributes", "are null");
Expand All @@ -42,6 +56,13 @@ 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>
/// 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 +92,38 @@ public Resource Merge(Resource other)
}
}

return new Resource(newAttributes);
return new Resource(newAttributes, MergeSchemaUrl(this.SchemaUrl, other?.SchemaUrl));
}

// 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)
{
// 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 use the updating resource's Schema URL, consistent with the
// attribute merge precedence where the updating ("other") resource takes precedence.
OpenTelemetrySdkEventSource.Log.ResourceSchemaUrlMergeConflict(oldSchemaUrl, updatingSchemaUrl);

return updatingSchemaUrl;
}

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