[api-logs] Add internal artifacts for OTel spec log bridge API (#4422)

This commit is contained in:
Mikel Blanchard 2023-04-25 10:20:17 -07:00 committed by GitHub
parent ef0c921708
commit 6a64dd2bc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1095 additions and 554 deletions

View File

@ -0,0 +1,65 @@
// <copyright file="InstrumentationScope.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
namespace OpenTelemetry;
/// <summary>
/// Contains details about the library emitting telemetry.
/// </summary>
internal sealed class InstrumentationScope
{
/// <summary>
/// Initializes a new instance of the <see cref="InstrumentationScope"/> class.
/// </summary>
public InstrumentationScope()
: this(name: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InstrumentationScope"/> class.
/// </summary>
/// <param name="name">Optional name identifying the instrumentation library.</param>
public InstrumentationScope(string? name)
{
this.Name = string.IsNullOrWhiteSpace(name)
? string.Empty
: name!;
}
/// <summary>
/// Gets the name identifying the instrumentation library.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the version of the instrumentation library.
/// </summary>
public string? Version { get; init; }
/// <summary>
/// Gets the schema url of the instrumentation library.
/// </summary>
public string? SchemaUrl { get; init; }
/// <summary>
/// Gets the attributes which should be associated with log records created
/// by the instrumentation library.
/// </summary>
public IReadOnlyDictionary<string, object>? Attributes { get; init; }
}

View File

@ -0,0 +1,36 @@
// <copyright file="IDeferredLoggerProviderBuilder.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
namespace OpenTelemetry.Logs;
/// <summary>
/// Describes a logger provider builder that supports deferred
/// initialization using an <see cref="IServiceProvider"/> to perform
/// dependency injection.
/// </summary>
internal interface IDeferredLoggerProviderBuilder
{
/// <summary>
/// Register a callback action to configure the <see
/// cref="LoggerProviderBuilder"/> once the application <see
/// cref="IServiceProvider"/> is available.
/// </summary>
/// <param name="configure">Configuration callback.</param>
/// <returns>The supplied <see cref="LoggerProviderBuilder"/> for chaining.</returns>
LoggerProviderBuilder Configure(Action<IServiceProvider, LoggerProviderBuilder> configure);
}

View File

@ -0,0 +1,333 @@
// <copyright file="LogRecordAttributeList.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;
namespace OpenTelemetry.Logs;
/// <summary>
/// Stores attributes to be added to a log message.
/// </summary>
internal struct LogRecordAttributeList : IReadOnlyList<KeyValuePair<string, object?>>
{
internal const int OverflowMaxCount = 8;
internal const int OverflowAdditionalCapacity = 16;
internal List<KeyValuePair<string, object?>>? OverflowAttributes;
private KeyValuePair<string, object?> attribute1;
private KeyValuePair<string, object?> attribute2;
private KeyValuePair<string, object?> attribute3;
private KeyValuePair<string, object?> attribute4;
private KeyValuePair<string, object?> attribute5;
private KeyValuePair<string, object?> attribute6;
private KeyValuePair<string, object?> attribute7;
private KeyValuePair<string, object?> attribute8;
private int count;
/// <inheritdoc/>
public readonly int Count => this.count;
/// <inheritdoc/>
public KeyValuePair<string, object?> 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;
}
}
}
/// <summary>
/// Add an attribute.
/// </summary>
/// <param name="key">Attribute name.</param>
/// <returns>Attribute value.</returns>
[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<string, object?>(key, value));
}
/// <summary>
/// Create a <see cref="LogRecordAttributeList"/> collection from an enumerable.
/// </summary>
/// <param name="attributes">Source attributes.</param>
/// <returns><see cref="LogRecordAttributeList"/>.</returns>
public static LogRecordAttributeList CreateFromEnumerable(IEnumerable<KeyValuePair<string, object?>> attributes)
{
Guard.ThrowIfNull(attributes);
LogRecordAttributeList logRecordAttributes = default;
logRecordAttributes.OverflowAttributes = new(attributes);
logRecordAttributes.count = logRecordAttributes.OverflowAttributes.Count;
return logRecordAttributes;
}
/// <summary>
/// Add an attribute.
/// </summary>
/// <param name="key">Attribute name.</param>
/// <param name="value">Attribute value.</param>
public void Add(string key, object? value)
=> this.Add(new KeyValuePair<string, object?>(key, value));
/// <summary>
/// Add an attribute.
/// </summary>
/// <param name="attribute">Attribute.</param>
public void Add(KeyValuePair<string, object?> 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);
}
/// <summary>
/// Removes all elements from the <see cref="LogRecordAttributeList"/>.
/// </summary>
public void Clear()
{
this.count = 0;
this.OverflowAttributes?.Clear();
}
/// <summary>
/// Adds attributes representing an <see cref="Exception"/> to the list.
/// </summary>
/// <param name="exception"><see cref="Exception"/>.</param>
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());
}
/// <summary>
/// Returns an enumerator that iterates through the <see cref="LogRecordAttributeList"/>.
/// </summary>
/// <returns><see cref="Enumerator"/>.</returns>
public readonly Enumerator GetEnumerator()
=> new(in this);
/// <inheritdoc/>
readonly IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
internal readonly List<KeyValuePair<string, object?>>? Export(ref List<KeyValuePair<string, object?>>? 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<KeyValuePair<string, object?>>(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);
}
/// <summary>
/// Enumerates the elements of a <see cref="LogRecordAttributeList"/>.
/// </summary>
public struct Enumerator : IEnumerator<KeyValuePair<string, object?>>, IEnumerator
{
private LogRecordAttributeList attributes;
private int index;
internal Enumerator(in LogRecordAttributeList attributes)
{
this.index = -1;
this.attributes = attributes;
}
/// <inheritdoc/>
public readonly KeyValuePair<string, object?> Current
=> this.attributes[this.index];
/// <inheritdoc/>
readonly object IEnumerator.Current => this.Current;
/// <inheritdoc/>
public bool MoveNext()
{
this.index++;
return this.index < this.attributes.Count;
}
/// <inheritdoc/>
public readonly void Dispose()
{
}
/// <inheritdoc/>
readonly void IEnumerator.Reset()
=> throw new NotSupportedException();
}
}

View File

@ -0,0 +1,140 @@
// <copyright file="LogRecordData.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
using System.Diagnostics;
namespace OpenTelemetry.Logs;
/// <summary>
/// Stores details about a log message.
/// </summary>
internal struct LogRecordData
{
internal DateTime TimestampBacking = DateTime.UtcNow;
/// <summary>
/// Initializes a new instance of the <see cref="LogRecordData"/> struct.
/// </summary>
/// <remarks>
/// Notes:
/// <list type="bullet">
/// <item>The <see cref="Timestamp"/> property is initialized to <see
/// cref="DateTime.UtcNow"/> automatically.</item>
/// <item>The <see cref="TraceId"/>, <see cref="SpanId"/>, and <see
/// cref="TraceFlags"/> properties will be set using the <see
/// cref="Activity.Current"/> instance.</item>
/// </list>
/// </remarks>
public LogRecordData()
: this(Activity.Current?.Context ?? default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LogRecordData"/> struct.
/// </summary>
/// <remarks>
/// Note: The <see cref="Timestamp"/> property is initialized to <see
/// cref="DateTime.UtcNow"/> automatically.
/// </remarks>
/// <param name="activity">Optional <see cref="Activity"/> used to populate
/// trace context properties (<see cref="TraceId"/>, <see cref="SpanId"/>,
/// and <see cref="TraceFlags"/>).</param>
public LogRecordData(Activity? activity)
: this(activity?.Context ?? default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LogRecordData"/> struct.
/// </summary>
/// <remarks>
/// Note: The <see cref="Timestamp"/> property is initialized to <see
/// cref="DateTime.UtcNow"/> automatically.
/// </remarks>
/// <param name="activityContext"><see cref="ActivityContext"/> used to
/// populate trace context properties (<see cref="TraceId"/>, <see
/// cref="SpanId"/>, and <see cref="TraceFlags"/>).</param>
public LogRecordData(in ActivityContext activityContext)
{
this.TraceId = activityContext.TraceId;
this.SpanId = activityContext.SpanId;
this.TraceFlags = activityContext.TraceFlags;
}
/// <summary>
/// Gets or sets the log timestamp.
/// </summary>
/// <remarks>
/// Note: If <see cref="Timestamp"/> is set to a value with <see
/// cref="DateTimeKind.Local"/> it will be automatically converted to
/// UTC using <see cref="DateTime.ToUniversalTime"/>.
/// </remarks>
public DateTime Timestamp
{
readonly get => this.TimestampBacking;
set { this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; }
}
/// <summary>
/// Gets or sets the log <see cref="ActivityTraceId"/>.
/// </summary>
public ActivityTraceId TraceId { get; set; }
/// <summary>
/// Gets or sets the log <see cref="ActivitySpanId"/>.
/// </summary>
public ActivitySpanId SpanId { get; set; }
/// <summary>
/// Gets or sets the log <see cref="ActivityTraceFlags"/>.
/// </summary>
public ActivityTraceFlags TraceFlags { get; set; }
/// <summary>
/// Gets or sets the original string representation of the severity as it is
/// known at the source.
/// </summary>
public string? SeverityText { get; set; } = null;
/// <summary>
/// Gets or sets the log severity.
/// </summary>
public LogRecordSeverity? Severity { get; set; } = null;
/// <summary>
/// Gets or sets the log body.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,43 @@
// <copyright file="LogRecordSeverity.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
namespace OpenTelemetry.Logs;
/// <summary>
/// Describes the severity level of a log record.
/// </summary>
internal enum LogRecordSeverity
{
/// <summary>Trace severity.</summary>
Trace,
/// <summary>Debug severity.</summary>
Debug,
/// <summary>Information severity.</summary>
Information,
/// <summary>Warning severity.</summary>
Warning,
/// <summary>Error severity.</summary>
Error,
/// <summary>Fatal severity.</summary>
Fatal,
}

View File

@ -0,0 +1,54 @@
// <copyright file="Logger.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
using OpenTelemetry.Internal;
namespace OpenTelemetry.Logs;
/// <summary>
/// Logger is the class responsible for creating log records.
/// </summary>
internal abstract class Logger
{
/// <summary>
/// Initializes a new instance of the <see cref="Logger"/> class.
/// </summary>
/// <param name="instrumentationScope"><see
/// cref="OpenTelemetry.InstrumentationScope"/>.</param>
protected Logger(InstrumentationScope instrumentationScope)
{
Guard.ThrowIfNull(instrumentationScope);
this.InstrumentationScope = instrumentationScope;
}
/// <summary>
/// Gets the <see cref="OpenTelemetry.InstrumentationScope"/> associated
/// with the logger.
/// </summary>
public InstrumentationScope InstrumentationScope { get; }
/// <summary>
/// Emit a log.
/// </summary>
/// <param name="data"><see cref="LogRecordData"/>.</param>
/// <param name="attributes"><see cref="LogRecordAttributeList"/>.</param>
public abstract void EmitLog(
in LogRecordData data,
in LogRecordAttributeList attributes = default);
}

View File

@ -0,0 +1,57 @@
// <copyright file="LoggerProvider.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
namespace OpenTelemetry.Logs;
/// <summary>
/// LoggerProvider is the entry point of the OpenTelemetry API. It provides access to <see cref="Logger"/>.
/// </summary>
internal class LoggerProvider : BaseProvider
{
private static readonly NoopLogger NoopLogger = new();
/// <summary>
/// Initializes a new instance of the <see cref="LoggerProvider"/> class.
/// </summary>
protected LoggerProvider()
{
}
/// <summary>
/// Gets a logger.
/// </summary>
/// <returns><see cref="Logger"/> instance.</returns>
public Logger GetLogger()
=> this.GetLogger(new InstrumentationScope());
/// <summary>
/// Gets a logger with the given name.
/// </summary>
/// <param name="name">Optional name identifying the instrumentation library.</param>
/// <returns><see cref="Logger"/> instance.</returns>
public Logger GetLogger(string name)
=> this.GetLogger(new InstrumentationScope(name));
/// <summary>
/// Gets a logger with given instrumentation scope.
/// </summary>
/// <param name="instrumentationScope"><see cref="InstrumentationScope"/>.</param>
/// <returns><see cref="Logger"/>.</returns>
public virtual Logger GetLogger(InstrumentationScope instrumentationScope)
=> NoopLogger;
}

View File

@ -0,0 +1,42 @@
// <copyright file="LoggerProviderBuilder.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
namespace OpenTelemetry.Logs;
/// <summary>
/// LoggerProviderBuilder base class.
/// </summary>
internal abstract class LoggerProviderBuilder
{
/// <summary>
/// Initializes a new instance of the <see cref="LoggerProviderBuilder"/> class.
/// </summary>
protected LoggerProviderBuilder()
{
}
/// <summary>
/// Adds instrumentation to the provider.
/// </summary>
/// <typeparam name="TInstrumentation">Type of instrumentation class.</typeparam>
/// <param name="instrumentationFactory">Function that builds instrumentation.</param>
/// <returns>Returns <see cref="LoggerProviderBuilder"/> for chaining.</returns>
public abstract LoggerProviderBuilder AddInstrumentation<TInstrumentation>(
Func<TInstrumentation> instrumentationFactory)
where TInstrumentation : class;
}

View File

@ -0,0 +1,33 @@
// <copyright file="NoopLogger.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#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)
{
}
}

View File

@ -1,324 +0,0 @@
// <copyright file="LogRecordAttributeList.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Logs
{
/// <summary>
/// Stores attributes to be added to a log message.
/// </summary>
internal struct LogRecordAttributeList : IReadOnlyList<KeyValuePair<string, object?>>
{
internal const int OverflowAdditionalCapacity = 8;
internal List<KeyValuePair<string, object?>>? OverflowAttributes;
private KeyValuePair<string, object?> attribute1;
private KeyValuePair<string, object?> attribute2;
private KeyValuePair<string, object?> attribute3;
private KeyValuePair<string, object?> attribute4;
private KeyValuePair<string, object?> attribute5;
private KeyValuePair<string, object?> attribute6;
private KeyValuePair<string, object?> attribute7;
private KeyValuePair<string, object?> attribute8;
private int count;
/// <inheritdoc/>
public readonly int Count => this.count;
/// <inheritdoc/>
public KeyValuePair<string, object?> 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;
}
}
}
/// <summary>
/// Add an attribute.
/// </summary>
/// <param name="key">Attribute name.</param>
/// <returns>Attribute value.</returns>
[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<string, object?>(key, value));
}
/// <summary>
/// Create a <see cref="LogRecordAttributeList"/> collection from an enumerable.
/// </summary>
/// <param name="attributes">Source attributes.</param>
/// <returns><see cref="LogRecordAttributeList"/>.</returns>
public static LogRecordAttributeList CreateFromEnumerable(IEnumerable<KeyValuePair<string, object?>> attributes)
{
Guard.ThrowIfNull(attributes);
LogRecordAttributeList logRecordAttributes = default;
logRecordAttributes.OverflowAttributes = new(attributes);
logRecordAttributes.count = logRecordAttributes.OverflowAttributes.Count;
return logRecordAttributes;
}
/// <summary>
/// Add an attribute.
/// </summary>
/// <param name="key">Attribute name.</param>
/// <param name="value">Attribute value.</param>
public void Add(string key, object? value)
=> this.Add(new KeyValuePair<string, object?>(key, value));
/// <summary>
/// Add an attribute.
/// </summary>
/// <param name="attribute">Attribute.</param>
public void Add(KeyValuePair<string, object?> 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++;
}
/// <summary>
/// Returns an enumerator that iterates through the <see cref="LogRecordAttributeList"/>.
/// </summary>
/// <returns><see cref="Enumerator"/>.</returns>
public readonly Enumerator GetEnumerator()
=> new(in this);
/// <inheritdoc/>
readonly IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
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<KeyValuePair<string, object?>>(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 },
};
}
/// <summary>
/// Enumerates the elements of a <see cref="LogRecordAttributeList"/>.
/// </summary>
public struct Enumerator : IEnumerator<KeyValuePair<string, object?>>, IEnumerator
{
private LogRecordAttributeList attributes;
private int index;
internal Enumerator(in LogRecordAttributeList attributes)
{
this.index = -1;
this.attributes = attributes;
}
/// <inheritdoc/>
public readonly KeyValuePair<string, object?> Current
=> this.attributes[this.index];
/// <inheritdoc/>
readonly object IEnumerator.Current => this.Current;
/// <inheritdoc/>
public bool MoveNext()
{
this.index++;
return this.index < this.attributes.Count;
}
/// <inheritdoc/>
public readonly void Dispose()
{
}
/// <inheritdoc/>
readonly void IEnumerator.Reset()
=> throw new NotSupportedException();
}
}
}

View File

@ -1,125 +0,0 @@
// <copyright file="LogRecordData.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
#nullable enable
using System.Diagnostics;
namespace OpenTelemetry.Logs
{
/// <summary>
/// Stores details about a log message.
/// </summary>
internal struct LogRecordData
{
internal DateTime TimestampBacking = DateTime.UtcNow;
/// <summary>
/// Initializes a new instance of the <see cref="LogRecordData"/> struct.
/// </summary>
public LogRecordData()
: this(activity: null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="LogRecordData"/> struct.
/// </summary>
/// <remarks>
/// Note: The <see cref="Timestamp"/> property is initialized to <see
/// cref="DateTime.UtcNow"/> automatically.
/// </remarks>
/// <param name="activity">Optional <see cref="Activity"/> used to populate context fields.</param>
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;
}
}
/// <summary>
/// Gets or sets the log timestamp.
/// </summary>
/// <remarks>
/// Note: If <see cref="Timestamp"/> is set to a value with <see
/// cref="DateTimeKind.Local"/> it will be automatically converted to
/// UTC using <see cref="DateTime.ToUniversalTime"/>.
/// </remarks>
public DateTime Timestamp
{
readonly get => this.TimestampBacking;
set { this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; }
}
/// <summary>
/// Gets or sets the log <see cref="ActivityTraceId"/>.
/// </summary>
public ActivityTraceId TraceId { get; set; }
/// <summary>
/// Gets or sets the log <see cref="ActivitySpanId"/>.
/// </summary>
public ActivitySpanId SpanId { get; set; }
/// <summary>
/// Gets or sets the log <see cref="ActivityTraceFlags"/>.
/// </summary>
public ActivityTraceFlags TraceFlags { get; set; }
/* TODO: Add these when log api/sdk spec is stable.
/// <summary>
/// Gets or sets the original string representation of the severity as it is
/// known at the source.
/// </summary>
public string? SeverityText { get; set; } = null;
/// <summary>
/// Gets or sets the log severity.
/// </summary>
public LogRecordSeverity? Severity { get; set; } = null;
*/
/// <summary>
/// Gets or sets the log body.
/// </summary>
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;
}
}
}
}

View File

@ -125,9 +125,14 @@ namespace OpenTelemetry.Logs
/* 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<KeyValuePair<string, object?>> stateList)

View File

@ -0,0 +1,145 @@
// <copyright file="LogRecordAttributeListTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<string, object> 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<KeyValuePair<string, object>> 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<string, object> item in exportedAttributes)
{
Assert.Equal($"key{index}", item.Key);
Assert.Equal(index, (int)item.Value);
index++;
}
}
}

View File

@ -0,0 +1,139 @@
// <copyright file="LogRecordDataTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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);
}
}

View File

@ -1,102 +0,0 @@
// <copyright file="LogRecordAttributeListTests.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>
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<string, object> 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<string, object> item in logRecord.StateValues)
{
Assert.Equal($"key{index}", item.Key);
Assert.Equal(index, (int)item.Value);
index++;
}
}
}
}