diff --git a/src/OpenTelemetry/Logs/LogEmitter.cs b/src/OpenTelemetry/Logs/LogEmitter.cs
new file mode 100644
index 00000000000..30bbd916ce2
--- /dev/null
+++ b/src/OpenTelemetry/Logs/LogEmitter.cs
@@ -0,0 +1,67 @@
+//
+// 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
+{
+ ///
+ /// LogEmitter implementation.
+ ///
+ ///
+ /// Spec reference: LogEmitter.
+ ///
+ internal sealed class LogEmitter
+ {
+ private readonly OpenTelemetryLoggerProvider loggerProvider;
+
+ internal LogEmitter(OpenTelemetryLoggerProvider loggerProvider)
+ {
+ Guard.ThrowIfNull(loggerProvider);
+
+ this.loggerProvider = loggerProvider;
+ }
+
+ ///
+ /// Emit a .
+ ///
+ /// .
+ /// .
+ public void Log(in LogRecordData data, in LogRecordAttributeList attributes = default)
+ {
+ var provider = this.loggerProvider;
+ var processor = provider.Processor;
+ if (processor != null)
+ {
+ var pool = provider.LogRecordPool;
+
+ var logRecord = pool.Rent();
+
+ logRecord.Data = data;
+
+ attributes.ApplyToLogRecord(logRecord);
+
+ processor.OnEnd(logRecord);
+
+ // Attempt to return the LogRecord to the pool. This will no-op
+ // if a batch exporter has added a reference.
+ pool.Return(logRecord);
+ }
+ }
+ }
+}
diff --git a/src/OpenTelemetry/Logs/LogRecordAttributeList.cs b/src/OpenTelemetry/Logs/LogRecordAttributeList.cs
new file mode 100644
index 00000000000..385ba8f108e
--- /dev/null
+++ b/src/OpenTelemetry/Logs/LogRecordAttributeList.cs
@@ -0,0 +1,326 @@
+//
+// 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;
+using System.Collections;
+using System.Collections.Generic;
+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
index c2411c74d52..538bf0a3666 100644
--- a/src/OpenTelemetry/Logs/LogRecordData.cs
+++ b/src/OpenTelemetry/Logs/LogRecordData.cs
@@ -29,6 +29,14 @@ 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.
///
@@ -37,7 +45,7 @@ internal struct LogRecordData
/// cref="DateTime.UtcNow"/> automatically.
///
/// Optional used to populate context fields.
- public LogRecordData(Activity? activity = null)
+ public LogRecordData(Activity? activity)
{
if (activity != null)
{
diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs
index 88b799434dd..91698d1123a 100644
--- a/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs
+++ b/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs
@@ -92,6 +92,13 @@ public bool IsEnabled(LogLevel logLevel)
private static IReadOnlyList> ParseState(LogRecord logRecord, TState state)
{
+ /* TODO: Enable this if/when LogRecordAttributeList becomes public.
+ if (state is LogRecordAttributeList logRecordAttributes)
+ {
+ logRecordAttributes.ApplyToLogRecord(logRecord);
+ return logRecord.AttributeStorage!;
+ }
+ else*/
if (state is IReadOnlyList> stateList)
{
return stateList;
diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs b/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs
index 221dbd11651..d94769b7f59 100644
--- a/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs
+++ b/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs
@@ -157,6 +157,12 @@ public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite)
return this.Processor?.ForceFlush(timeoutMilliseconds) ?? true;
}
+ ///
+ /// Create a .
+ ///
+ /// .
+ internal LogEmitter CreateEmitter() => new(this);
+
internal OpenTelemetryLoggerProvider AddProcessor(BaseProcessor processor)
{
Guard.ThrowIfNull(processor);
diff --git a/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs b/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs
new file mode 100644
index 00000000000..7b2b649dba5
--- /dev/null
+++ b/test/OpenTelemetry.Tests/Logs/LogEmitterTests.cs
@@ -0,0 +1,177 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Xunit;
+
+namespace OpenTelemetry.Logs.Tests
+{
+ public sealed class LogEmitterTests
+ {
+ [Fact]
+ public void LogEmitterBasicTest()
+ {
+ var exportedItems = new List();
+
+ using var provider = new OpenTelemetryLoggerProvider(options =>
+ {
+ options.AddInMemoryExporter(exportedItems);
+ });
+
+ var logEmitter = provider.CreateEmitter();
+
+ Exception ex = new InvalidOperationException();
+
+ logEmitter.Log(
+ new()
+ {
+ CategoryName = "LogEmitter",
+ Message = "Hello world",
+ LogLevel = LogLevel.Warning,
+ EventId = new EventId(18, "CustomEvent"),
+ Exception = ex,
+ },
+ new()
+ {
+ ["key1"] = "value1",
+ ["key2"] = "value2",
+ });
+
+ Assert.Single(exportedItems);
+
+ var logRecord = exportedItems[0];
+
+ Assert.NotNull(logRecord);
+ Assert.Equal("LogEmitter", logRecord.CategoryName);
+ Assert.Equal("Hello world", logRecord.FormattedMessage);
+ Assert.Equal(LogLevel.Warning, logRecord.LogLevel);
+ Assert.Equal(18, logRecord.EventId.Id);
+ Assert.Equal("CustomEvent", logRecord.EventId.Name);
+ Assert.Equal(ex, logRecord.Exception);
+ Assert.NotEqual(DateTime.MinValue, logRecord.Timestamp);
+
+ Assert.Equal(default, logRecord.TraceId);
+ Assert.Equal(default, logRecord.SpanId);
+ Assert.Equal(ActivityTraceFlags.None, logRecord.TraceFlags);
+ Assert.Null(logRecord.TraceState);
+
+ Assert.NotNull(logRecord.StateValues);
+ Assert.Equal(2, logRecord.StateValues.Count);
+ Assert.Contains(logRecord.StateValues, item => item.Key == "key1" && (string)item.Value == "value1");
+ Assert.Contains(logRecord.StateValues, item => item.Key == "key2" && (string)item.Value == "value2");
+ }
+
+ [Fact]
+ public void LogEmitterFromActivityTest()
+ {
+ var exportedItems = new List();
+
+ using var provider = new OpenTelemetryLoggerProvider(options =>
+ {
+ options.AddInMemoryExporter(exportedItems);
+ });
+
+ var logEmitter = provider.CreateEmitter();
+
+ using var activity = new Activity("Test");
+
+ activity.Start();
+
+ activity.ActivityTraceFlags = ActivityTraceFlags.Recorded;
+ activity.TraceStateString = "key1=value1";
+
+ logEmitter.Log(new(activity));
+
+ Assert.Single(exportedItems);
+
+ var logRecord = exportedItems[0];
+
+ Assert.NotNull(logRecord);
+
+ Assert.Equal(activity.TraceId, logRecord.TraceId);
+ Assert.Equal(activity.SpanId, logRecord.SpanId);
+ Assert.Equal(activity.ActivityTraceFlags, logRecord.TraceFlags);
+ Assert.Equal(activity.TraceStateString, logRecord.TraceState);
+
+ Assert.Null(logRecord.StateValues);
+ }
+
+ [Fact]
+ public void LogEmitterLocalToUtcTimestampTest()
+ {
+ var exportedItems = new List();
+
+ using var provider = new OpenTelemetryLoggerProvider(options =>
+ {
+ options.AddInMemoryExporter(exportedItems);
+ });
+
+ var logEmitter = provider.CreateEmitter();
+
+ DateTime timestamp = DateTime.SpecifyKind(
+ new DateTime(2022, 6, 30, 16, 0, 0),
+ DateTimeKind.Local);
+
+ logEmitter.Log(new()
+ {
+ Timestamp = timestamp,
+ });
+
+ Assert.Single(exportedItems);
+
+ var logRecord = exportedItems[0];
+
+ Assert.NotNull(logRecord);
+
+ Assert.Equal(timestamp.ToUniversalTime(), logRecord.Timestamp);
+ Assert.Equal(DateTimeKind.Utc, logRecord.Timestamp.Kind);
+ }
+
+ [Fact]
+ public void LogEmitterUnspecifiedTimestampTest()
+ {
+ var exportedItems = new List();
+
+ using var provider = new OpenTelemetryLoggerProvider(options =>
+ {
+ options.AddInMemoryExporter(exportedItems);
+ });
+
+ var logEmitter = provider.CreateEmitter();
+
+ DateTime timestamp = DateTime.SpecifyKind(
+ new DateTime(2022, 6, 30, 16, 0, 0),
+ DateTimeKind.Unspecified);
+
+ logEmitter.Log(new()
+ {
+ Timestamp = timestamp,
+ });
+
+ Assert.Single(exportedItems);
+
+ var logRecord = exportedItems[0];
+
+ Assert.NotNull(logRecord);
+
+ Assert.Equal(timestamp, logRecord.Timestamp);
+ Assert.Equal(DateTimeKind.Unspecified, logRecord.Timestamp.Kind);
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordAttributeListTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordAttributeListTests.cs
new file mode 100644
index 00000000000..d0e068c46b1
--- /dev/null
+++ b/test/OpenTelemetry.Tests/Logs/LogRecordAttributeListTests.cs
@@ -0,0 +1,103 @@
+//
+// 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.Collections.Generic;
+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++;
+ }
+ }
+ }
+}