diff --git a/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt index e4f616a9dca..72461af5d64 100644 --- a/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -17,6 +17,7 @@ [OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Dispose() -> void [OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.Enumerator() -> void [OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Enumerator.MoveNext() -> bool +[OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.Equals(OpenTelemetry.Logs.LogRecordAttributeList other) -> bool [OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.GetEnumerator() -> OpenTelemetry.Logs.LogRecordAttributeList.Enumerator [OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.LogRecordAttributeList() -> void [OTEL1001]OpenTelemetry.Logs.LogRecordAttributeList.RecordException(System.Exception! exception) -> void @@ -26,6 +27,7 @@ [OTEL1001]OpenTelemetry.Logs.LogRecordData [OTEL1001]OpenTelemetry.Logs.LogRecordData.Body.get -> string? [OTEL1001]OpenTelemetry.Logs.LogRecordData.Body.set -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.Equals(OpenTelemetry.Logs.LogRecordData other) -> bool [OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData() -> void [OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData(in System.Diagnostics.ActivityContext activityContext) -> void [OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData(System.Diagnostics.Activity? activity) -> void @@ -70,6 +72,14 @@ [OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Warn3 = 15 -> OpenTelemetry.Logs.LogRecordSeverity [OTEL1001]OpenTelemetry.Logs.LogRecordSeverity.Warn4 = 16 -> OpenTelemetry.Logs.LogRecordSeverity [OTEL1001]OpenTelemetry.Logs.LogRecordSeverityExtensions +[OTEL1001]override OpenTelemetry.Logs.LogRecordAttributeList.Equals(object? obj) -> bool +[OTEL1001]override OpenTelemetry.Logs.LogRecordAttributeList.GetHashCode() -> int +[OTEL1001]override OpenTelemetry.Logs.LogRecordData.Equals(object? obj) -> bool +[OTEL1001]override OpenTelemetry.Logs.LogRecordData.GetHashCode() -> int [OTEL1001]static OpenTelemetry.Logs.LogRecordAttributeList.CreateFromEnumerable(System.Collections.Generic.IEnumerable>! attributes) -> OpenTelemetry.Logs.LogRecordAttributeList +[OTEL1001]static OpenTelemetry.Logs.LogRecordAttributeList.operator !=(OpenTelemetry.Logs.LogRecordAttributeList left, OpenTelemetry.Logs.LogRecordAttributeList right) -> bool +[OTEL1001]static OpenTelemetry.Logs.LogRecordAttributeList.operator ==(OpenTelemetry.Logs.LogRecordAttributeList left, OpenTelemetry.Logs.LogRecordAttributeList right) -> bool +[OTEL1001]static OpenTelemetry.Logs.LogRecordData.operator !=(OpenTelemetry.Logs.LogRecordData left, OpenTelemetry.Logs.LogRecordData right) -> bool +[OTEL1001]static OpenTelemetry.Logs.LogRecordData.operator ==(OpenTelemetry.Logs.LogRecordData left, OpenTelemetry.Logs.LogRecordData right) -> bool [OTEL1001]static OpenTelemetry.Logs.LogRecordSeverityExtensions.ToShortName(this OpenTelemetry.Logs.LogRecordSeverity logRecordSeverity) -> string! [OTEL1001]virtual OpenTelemetry.Logs.LoggerProvider.TryCreateLogger(string? name, out OpenTelemetry.Logs.Logger? logger) -> bool diff --git a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs index 96621ee27ed..05f1802bda8 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs @@ -25,9 +25,7 @@ namespace OpenTelemetry.Logs; /// internal #endif -#pragma warning disable CA1815 // Override equals and operator equals on value types - struct LogRecordAttributeList : IReadOnlyList> -#pragma warning restore CA1815 // Override equals and operator equals on value types + struct LogRecordAttributeList : IReadOnlyList>, IEquatable { internal const int OverflowMaxCount = 8; internal const int OverflowAdditionalCapacity = 16; @@ -122,6 +120,26 @@ public object? this[string key] set => this.Add(new KeyValuePair(key, value)); } + /// + /// Determines whether the two instances of are equal. + /// + /// An instance of . + /// Another instance of . + /// + /// if the values are considered equal; otherwise, . + /// + public static bool operator ==(LogRecordAttributeList left, LogRecordAttributeList right) => left.Equals(right); + + /// + /// Determines whether the two instances of are not equal. + /// + /// An instance of . + /// Another instance of . + /// + /// if the values are not considered equal; otherwise, . + /// + public static bool operator !=(LogRecordAttributeList left, LogRecordAttributeList right) => !(left == right); + /// /// Create a collection from an enumerable. /// @@ -137,6 +155,60 @@ public static LogRecordAttributeList CreateFromEnumerable(IEnumerable + public override readonly bool Equals(object? obj) => + obj is LogRecordAttributeList other && this.Equals(other); + + /// + public readonly bool Equals(LogRecordAttributeList other) + { + if (this.count != other.count) + { + return false; + } + + for (int i = 0; i < this.count; i++) + { + if (!this[i].Equals(other[i])) + { + return false; + } + } + + return true; + } + + /// + public override readonly int GetHashCode() + { +#if NET + HashCode combined = default; + + for (int i = 0; i < this.count; i++) + { + var item = this[i]; + combined.Add(item.Key); + combined.Add(item.Value); + } + + return combined.ToHashCode(); +#else + unchecked + { + var hash = 17; + + for (int i = 0; i < this.count; i++) + { + var item = this[i]; + hash = (hash * 31) + item.Key.GetHashCode(); + hash = (hash * 31) + (item.Value?.GetHashCode() ?? 0); + } + + return hash; + } +#endif + } + /// /// Add an attribute. /// diff --git a/src/OpenTelemetry.Api/Logs/LogRecordData.cs b/src/OpenTelemetry.Api/Logs/LogRecordData.cs index 374b95d736a..0c13f031421 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordData.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordData.cs @@ -22,9 +22,7 @@ namespace OpenTelemetry.Logs; /// internal #endif -#pragma warning disable CA1815 // Override equals and operator equals on value types - struct LogRecordData -#pragma warning restore CA1815 // Override equals and operator equals on value types + struct LogRecordData : IEquatable { internal DateTime TimestampBacking = DateTime.UtcNow; @@ -89,7 +87,7 @@ public LogRecordData(in ActivityContext activityContext) public DateTime Timestamp { readonly get => this.TimestampBacking; - set { this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; } + set => this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; } /// @@ -128,6 +126,71 @@ public DateTime Timestamp /// public string? EventName { get; set; } = null; + /// + /// Determines whether the two instances of are equal. + /// + /// An instance of . + /// Another instance of . + /// + /// if the values are considered equal; otherwise, . + /// + public static bool operator ==(LogRecordData left, LogRecordData right) => left.Equals(right); + + /// + /// Determines whether the two instances of are not equal. + /// + /// An instance of . + /// Another instance of . + /// + /// if the values are not considered equal; otherwise, . + /// + public static bool operator !=(LogRecordData left, LogRecordData right) => !(left == right); + + /// + public override readonly bool Equals(object? obj) => + obj is LogRecordData other && this.Equals(other); + + /// + public readonly bool Equals(LogRecordData other) => + this.TimestampBacking == other.TimestampBacking && + this.TraceId == other.TraceId && + this.SpanId == other.SpanId && + this.TraceFlags == other.TraceFlags && + this.Severity == other.Severity && + this.SeverityText == other.SeverityText && + this.Body == other.Body && + this.EventName == other.EventName; + + /// + public override readonly int GetHashCode() +#if NET + => HashCode.Combine( + this.TimestampBacking, + this.TraceId, + this.SpanId, + this.TraceFlags, + this.Severity, + this.SeverityText, + this.Body, + this.EventName); +#else + { + unchecked + { + var hash = 17; + hash = (hash * 31) + this.TimestampBacking.GetHashCode(); + hash = (hash * 31) + this.TraceId.GetHashCode(); + hash = (hash * 31) + this.SpanId.GetHashCode(); + hash = (hash * 31) + this.TraceFlags.GetHashCode(); + hash = (hash * 31) + (this.Severity?.GetHashCode() ?? 0); + hash = (hash * 31) + (this.SeverityText?.GetHashCode() ?? 0); + hash = (hash * 31) + (this.Body?.GetHashCode() ?? 0); + hash = (hash * 31) + (this.EventName?.GetHashCode() ?? 0); + return hash; + } + } +#endif + internal static void SetActivityContext(ref LogRecordData data, Activity? activity) { if (activity != null) diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs index 4e0aaacb89f..1e730abd7be 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs @@ -157,4 +157,147 @@ public void InitializerIndexesSyntaxTest() Assert.Equal(2, list.Count); } + + [Fact] + public void Equals_Object_ReturnsTrueForSameAttributes() + { + var left = new LogRecordAttributeList + { + ["key1"] = "value1", + ["key2"] = 123, + }; + + var right = new LogRecordAttributeList + { + ["key1"] = "value1", + ["key2"] = 123, + }; + + Assert.True(left.Equals((object)right)); + Assert.True(((object)left).Equals(right)); + } + + [Fact] + public void Equals_Object_ReturnsFalseForDifferentAttributes() + { + var left = new LogRecordAttributeList + { + ["key1"] = "value1", + ["key2"] = 123, + }; + + var right = new LogRecordAttributeList + { + ["key1"] = "value2", + ["key2"] = 123, + }; + + Assert.False(left.Equals((object)right)); + Assert.False(((object)left).Equals(right)); + } + + [Fact] + public void Equals_Object_ReturnsFalseForAnotherType() + { + var left = new LogRecordAttributeList + { + ["key1"] = "value1", + ["key2"] = 123, + }; + + var right = "foo"; + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsTrueForSameAttributes() + { + var left = new LogRecordAttributeList + { + ["a"] = 1, + }; + + var right = new LogRecordAttributeList + { + ["a"] = 1, + }; + + Assert.True(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferentCount() + { + var left = new LogRecordAttributeList + { + ["a"] = 1, + }; + + var right = default(LogRecordAttributeList); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Operator_Equality_ReturnsTrueForEqualLists() + { + var left = new LogRecordAttributeList + { + ["x"] = 42, + }; + var right = new LogRecordAttributeList + { + ["x"] = 42, + }; + + Assert.True(left == right); + Assert.False(left != right); + } + + [Fact] + public void Operator_Equality_ReturnsFalseForDifferentLists() + { + var left = new LogRecordAttributeList + { + ["x"] = 42, + }; + var right = new LogRecordAttributeList + { + ["x"] = 43, + }; + + Assert.False(left == right); + Assert.True(left != right); + } + + [Fact] + public void GetHashCode_SameForEqualLists() + { + var left = new LogRecordAttributeList + { + ["foo"] = "bar", + }; + var right = new LogRecordAttributeList + { + ["foo"] = "bar", + }; + + Assert.Equal(left.GetHashCode(), right.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentForDifferentLists() + { + var left = new LogRecordAttributeList + { + ["foo"] = "bar", + }; + var right = new LogRecordAttributeList + { + ["foo"] = "baz", + }; + + Assert.NotEqual(left.GetHashCode(), right.GetHashCode()); + } } diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs index 03d65ab3dd0..437d903ac39 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs @@ -123,4 +123,202 @@ public void SetActivityContextTest() Assert.Equal(default, record.SpanId); Assert.Equal(default, record.TraceFlags); } + + [Fact] + public void Equals_Object_ReturnsTrueForSameValues() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var timestamp = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + var left = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + var right = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + + Assert.True(left.Equals((object)right)); + Assert.True(((object)left).Equals(right)); + } + + [Fact] + public void Equals_Object_ReturnsFalseForDifferentValues() + { + var left = CreateSample(severityText: "Info"); + var right = CreateSample(severityText: "Warn"); + + Assert.False(left.Equals((object)right)); + Assert.False(((object)left).Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsTrueForSameValues() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var timestamp = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + var left = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + var right = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + + Assert.True(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_Timestamp() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(timestamp: new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), traceId: traceId, spanId: spanId); + var right = CreateSample(timestamp: new DateTime(2024, 1, 1, 0, 0, 1, DateTimeKind.Utc), traceId: traceId, spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_TraceId() + { + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(traceId: ActivityTraceId.CreateRandom(), spanId: spanId); + var right = CreateSample(traceId: ActivityTraceId.CreateRandom(), spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_SpanId() + { + var traceId = ActivityTraceId.CreateRandom(); + + var left = CreateSample(spanId: ActivitySpanId.CreateRandom(), traceId: traceId); + var right = CreateSample(spanId: ActivitySpanId.CreateRandom(), traceId: traceId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_Body() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(body: "A", traceId: traceId, spanId: spanId); + var right = CreateSample(body: "B", traceId: traceId, spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_TraceFlags() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(traceFlags: ActivityTraceFlags.Recorded, traceId: traceId, spanId: spanId); + var right = CreateSample(traceFlags: ActivityTraceFlags.None, traceId: traceId, spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_Severity() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(severity: LogRecordSeverity.Debug, traceId: traceId, spanId: spanId); + var right = CreateSample(severity: LogRecordSeverity.Debug2, traceId: traceId, spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_SeverityText() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(severityText: "foo", traceId: traceId, spanId: spanId); + var right = CreateSample(severityText: "bar", traceId: traceId, spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_Typed_ReturnsFalseForDifferent_EventName() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + + var left = CreateSample(eventName: "foo", traceId: traceId, spanId: spanId); + var right = CreateSample(eventName: "bar", traceId: traceId, spanId: spanId); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Operator_Equality_ReturnsTrueForEqualStructs() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var timestamp = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + var left = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + var right = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + + Assert.True(left == right); + Assert.False(left != right); + } + + [Fact] + public void Operator_Equality_ReturnsFalseForDifferentStructs() + { + var left = CreateSample(eventName: "A"); + var right = CreateSample(eventName: "B"); + + Assert.False(left == right); + Assert.True(left != right); + } + + [Fact] + public void GetHashCode_SameForEqualStructs() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var timestamp = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + var left = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + var right = CreateSample(timestamp, "Info", LogRecordSeverity.Info, "Body", "Event", traceId, spanId, ActivityTraceFlags.Recorded); + + Assert.Equal(left.GetHashCode(), right.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentForDifferentStructs() + { + var left = CreateSample(severity: LogRecordSeverity.Info); + var right = CreateSample(severity: LogRecordSeverity.Error); + + Assert.NotEqual(left.GetHashCode(), right.GetHashCode()); + } + + private static LogRecordData CreateSample( + DateTime? timestamp = null, + string? severityText = "Info", + LogRecordSeverity? severity = LogRecordSeverity.Info, + string? body = "Test body", + string? eventName = "TestEvent", + ActivityTraceId? traceId = null, + ActivitySpanId? spanId = null, + ActivityTraceFlags? traceFlags = null) => + new() + { + Timestamp = timestamp ?? new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + SeverityText = severityText, + Severity = severity, + Body = body, + EventName = eventName, + TraceId = traceId ?? ActivityTraceId.CreateRandom(), + SpanId = spanId ?? ActivitySpanId.CreateRandom(), + TraceFlags = traceFlags ?? ActivityTraceFlags.Recorded, + }; }