Skip to content

Commit

Permalink
PassThru TableNameMappings using the logger category name. (#345)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yun-Ting authored May 24, 2022
1 parent 3603cbe commit d72b4cd
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 12 deletions.
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* PassThru TableNameMappings using the logger category name.
[345](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/345)

* Throw exception when `TableNameMappings` contains a `null` value.
[322](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/322)

Expand Down
16 changes: 16 additions & 0 deletions src/OpenTelemetry.Exporter.Geneva/GenevaBaseExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>

using System;
using System.Collections.Generic;

namespace OpenTelemetry.Exporter.Geneva;
Expand Down Expand Up @@ -89,4 +90,19 @@ internal static int AddPartAField(byte[] buffer, int cursor, string name, object
cursor = MessagePackSerializer.Serialize(buffer, cursor, value);
return cursor;
}

internal static int AddPartAField(byte[] buffer, int cursor, string name, Span<byte> value)
{
if (V40_PART_A_MAPPING.TryGetValue(name, out string replacementKey))
{
cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, replacementKey);
}
else
{
cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, name);
}

cursor = MessagePackSerializer.SerializeSpan(buffer, cursor, value);
return cursor;
}
}
124 changes: 112 additions & 12 deletions src/OpenTelemetry.Exporter.Geneva/GenevaLogExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespace OpenTelemetry.Exporter.Geneva;
public class GenevaLogExporter : GenevaBaseExporter<LogRecord>
{
private const int BUFFER_SIZE = 65360; // the maximum ETW payload (inclusive)
private const int MaxSanitizedEventNameLength = 50;

private readonly IReadOnlyDictionary<string, object> m_customFields;
private readonly string m_defaultEventName = "Log";
Expand All @@ -44,6 +45,7 @@ public class GenevaLogExporter : GenevaBaseExporter<LogRecord>
};

private readonly IDataTransport m_dataTransport;
private readonly bool shouldPassThruTableMappings;
private bool isDisposed;
private Func<object, string> convertToJson;

Expand All @@ -65,7 +67,14 @@ public GenevaLogExporter(GenevaExporterOptions options)

if (kv.Key == "*")
{
this.m_defaultEventName = kv.Value;
if (kv.Value == "*")
{
this.shouldPassThruTableMappings = true;
}
else
{
this.m_defaultEventName = kv.Value;
}
}
else
{
Expand Down Expand Up @@ -204,14 +213,6 @@ internal int SerializeLogRecord(LogRecord logRecord)
listKvp = logRecord.State as IReadOnlyList<KeyValuePair<string, object>>;
}

var name = logRecord.CategoryName;

// If user configured explicit TableName, use it.
if (this.m_tableMappings == null || !this.m_tableMappings.TryGetValue(name, out var eventName))
{
eventName = this.m_defaultEventName;
}

var buffer = m_buffer.Value;
if (buffer == null)
{
Expand Down Expand Up @@ -242,7 +243,43 @@ internal int SerializeLogRecord(LogRecord logRecord)
var timestamp = logRecord.Timestamp;
var cursor = 0;
cursor = MessagePackSerializer.WriteArrayHeader(buffer, cursor, 3);
cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, eventName);

var categoryName = logRecord.CategoryName;
string eventName = null;

Span<byte> sanitizedEventName = default;

// If user configured explicit TableName, use it.
if (this.m_tableMappings != null && this.m_tableMappings.TryGetValue(categoryName, out eventName))
{
cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, eventName);
}
else if (!this.shouldPassThruTableMappings)
{
eventName = this.m_defaultEventName;
cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, eventName);
}
else
{
int cursorStartIdx = cursor;

if (categoryName.Length > 0)
{
cursor = SerializeSanitizedCategoryName(buffer, cursor, categoryName);
}

if (cursor == cursorStartIdx)
{
// Serializing null as categoryName could not be sanitized into a valid string.
cursor = MessagePackSerializer.SerializeNull(buffer, cursor);
}
else
{
// Sanitized category name has been serialized.
sanitizedEventName = buffer.AsSpan().Slice(cursorStartIdx, cursor - cursorStartIdx);
}
}

cursor = MessagePackSerializer.WriteArrayHeader(buffer, cursor, 1);
cursor = MessagePackSerializer.WriteArrayHeader(buffer, cursor, 2);
cursor = MessagePackSerializer.SerializeUtcDateTime(buffer, cursor, timestamp);
Expand Down Expand Up @@ -282,7 +319,15 @@ internal int SerializeLogRecord(LogRecord logRecord)
}

// Part A - core envelope
cursor = AddPartAField(buffer, cursor, Schema.V40.PartA.Name, eventName);
if (sanitizedEventName.Length != 0)
{
cursor = AddPartAField(buffer, cursor, Schema.V40.PartA.Name, sanitizedEventName);
}
else
{
cursor = AddPartAField(buffer, cursor, Schema.V40.PartA.Name, eventName);
}

cntFields += 1;

cursor = AddPartAField(buffer, cursor, Schema.V40.PartA.Time, timestamp);
Expand Down Expand Up @@ -329,7 +374,7 @@ internal int SerializeLogRecord(LogRecord logRecord)
cntFields += 1;

cursor = MessagePackSerializer.SerializeAsciiString(buffer, cursor, "name");
cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, name);
cursor = MessagePackSerializer.SerializeUnicodeString(buffer, cursor, categoryName);
cntFields += 1;

bool hasEnvProperties = false;
Expand Down Expand Up @@ -443,5 +488,60 @@ private static byte GetSeverityNumber(LogLevel logLevel)
return 1;
}
}

// This method would map the logger category to a table name which only contains alphanumeric values with the following additions:
// Any character that is not allowed will be removed.
// If the resulting string is longer than 50 characters, only the first 50 characters will be taken.
// If the first character in the resulting string is a lower-case alphabet, it will be converted to the corresponding upper-case.
// If the resulting string still does not comply with Rule, the category name will not be serialized.
private static int SerializeSanitizedCategoryName(byte[] buffer, int cursor, string categoryName)
{
int cursorStartIdx = cursor;

// Reserve 2 bytes for storing LIMIT_MAX_STR8_LENGTH_IN_BYTES and (byte)validNameLength -
// these 2 bytes will be back filled after iterating through categoryName.
cursor += 2;
int validNameLength = 0;

// Special treatment for the first character.
var firstChar = categoryName[0];
if (firstChar >= 'A' && firstChar <= 'Z')
{
buffer[cursor++] = (byte)firstChar;
++validNameLength;
}
else if (firstChar >= 'a' && firstChar <= 'z')
{
// If the first character in the resulting string is a lower-case alphabet,
// it will be converted to the corresponding upper-case.
buffer[cursor++] = (byte)(firstChar - 32);
++validNameLength;
}
else
{
// Not a valid name.
return cursor -= 2;
}

for (int i = 1; i < categoryName.Length; ++i)
{
if (validNameLength == MaxSanitizedEventNameLength)
{
break;
}

var cur = categoryName[i];
if ((cur >= 'a' && cur <= 'z') || (cur >= 'A' && cur <= 'Z') || (cur >= '0' && cur <= '9'))
{
buffer[cursor++] = (byte)cur;
++validNameLength;
}
}

// Backfilling MessagePack serialization protocol and valid category length to the startIdx of the categoryName byte array.
MessagePackSerializer.WriteStr8Header(buffer, cursorStartIdx, validNameLength);

return cursor;
}
}
#endif
17 changes: 17 additions & 0 deletions src/OpenTelemetry.Exporter.Geneva/MessagePackSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ private static unsafe long Float64ToInt64(double value)
return *(long*)&value;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteStr8Header(byte[] buffer, int nameStartIdx, int validNameLength)
{
buffer[nameStartIdx] = STR8;
buffer[nameStartIdx + 1] = unchecked((byte)validNameLength);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SerializeAsciiString(byte[] buffer, int cursor, string value)
{
Expand Down Expand Up @@ -571,4 +578,14 @@ public static int Serialize(byte[] buffer, int cursor, object obj)
return SerializeUnicodeString(buffer, cursor, repr);
}
}

public static int SerializeSpan(byte[] buffer, int cursor, Span<byte> value)
{
for (int i = 0; i < value.Length; ++i)
{
buffer[cursor++] = value[i];
}

return cursor;
}
}
95 changes: 95 additions & 0 deletions test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,101 @@ public void TableNameMappingTest(params string[] category)
}
}

[Fact]
[Trait("Platform", "Any")]
public void PassThruTableMappingsWhenTheRuleIsEnabled()
{
var userInitializedCategoryToTableNameMappings = new Dictionary<string, string>
{
["Company.Store"] = "Store",
["Company.Orders"] = "Orders",
["*"] = "*",
};

var expectedCategoryToTableNameList = new List<KeyValuePair<string, string>>
{
// The category name must match "^[A-Z][a-zA-Z0-9]*$"; any character that is not allowed will be removed.
new KeyValuePair<string, string>("Company.Customer", "CompanyCustomer"),

new KeyValuePair<string, string>("Company-%-Customer*Region$##", "CompanyCustomerRegion"),

// If the first character in the resulting string is lower-case ALPHA,
// it will be converted to the corresponding upper-case.
new KeyValuePair<string, string>("company.Calendar", "CompanyCalendar"),

// After removing not allowed characters,
// if the resulting string is still an illegal event name, the data will get dropped on the floor.
new KeyValuePair<string, string>("$&-.$~!!", null),

new KeyValuePair<string, string>("dlmwl3bvd84bxsx8wf700nx9rydrrhfewbxf82ceoo0h8rpla4", "Dlmwl3bvd84bxsx8wf700nx9rydrrhfewbxf82ceoo0h8rpla4"),

// If the resulting string is longer than 50 characters, only the first 50 characters will be taken.
new KeyValuePair<string, string>("Company.Customer.rsLiheLClHJasBOvM.XI4uW7iop6ghvwBzahfs", "CompanyCustomerrsLiheLClHJasBOvMXI4uW7iop6ghvwBzah"),

// The data will be dropped on the floor as the exporter cannot deduce a valid table name.
new KeyValuePair<string, string>("1.2", null),
};

var logRecordList = new List<LogRecord>();
var exporterOptions = new GenevaExporterOptions
{
TableNameMappings = userInitializedCategoryToTableNameMappings,
ConnectionString = "EtwSession=OpenTelemetry",
};

using var loggerFactory = LoggerFactory.Create(builder => builder
.AddOpenTelemetry(options =>
{
options.AddInMemoryExporter(logRecordList);
})
.AddFilter("*", LogLevel.Trace)); // Enable all LogLevels

// Create a test exporter to get MessagePack byte data to validate if the data was serialized correctly.
using var exporter = new GenevaLogExporter(exporterOptions);

ILogger passThruTableMappingsLogger, userInitializedTableMappingsLogger;
ThreadLocal<byte[]> m_buffer;
object fluentdData;
string actualTableName;
m_buffer = typeof(GenevaLogExporter).GetField("m_buffer", BindingFlags.NonPublic | BindingFlags.Static).GetValue(exporter) as ThreadLocal<byte[]>;

// Verify that the category table mappings specified by the users in the Geneva Configuration are mapped correctly.
foreach (var mapping in userInitializedCategoryToTableNameMappings)
{
if (mapping.Key != "*")
{
userInitializedTableMappingsLogger = loggerFactory.CreateLogger(mapping.Key);
userInitializedTableMappingsLogger.LogInformation("This information does not matter.");
Assert.Single(logRecordList);

_ = exporter.SerializeLogRecord(logRecordList[0]);
fluentdData = MessagePack.MessagePackSerializer.Deserialize<object>(m_buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Instance);
actualTableName = (fluentdData as object[])[0] as string;
userInitializedCategoryToTableNameMappings.TryGetValue(mapping.Key, out var expectedTableNme);
Assert.Equal(expectedTableNme, actualTableName);

logRecordList.Clear();
}
}

// Verify that when the "*" = "*" were enabled, the correct table names were being deduced following the set of rules.
foreach (var mapping in expectedCategoryToTableNameList)
{
passThruTableMappingsLogger = loggerFactory.CreateLogger(mapping.Key);
passThruTableMappingsLogger.LogInformation("This information does not matter.");
Assert.Single(logRecordList);

_ = exporter.SerializeLogRecord(logRecordList[0]);
fluentdData = MessagePack.MessagePackSerializer.Deserialize<object>(m_buffer.Value, MessagePack.Resolvers.ContractlessStandardResolver.Instance);
actualTableName = (fluentdData as object[])[0] as string;
string expectedTableName = string.Empty;
expectedTableName = mapping.Value;
Assert.Equal(expectedTableName, actualTableName);

logRecordList.Clear();
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down

0 comments on commit d72b4cd

Please sign in to comment.