diff --git a/src/OpenTelemetry.Api/InstrumentationScope.cs b/src/OpenTelemetry.Api/InstrumentationScope.cs new file mode 100644 index 00000000000..9b6540d032e --- /dev/null +++ b/src/OpenTelemetry.Api/InstrumentationScope.cs @@ -0,0 +1,65 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +namespace OpenTelemetry; + +/// +/// Contains details about the library emitting telemetry. +/// +internal sealed class InstrumentationScope +{ + /// + /// Initializes a new instance of the class. + /// + public InstrumentationScope() + : this(name: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Optional name identifying the instrumentation library. + public InstrumentationScope(string? name) + { + this.Name = string.IsNullOrWhiteSpace(name) + ? string.Empty + : name!; + } + + /// + /// Gets the name identifying the instrumentation library. + /// + public string Name { get; } + + /// + /// Gets the version of the instrumentation library. + /// + public string? Version { get; init; } + + /// + /// Gets the schema url of the instrumentation library. + /// + public string? SchemaUrl { get; init; } + + /// + /// Gets the attributes which should be associated with log records created + /// by the instrumentation library. + /// + public IReadOnlyDictionary? Attributes { get; init; } +} diff --git a/src/OpenTelemetry/Internal/Shims/IsExternalInit.cs b/src/OpenTelemetry.Api/Internal/Shims/IsExternalInit.cs similarity index 100% rename from src/OpenTelemetry/Internal/Shims/IsExternalInit.cs rename to src/OpenTelemetry.Api/Internal/Shims/IsExternalInit.cs diff --git a/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs b/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs new file mode 100644 index 00000000000..698272fda89 --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/IDeferredLoggerProviderBuilder.cs @@ -0,0 +1,36 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +namespace OpenTelemetry.Logs; + +/// +/// Describes a logger provider builder that supports deferred +/// initialization using an to perform +/// dependency injection. +/// +internal interface IDeferredLoggerProviderBuilder +{ + /// + /// Register a callback action to configure the once the application is available. + /// + /// Configuration callback. + /// The supplied for chaining. + LoggerProviderBuilder Configure(Action configure); +} diff --git a/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs new file mode 100644 index 00000000000..ffe70191c5e --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs @@ -0,0 +1,333 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Logs; + +/// +/// Stores attributes to be added to a log message. +/// +internal struct LogRecordAttributeList : IReadOnlyList> +{ + internal const int OverflowMaxCount = 8; + internal const int OverflowAdditionalCapacity = 16; + internal List>? OverflowAttributes; + private KeyValuePair attribute1; + private KeyValuePair attribute2; + private KeyValuePair attribute3; + private KeyValuePair attribute4; + private KeyValuePair attribute5; + private KeyValuePair attribute6; + private KeyValuePair attribute7; + private KeyValuePair attribute8; + private int count; + + /// + public readonly int Count => this.count; + + /// + public KeyValuePair this[int index] + { + readonly get + { + if (this.OverflowAttributes is not null) + { + Debug.Assert(index < this.OverflowAttributes.Count, "Invalid index accessed."); + return this.OverflowAttributes[index]; + } + + if ((uint)index >= (uint)this.count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return index switch + { + 0 => this.attribute1, + 1 => this.attribute2, + 2 => this.attribute3, + 3 => this.attribute4, + 4 => this.attribute5, + 5 => this.attribute6, + 6 => this.attribute7, + 7 => this.attribute8, + _ => default, // we shouldn't come here anyway. + }; + } + + set + { + if (this.OverflowAttributes is not null) + { + Debug.Assert(index < this.OverflowAttributes.Count, "Invalid index accessed."); + this.OverflowAttributes[index] = value; + return; + } + + if ((uint)index >= (uint)this.count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + switch (index) + { + case 0: this.attribute1 = value; break; + case 1: this.attribute2 = value; break; + case 2: this.attribute3 = value; break; + case 3: this.attribute4 = value; break; + case 4: this.attribute5 = value; break; + case 5: this.attribute6 = value; break; + case 6: this.attribute7 = value; break; + case 7: this.attribute8 = value; break; + default: + Debug.Assert(false, "Unreachable code executed."); + break; + } + } + } + + /// + /// Add an attribute. + /// + /// Attribute name. + /// Attribute value. + [EditorBrowsable(EditorBrowsableState.Never)] + public object? this[string key] + { + // Note: This only exists to enable collection initializer syntax + // like { ["key"] = value }. + set => this.Add(new KeyValuePair(key, value)); + } + + /// + /// Create a collection from an enumerable. + /// + /// Source attributes. + /// . + public static LogRecordAttributeList CreateFromEnumerable(IEnumerable> attributes) + { + Guard.ThrowIfNull(attributes); + + LogRecordAttributeList logRecordAttributes = default; + logRecordAttributes.OverflowAttributes = new(attributes); + logRecordAttributes.count = logRecordAttributes.OverflowAttributes.Count; + return logRecordAttributes; + } + + /// + /// Add an attribute. + /// + /// Attribute name. + /// Attribute value. + public void Add(string key, object? value) + => this.Add(new KeyValuePair(key, value)); + + /// + /// Add an attribute. + /// + /// Attribute. + public void Add(KeyValuePair attribute) + { + var count = this.count++; + + if (count <= OverflowMaxCount) + { + switch (count) + { + case 0: this.attribute1 = attribute; return; + case 1: this.attribute2 = attribute; return; + case 2: this.attribute3 = attribute; return; + case 3: this.attribute4 = attribute; return; + case 4: this.attribute5 = attribute; return; + case 5: this.attribute6 = attribute; return; + case 6: this.attribute7 = attribute; return; + case 7: this.attribute8 = attribute; return; + case 8: + this.MoveAttributesToTheOverflowList(); + break; + } + } + + Debug.Assert(this.OverflowAttributes is not null, "Overflow attributes creation failure."); + this.OverflowAttributes!.Add(attribute); + } + + /// + /// Removes all elements from the . + /// + public void Clear() + { + this.count = 0; + this.OverflowAttributes?.Clear(); + } + + /// + /// Adds attributes representing an to the list. + /// + /// . + public void RecordException(Exception exception) + { + Guard.ThrowIfNull(exception); + + this.Add(SemanticConventions.AttributeExceptionType, exception.GetType().Name); + this.Add(SemanticConventions.AttributeExceptionMessage, exception.Message); + this.Add(SemanticConventions.AttributeExceptionStacktrace, exception.ToInvariantString()); + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// . + public readonly Enumerator GetEnumerator() + => new(in this); + + /// + readonly IEnumerator> IEnumerable>.GetEnumerator() => this.GetEnumerator(); + + /// + readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + internal readonly List>? Export(ref List>? attributeStorage) + { + int count = this.count; + if (count <= 0) + { + return null; + } + + var overflowAttributes = this.OverflowAttributes; + if (overflowAttributes != null) + { + // An allocation has already occurred, just use the list. + return overflowAttributes; + } + + Debug.Assert(count <= 8, "Invalid size detected."); + + attributeStorage ??= new List>(OverflowAdditionalCapacity); + + // TODO: Perf test this, adjust as needed. + if (count > 0) + { + attributeStorage.Add(this.attribute1); + if (count == 1) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute2); + if (count == 2) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute3); + if (count == 3) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute4); + if (count == 4) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute5); + if (count == 5) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute6); + if (count == 6) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute7); + if (count == 7) + { + return attributeStorage; + } + + attributeStorage.Add(this.attribute8); + } + + return attributeStorage; + } + + private void MoveAttributesToTheOverflowList() + { + Debug.Assert(this.count - 1 == OverflowMaxCount, "count did not match OverflowMaxCount"); + + var attributes = this.OverflowAttributes ??= new(OverflowAdditionalCapacity); + + attributes.Add(this.attribute1); + attributes.Add(this.attribute2); + attributes.Add(this.attribute3); + attributes.Add(this.attribute4); + attributes.Add(this.attribute5); + attributes.Add(this.attribute6); + attributes.Add(this.attribute7); + attributes.Add(this.attribute8); + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator>, IEnumerator + { + private LogRecordAttributeList attributes; + private int index; + + internal Enumerator(in LogRecordAttributeList attributes) + { + this.index = -1; + this.attributes = attributes; + } + + /// + public readonly KeyValuePair Current + => this.attributes[this.index]; + + /// + readonly object IEnumerator.Current => this.Current; + + /// + public bool MoveNext() + { + this.index++; + return this.index < this.attributes.Count; + } + + /// + public readonly void Dispose() + { + } + + /// + readonly void IEnumerator.Reset() + => throw new NotSupportedException(); + } +} diff --git a/src/OpenTelemetry.Api/Logs/LogRecordData.cs b/src/OpenTelemetry.Api/Logs/LogRecordData.cs new file mode 100644 index 00000000000..fec2c11dbe8 --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/LogRecordData.cs @@ -0,0 +1,140 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +using System.Diagnostics; + +namespace OpenTelemetry.Logs; + +/// +/// Stores details about a log message. +/// +internal struct LogRecordData +{ + internal DateTime TimestampBacking = DateTime.UtcNow; + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Notes: + /// + /// The property is initialized to automatically. + /// The , , and properties will be set using the instance. + /// + /// + public LogRecordData() + : this(Activity.Current?.Context ?? default) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Note: The property is initialized to automatically. + /// + /// Optional used to populate + /// trace context properties (, , + /// and ). + public LogRecordData(Activity? activity) + : this(activity?.Context ?? default) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// Note: The property is initialized to automatically. + /// + /// used to + /// populate trace context properties (, , and ). + public LogRecordData(in ActivityContext activityContext) + { + this.TraceId = activityContext.TraceId; + this.SpanId = activityContext.SpanId; + this.TraceFlags = activityContext.TraceFlags; + } + + /// + /// Gets or sets the log timestamp. + /// + /// + /// Note: If is set to a value with it will be automatically converted to + /// UTC using . + /// + public DateTime Timestamp + { + readonly get => this.TimestampBacking; + set { this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; } + } + + /// + /// Gets or sets the log . + /// + public ActivityTraceId TraceId { get; set; } + + /// + /// Gets or sets the log . + /// + public ActivitySpanId SpanId { get; set; } + + /// + /// Gets or sets the log . + /// + public ActivityTraceFlags TraceFlags { get; set; } + + /// + /// Gets or sets the original string representation of the severity as it is + /// known at the source. + /// + public string? SeverityText { get; set; } = null; + + /// + /// Gets or sets the log severity. + /// + public LogRecordSeverity? Severity { get; set; } = null; + + /// + /// Gets or sets the log body. + /// + public string? Body { get; set; } = null; + + internal static void SetActivityContext(ref LogRecordData data, Activity? activity) + { + if (activity != null) + { + data.TraceId = activity.TraceId; + data.SpanId = activity.SpanId; + data.TraceFlags = activity.ActivityTraceFlags; + } + else + { + data.TraceId = default; + data.SpanId = default; + data.TraceFlags = ActivityTraceFlags.None; + } + } +} diff --git a/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs b/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs new file mode 100644 index 00000000000..3256b530952 --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/LogRecordSeverity.cs @@ -0,0 +1,43 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +namespace OpenTelemetry.Logs; + +/// +/// Describes the severity level of a log record. +/// +internal enum LogRecordSeverity +{ + /// Trace severity. + Trace, + + /// Debug severity. + Debug, + + /// Information severity. + Information, + + /// Warning severity. + Warning, + + /// Error severity. + Error, + + /// Fatal severity. + Fatal, +} diff --git a/src/OpenTelemetry.Api/Logs/Logger.cs b/src/OpenTelemetry.Api/Logs/Logger.cs new file mode 100644 index 00000000000..6addbadd20e --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/Logger.cs @@ -0,0 +1,54 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Logs; + +/// +/// Logger is the class responsible for creating log records. +/// +internal abstract class Logger +{ + /// + /// Initializes a new instance of the class. + /// + /// . + protected Logger(InstrumentationScope instrumentationScope) + { + Guard.ThrowIfNull(instrumentationScope); + + this.InstrumentationScope = instrumentationScope; + } + + /// + /// Gets the associated + /// with the logger. + /// + public InstrumentationScope InstrumentationScope { get; } + + /// + /// Emit a log. + /// + /// . + /// . + public abstract void EmitLog( + in LogRecordData data, + in LogRecordAttributeList attributes = default); +} diff --git a/src/OpenTelemetry.Api/Logs/LoggerProvider.cs b/src/OpenTelemetry.Api/Logs/LoggerProvider.cs new file mode 100644 index 00000000000..6e0f07658b6 --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/LoggerProvider.cs @@ -0,0 +1,57 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +namespace OpenTelemetry.Logs; + +/// +/// LoggerProvider is the entry point of the OpenTelemetry API. It provides access to . +/// +internal class LoggerProvider : BaseProvider +{ + private static readonly NoopLogger NoopLogger = new(); + + /// + /// Initializes a new instance of the class. + /// + protected LoggerProvider() + { + } + + /// + /// Gets a logger. + /// + /// instance. + public Logger GetLogger() + => this.GetLogger(new InstrumentationScope()); + + /// + /// Gets a logger with the given name. + /// + /// Optional name identifying the instrumentation library. + /// instance. + public Logger GetLogger(string name) + => this.GetLogger(new InstrumentationScope(name)); + + /// + /// Gets a logger with given instrumentation scope. + /// + /// . + /// . + public virtual Logger GetLogger(InstrumentationScope instrumentationScope) + => NoopLogger; +} diff --git a/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs b/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs new file mode 100644 index 00000000000..5e525e73190 --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/LoggerProviderBuilder.cs @@ -0,0 +1,42 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +namespace OpenTelemetry.Logs; + +/// +/// LoggerProviderBuilder base class. +/// +internal abstract class LoggerProviderBuilder +{ + /// + /// Initializes a new instance of the class. + /// + protected LoggerProviderBuilder() + { + } + + /// + /// Adds instrumentation to the provider. + /// + /// Type of instrumentation class. + /// Function that builds instrumentation. + /// Returns for chaining. + public abstract LoggerProviderBuilder AddInstrumentation( + Func instrumentationFactory) + where TInstrumentation : class; +} diff --git a/src/OpenTelemetry.Api/Logs/NoopLogger.cs b/src/OpenTelemetry.Api/Logs/NoopLogger.cs new file mode 100644 index 00000000000..62c74ae8e6e --- /dev/null +++ b/src/OpenTelemetry.Api/Logs/NoopLogger.cs @@ -0,0 +1,33 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#nullable enable + +namespace OpenTelemetry.Logs; + +internal sealed class NoopLogger : Logger +{ + public NoopLogger() + : base(instrumentationScope: new()) + { + } + + public override void EmitLog( + in LogRecordData data, + in LogRecordAttributeList attributes = default) + { + } +} diff --git a/src/OpenTelemetry/Logs/LogRecordAttributeList.cs b/src/OpenTelemetry/Logs/LogRecordAttributeList.cs deleted file mode 100644 index 3b19a84b5f0..00000000000 --- a/src/OpenTelemetry/Logs/LogRecordAttributeList.cs +++ /dev/null @@ -1,324 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#nullable enable - -using System.Collections; -using System.ComponentModel; -using System.Diagnostics; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Logs -{ - /// - /// Stores attributes to be added to a log message. - /// - internal struct LogRecordAttributeList : IReadOnlyList> - { - internal const int OverflowAdditionalCapacity = 8; - internal List>? OverflowAttributes; - private KeyValuePair attribute1; - private KeyValuePair attribute2; - private KeyValuePair attribute3; - private KeyValuePair attribute4; - private KeyValuePair attribute5; - private KeyValuePair attribute6; - private KeyValuePair attribute7; - private KeyValuePair attribute8; - private int count; - - /// - public readonly int Count => this.count; - - /// - public KeyValuePair this[int index] - { - readonly get - { - if (this.OverflowAttributes is not null) - { - Debug.Assert(index < this.OverflowAttributes.Count, "Invalid index accessed."); - return this.OverflowAttributes[index]; - } - - if ((uint)index >= (uint)this.count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return index switch - { - 0 => this.attribute1, - 1 => this.attribute2, - 2 => this.attribute3, - 3 => this.attribute4, - 4 => this.attribute5, - 5 => this.attribute6, - 6 => this.attribute7, - 7 => this.attribute8, - _ => default, // we shouldn't come here anyway. - }; - } - - set - { - if (this.OverflowAttributes is not null) - { - Debug.Assert(index < this.OverflowAttributes.Count, "Invalid index accessed."); - this.OverflowAttributes[index] = value; - return; - } - - if ((uint)index >= (uint)this.count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - switch (index) - { - case 0: this.attribute1 = value; break; - case 1: this.attribute2 = value; break; - case 2: this.attribute3 = value; break; - case 3: this.attribute4 = value; break; - case 4: this.attribute5 = value; break; - case 5: this.attribute6 = value; break; - case 6: this.attribute7 = value; break; - case 7: this.attribute8 = value; break; - default: - Debug.Assert(false, "Unreachable code executed."); - break; - } - } - } - - /// - /// Add an attribute. - /// - /// Attribute name. - /// Attribute value. - [EditorBrowsable(EditorBrowsableState.Never)] - public object? this[string key] - { - // Note: This only exists to enable collection initializer syntax - // like { ["key"] = value }. - set => this.Add(new KeyValuePair(key, value)); - } - - /// - /// Create a collection from an enumerable. - /// - /// Source attributes. - /// . - public static LogRecordAttributeList CreateFromEnumerable(IEnumerable> attributes) - { - Guard.ThrowIfNull(attributes); - - LogRecordAttributeList logRecordAttributes = default; - logRecordAttributes.OverflowAttributes = new(attributes); - logRecordAttributes.count = logRecordAttributes.OverflowAttributes.Count; - return logRecordAttributes; - } - - /// - /// Add an attribute. - /// - /// Attribute name. - /// Attribute value. - public void Add(string key, object? value) - => this.Add(new KeyValuePair(key, value)); - - /// - /// Add an attribute. - /// - /// Attribute. - public void Add(KeyValuePair attribute) - { - if (this.OverflowAttributes is not null) - { - this.OverflowAttributes.Add(attribute); - this.count++; - return; - } - - Debug.Assert(this.count <= 8, "Item added beyond struct capacity."); - - switch (this.count) - { - case 0: this.attribute1 = attribute; break; - case 1: this.attribute2 = attribute; break; - case 2: this.attribute3 = attribute; break; - case 3: this.attribute4 = attribute; break; - case 4: this.attribute5 = attribute; break; - case 5: this.attribute6 = attribute; break; - case 6: this.attribute7 = attribute; break; - case 7: this.attribute8 = attribute; break; - case 8: - Debug.Assert(this.OverflowAttributes is null, "Overflow attributes already created."); - this.MoveAttributesToTheOverflowList(); - Debug.Assert(this.OverflowAttributes is not null, "Overflow attributes creation failure."); - this.OverflowAttributes!.Add(attribute); - break; - default: - // We shouldn't come here. - Debug.Assert(this.OverflowAttributes is null, "Unreachable code executed."); - return; - } - - this.count++; - } - - /// - /// Returns an enumerator that iterates through the . - /// - /// . - public readonly Enumerator GetEnumerator() - => new(in this); - - /// - readonly IEnumerator> IEnumerable>.GetEnumerator() => this.GetEnumerator(); - - /// - readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - internal readonly void ApplyToLogRecord(LogRecord logRecord) - { - int count = this.count; - if (count <= 0) - { - logRecord.StateValues = null; - return; - } - - var overflowAttributes = this.OverflowAttributes; - if (overflowAttributes != null) - { - // An allocation has already occurred, just use the buffer. - logRecord.StateValues = overflowAttributes; - return; - } - - Debug.Assert(count <= 8, "Invalid size detected."); - - var attributeStorage = logRecord.AttributeStorage ??= new List>(OverflowAdditionalCapacity); - - try - { - // TODO: Perf test this, adjust as needed. - - attributeStorage.Add(this.attribute1); - if (count == 1) - { - return; - } - - attributeStorage.Add(this.attribute2); - if (count == 2) - { - return; - } - - attributeStorage.Add(this.attribute3); - if (count == 3) - { - return; - } - - attributeStorage.Add(this.attribute4); - if (count == 4) - { - return; - } - - attributeStorage.Add(this.attribute5); - if (count == 5) - { - return; - } - - attributeStorage.Add(this.attribute6); - if (count == 6) - { - return; - } - - attributeStorage.Add(this.attribute7); - if (count == 7) - { - return; - } - - attributeStorage.Add(this.attribute8); - } - finally - { - logRecord.StateValues = attributeStorage; - } - } - - private void MoveAttributesToTheOverflowList() - { - this.OverflowAttributes = new(16) - { - { this.attribute1 }, - { this.attribute2 }, - { this.attribute3 }, - { this.attribute4 }, - { this.attribute5 }, - { this.attribute6 }, - { this.attribute7 }, - { this.attribute8 }, - }; - } - - /// - /// Enumerates the elements of a . - /// - public struct Enumerator : IEnumerator>, IEnumerator - { - private LogRecordAttributeList attributes; - private int index; - - internal Enumerator(in LogRecordAttributeList attributes) - { - this.index = -1; - this.attributes = attributes; - } - - /// - public readonly KeyValuePair Current - => this.attributes[this.index]; - - /// - readonly object IEnumerator.Current => this.Current; - - /// - public bool MoveNext() - { - this.index++; - return this.index < this.attributes.Count; - } - - /// - public readonly void Dispose() - { - } - - /// - readonly void IEnumerator.Reset() - => throw new NotSupportedException(); - } - } -} diff --git a/src/OpenTelemetry/Logs/LogRecordData.cs b/src/OpenTelemetry/Logs/LogRecordData.cs deleted file mode 100644 index cd72425efe3..00000000000 --- a/src/OpenTelemetry/Logs/LogRecordData.cs +++ /dev/null @@ -1,125 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#nullable enable - -using System.Diagnostics; - -namespace OpenTelemetry.Logs -{ - /// - /// Stores details about a log message. - /// - internal struct LogRecordData - { - internal DateTime TimestampBacking = DateTime.UtcNow; - - /// - /// Initializes a new instance of the struct. - /// - public LogRecordData() - : this(activity: null) - { - } - - /// - /// Initializes a new instance of the struct. - /// - /// - /// Note: The property is initialized to automatically. - /// - /// Optional used to populate context fields. - public LogRecordData(Activity? activity) - { - if (activity != null) - { - this.TraceId = activity.TraceId; - this.SpanId = activity.SpanId; - this.TraceFlags = activity.ActivityTraceFlags; - } - else - { - this.TraceId = default; - this.SpanId = default; - this.TraceFlags = ActivityTraceFlags.None; - } - } - - /// - /// Gets or sets the log timestamp. - /// - /// - /// Note: If is set to a value with it will be automatically converted to - /// UTC using . - /// - public DateTime Timestamp - { - readonly get => this.TimestampBacking; - set { this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; } - } - - /// - /// Gets or sets the log . - /// - public ActivityTraceId TraceId { get; set; } - - /// - /// Gets or sets the log . - /// - public ActivitySpanId SpanId { get; set; } - - /// - /// Gets or sets the log . - /// - public ActivityTraceFlags TraceFlags { get; set; } - - /* TODO: Add these when log api/sdk spec is stable. - /// - /// Gets or sets the original string representation of the severity as it is - /// known at the source. - /// - public string? SeverityText { get; set; } = null; - - /// - /// Gets or sets the log severity. - /// - public LogRecordSeverity? Severity { get; set; } = null; - */ - - /// - /// Gets or sets the log body. - /// - public string? Body { get; set; } = null; - - internal static void SetActivityContext(ref LogRecordData data, Activity? activity = null) - { - if (activity != null) - { - data.TraceId = activity.TraceId; - data.SpanId = activity.SpanId; - data.TraceFlags = activity.ActivityTraceFlags; - } - else - { - data.TraceId = default; - data.SpanId = default; - data.TraceFlags = ActivityTraceFlags.None; - } - } - } -} diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs index 18d052584f2..2b92c974a33 100644 --- a/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs +++ b/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs @@ -125,9 +125,14 @@ public bool IsEnabled(LogLevel logLevel) /* TODO: Enable this if/when LogRecordAttributeList becomes public. if (typeof(TState) == typeof(LogRecordAttributeList)) { - // Note: This cast looks strange, but it is meant for the JIT to optimize/remove. - ((LogRecordAttributeList)(object)state!).ApplyToLogRecord(logRecord); - return logRecord.AttributeStorage!; + // Note: This block is written to be elided by the JIT when + // TState is not LogRecordAttributeList or optimized when it is. + // For users that pass LogRecordAttributeList as TState to + // ILogger.Log this will avoid boxing the struct. + + var logRecordAttributes = (LogRecordAttributeList)(object)state!; + + return logRecordAttributes.Export(ref logRecord.AttributeStorage); } else */ if (state is IReadOnlyList> stateList) diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs new file mode 100644 index 00000000000..21d856c9799 --- /dev/null +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordAttributeListTests.cs @@ -0,0 +1,145 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Xunit; + +namespace OpenTelemetry.Logs.Tests; + +public sealed class LogRecordAttributeListTests +{ + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(8)] + [InlineData(9)] + [InlineData(64)] + public void ReadWriteTest(int numberOfItems) + { + LogRecordAttributeList attributes = default; + + for (int i = 0; i < numberOfItems; i++) + { + attributes.Add($"key{i}", i); + } + + Assert.Equal(numberOfItems, attributes.Count); + + for (int i = 0; i < numberOfItems; i++) + { + var item = attributes[i]; + + Assert.Equal($"key{i}", item.Key); + Assert.Equal(i, (int)item.Value); + } + + int index = 0; + foreach (KeyValuePair item in attributes) + { + Assert.Equal($"key{index}", item.Key); + Assert.Equal(index, (int)item.Value); + index++; + } + + if (attributes.Count <= LogRecordAttributeList.OverflowMaxCount) + { + Assert.Null(attributes.OverflowAttributes); + } + else + { + Assert.NotNull(attributes.OverflowAttributes); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(8)] + [InlineData(9)] + [InlineData(64)] + public void ClearTest(int numberOfItems) + { + LogRecordAttributeList attributes = default; + + for (int c = 0; c <= 1; c++) + { + for (int i = 0; i < numberOfItems; i++) + { + attributes.Add($"key{i}", i); + } + + Assert.Equal(numberOfItems, attributes.Count); + + for (int i = 0; i < numberOfItems; i++) + { + var item = attributes[i]; + + Assert.Equal($"key{i}", item.Key); + Assert.Equal(i, (int)item.Value); + } + + attributes.Clear(); + + Assert.Empty(attributes); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(8)] + [InlineData(9)] + [InlineData(64)] + public void ExportTest(int numberOfItems) + { + LogRecordAttributeList attributes = default; + + for (int i = 0; i < numberOfItems; i++) + { + attributes.Add($"key{i}", i); + } + + List> storage = null; + + var exportedAttributes = attributes.Export(ref storage); + + if (numberOfItems == 0) + { + Assert.Null(exportedAttributes); + Assert.Null(storage); + return; + } + + Assert.NotNull(exportedAttributes); + + if (numberOfItems <= LogRecordAttributeList.OverflowMaxCount) + { + Assert.NotNull(storage); + Assert.True(ReferenceEquals(storage, exportedAttributes)); + } + else + { + Assert.Null(storage); + } + + int index = 0; + foreach (KeyValuePair item in exportedAttributes) + { + Assert.Equal($"key{index}", item.Key); + Assert.Equal(index, (int)item.Value); + index++; + } + } +} diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs new file mode 100644 index 00000000000..09d15e2b0f8 --- /dev/null +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs @@ -0,0 +1,139 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Diagnostics; +using Xunit; + +namespace OpenTelemetry.Logs.Tests; + +public sealed class LogRecordDataTests +{ + [Fact] + public void ParameterlessConstructorWithActiveActivityTest() + { + using var activity = new Activity("Test"); + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; + activity.Start(); + + var record = new LogRecordData(); + + Assert.Equal(activity.TraceId, record.TraceId); + Assert.Equal(activity.SpanId, record.SpanId); + Assert.Equal(activity.ActivityTraceFlags, record.TraceFlags); + + record = default; + + Assert.Equal(default, record.TraceId); + Assert.Equal(default, record.SpanId); + Assert.Equal(default, record.TraceFlags); + } + + [Fact] + public void ParameterlessConstructorWithoutActiveActivityTest() + { + var record = new LogRecordData(); + + Assert.Equal(default, record.TraceId); + Assert.Equal(default, record.SpanId); + Assert.Equal(default, record.TraceFlags); + } + + [Fact] + public void ActivityConstructorTest() + { + using var activity = new Activity("Test"); + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; + activity.Start(); + + Activity.Current = null; + + var record = new LogRecordData(activity); + + Assert.Equal(activity.TraceId, record.TraceId); + Assert.Equal(activity.SpanId, record.SpanId); + Assert.Equal(activity.ActivityTraceFlags, record.TraceFlags); + + record = new LogRecordData(activity: null); + + Assert.Equal(default, record.TraceId); + Assert.Equal(default, record.SpanId); + Assert.Equal(default, record.TraceFlags); + } + + [Fact] + public void ActivityContextConstructorTest() + { + var context = new ActivityContext( + ActivityTraceId.CreateRandom(), + ActivitySpanId.CreateRandom(), + ActivityTraceFlags.Recorded, + traceState: null, + isRemote: true); + + var record = new LogRecordData(in context); + + Assert.Equal(context.TraceId, record.TraceId); + Assert.Equal(context.SpanId, record.SpanId); + Assert.Equal(context.TraceFlags, record.TraceFlags); + + record = new LogRecordData(activityContext: default); + + Assert.Equal(default, record.TraceId); + Assert.Equal(default, record.SpanId); + Assert.Equal(default, record.TraceFlags); + } + + [Fact] + public void TimestampTest() + { + var nowUtc = DateTime.UtcNow; + + var record = new LogRecordData(); + Assert.True(record.Timestamp >= nowUtc); + + record = default; + Assert.Equal(DateTime.MinValue, record.Timestamp); + + var now = DateTime.Now; + + record.Timestamp = now; + + Assert.Equal(DateTimeKind.Utc, record.Timestamp.Kind); + Assert.Equal(now.ToUniversalTime(), record.Timestamp); + } + + [Fact] + public void SetActivityContextTest() + { + LogRecordData record = default; + + using var activity = new Activity("Test"); + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; + activity.Start(); + + LogRecordData.SetActivityContext(ref record, activity); + + Assert.Equal(activity.TraceId, record.TraceId); + Assert.Equal(activity.SpanId, record.SpanId); + Assert.Equal(activity.ActivityTraceFlags, record.TraceFlags); + + LogRecordData.SetActivityContext(ref record, activity: null); + + Assert.Equal(default, record.TraceId); + Assert.Equal(default, record.SpanId); + Assert.Equal(default, record.TraceFlags); + } +} diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordAttributeListTests.cs deleted file mode 100644 index 255076614e2..00000000000 --- a/test/OpenTelemetry.Tests/Logs/LogRecordAttributeListTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using Xunit; - -namespace OpenTelemetry.Logs.Tests -{ - public sealed class LogRecordAttributeListTests - { - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(8)] - [InlineData(9)] - [InlineData(64)] - public void ReadWriteTest(int numberOfItems) - { - LogRecordAttributeList attributes = default; - - for (int i = 0; i < numberOfItems; i++) - { - attributes.Add($"key{i}", i); - } - - Assert.Equal(numberOfItems, attributes.Count); - - for (int i = 0; i < numberOfItems; i++) - { - var item = attributes[i]; - - Assert.Equal($"key{i}", item.Key); - Assert.Equal(i, (int)item.Value); - } - - int index = 0; - foreach (KeyValuePair item in attributes) - { - Assert.Equal($"key{index}", item.Key); - Assert.Equal(index, (int)item.Value); - index++; - } - - if (attributes.Count <= LogRecordAttributeList.OverflowAdditionalCapacity) - { - Assert.Null(attributes.OverflowAttributes); - } - else - { - Assert.NotNull(attributes.OverflowAttributes); - } - } - - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(8)] - [InlineData(9)] - [InlineData(64)] - public void ApplyToLogRecordTest(int numberOfItems) - { - LogRecordAttributeList attributes = default; - - for (int i = 0; i < numberOfItems; i++) - { - attributes.Add($"key{i}", i); - } - - LogRecord logRecord = new(); - - attributes.ApplyToLogRecord(logRecord); - - if (numberOfItems == 0) - { - Assert.Null(logRecord.StateValues); - return; - } - - Assert.NotNull(logRecord.StateValues); - - int index = 0; - foreach (KeyValuePair item in logRecord.StateValues) - { - Assert.Equal($"key{index}", item.Key); - Assert.Equal(index, (int)item.Value); - index++; - } - } - } -}