Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OneCollectorExporter] Extension feature improvements #1327

Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
`{OriginalFormat}` key or `LogRecord.Body`).
([#1321](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1321))

* Expanded the common schema extension feature to allow consumers to control
entire elements (e.g. `ext.metadata`) instead of just fields (e.g.
`ext.dt.traceId`).
([#1327](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1327))

## 1.5.1

Released 2023-Aug-07
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <copyright file="ExtensionFieldInformation.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.Text.Json;

namespace OpenTelemetry.Exporter.OneCollector;

internal sealed class ExtensionFieldInformation
{
public string? ExtensionName;
public JsonEncodedText EncodedExtensionName;
public string? FieldName;
public JsonEncodedText EncodedFieldName;

public bool IsValid => this.ExtensionName != null;
CodeBlanch marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,89 +16,247 @@

using System.Collections;
using System.Diagnostics;
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Text.Json;

namespace OpenTelemetry.Exporter.OneCollector;

internal sealed class ExtensionFieldInformationManager
{
public const int MaxNumberOfCachedFieldInformations = 2048;
[ThreadStatic]
private static ExtensionFieldInformationCacheKey? threadCacheKey;
private readonly Hashtable fieldInformationCache = new(16, StringComparer.OrdinalIgnoreCase);

public static ExtensionFieldInformationManager SharedCache { get; } = new();

public int CountOfCachedExtensionFields => this.fieldInformationCache.Count;

public bool TryResolveExtensionFieldInformation(string fullFieldName, out (string ExtensionName, string FieldName) resolvedFieldInformation)
public bool TryResolveExtensionFieldInformation(
string fullFieldName,
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
[NotNullWhen(true)]
#endif
out ExtensionFieldInformation? resolvedFieldInformation)
{
if (this.fieldInformationCache[fullFieldName] is not FieldInformation fieldInformation)
{
fieldInformation = this.ResolveExtensionFieldInformationRare(fullFieldName);
}
var cacheKey = BuildCacheKey(
fullFieldName,
extensionName: null,
fieldName: null);

if (!fieldInformation.IsValid)
{
resolvedFieldInformation = default;
return false;
}
return this.TryResolveExtensionFieldInformation(cacheKey, out resolvedFieldInformation);
}

resolvedFieldInformation = new(fieldInformation.ExtensionName!, fieldInformation.FieldName!);
return true;
public bool TryResolveExtensionFieldInformation(
string extensionName,
string fieldName,
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
[NotNullWhen(true)]
#endif
out ExtensionFieldInformation? resolvedFieldInformation)
{
var cacheKey = BuildCacheKey(
fullFieldName: null,
extensionName,
fieldName);

return this.TryResolveExtensionFieldInformation(cacheKey, out resolvedFieldInformation);
}

private static FieldInformation BuildFieldInformation(string fullFieldName)
private static ExtensionFieldInformationCacheKey BuildCacheKey(
string? fullFieldName,
string? extensionName,
string? fieldName)
{
Debug.Assert(fullFieldName.Length >= 4, "fullFieldName length was invalid");
Debug.Assert(fullFieldName.StartsWith("ext.", StringComparison.OrdinalIgnoreCase), "fullFieldName did not start with 'ext.'");
var cacheKey = threadCacheKey ??= new();
cacheKey.FullFieldName = fullFieldName;
cacheKey.ExtensionName = extensionName;
cacheKey.FieldName = fieldName;
return cacheKey;
}

var extensionName = fullFieldName.AsSpan().Slice(4);
var locationOfDot = extensionName.IndexOf('.');
if (locationOfDot <= 0)
private bool TryResolveExtensionFieldInformation(
ExtensionFieldInformationCacheKey cacheKey,
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
[NotNullWhen(true)]
#endif
out ExtensionFieldInformation? resolvedFieldInformation)
{
if (this.fieldInformationCache[cacheKey] is not ExtensionFieldInformation fieldInformation)
{
return new();
fieldInformation = this.ResolveExtensionFieldInformationRare(cacheKey);
}

var fieldName = extensionName.Slice(locationOfDot + 1);
if (fieldName.Length <= 0)
if (!fieldInformation.IsValid)
{
return new();
resolvedFieldInformation = default;
return false;
}

extensionName = extensionName.Slice(0, locationOfDot);

return new FieldInformation
{
ExtensionName = extensionName.ToString(),
FieldName = fieldName.ToString(),
IsValid = true,
};
resolvedFieldInformation = fieldInformation;
return true;
}

private FieldInformation ResolveExtensionFieldInformationRare(string fullFieldName)
private ExtensionFieldInformation ResolveExtensionFieldInformationRare(ExtensionFieldInformationCacheKey cacheKey)
{
if (this.fieldInformationCache.Count >= MaxNumberOfCachedFieldInformations)
{
return BuildFieldInformation(fullFieldName);
return cacheKey.ToFieldInformation();
}

lock (this.fieldInformationCache)
{
if (this.fieldInformationCache[fullFieldName] is not FieldInformation fieldInformation)
if (this.fieldInformationCache[cacheKey] is not ExtensionFieldInformation fieldInformation)
{
fieldInformation = BuildFieldInformation(fullFieldName);
fieldInformation = cacheKey.ToFieldInformation();
if (this.fieldInformationCache.Count < MaxNumberOfCachedFieldInformations)
{
this.fieldInformationCache[fullFieldName] = fieldInformation;
// Note: We make a copy of the [ThreadStatic] key here so it
// remains immutable in the hastable.
this.fieldInformationCache[cacheKey.Clone()] = fieldInformation;
}
}

return fieldInformation;
}
}

private sealed class FieldInformation
internal sealed class ExtensionFieldInformationCacheKey : IEquatable<ExtensionFieldInformationCacheKey>
{
public string? FullFieldName;

public string? ExtensionName;

public string? FieldName;
public bool IsValid;

public static bool operator ==(ExtensionFieldInformationCacheKey? left, ExtensionFieldInformationCacheKey? right)
{
if (left is null)
{
return right is null;
}

return left.Equals(right);
}

public static bool operator !=(ExtensionFieldInformationCacheKey? left, ExtensionFieldInformationCacheKey? right)
=> !(left == right);

public ExtensionFieldInformationCacheKey Clone()
{
return new()
{
FullFieldName = this.FullFieldName,
ExtensionName = this.ExtensionName,
FieldName = this.FieldName,
};
}

public override int GetHashCode()
{
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
HashCode hash = default;
hash.Add(this.FullFieldName, StringComparer.OrdinalIgnoreCase);
hash.Add(this.ExtensionName, StringComparer.OrdinalIgnoreCase);
hash.Add(this.FieldName, StringComparer.OrdinalIgnoreCase);
return hash.ToHashCode();
#else
var hash = 17;
unchecked
{
hash = (hash * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(this.FullFieldName ?? string.Empty);
hash = (hash * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(this.ExtensionName ?? string.Empty);
hash = (hash * 31) + StringComparer.OrdinalIgnoreCase.GetHashCode(this.FieldName ?? string.Empty);
}

return hash;
#endif
}

public bool Equals(ExtensionFieldInformationCacheKey? other)
{
if (other is null)
{
return false;
}

return string.Equals(this.FullFieldName, other.FullFieldName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(this.ExtensionName, other.ExtensionName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(this.FieldName, other.FieldName, StringComparison.OrdinalIgnoreCase);
}

public override bool Equals(object? obj)
=> this.Equals(obj as ExtensionFieldInformationCacheKey);

public ExtensionFieldInformation ToFieldInformation()
{
if (this.FullFieldName != null)
{
return BuildFieldInformationFromFullFieldName(this.FullFieldName);
}
else
{
Debug.Assert(this.ExtensionName != null, "ExtensionName was null");
Debug.Assert(this.FieldName != null, "FieldName was null");

var fieldName = this.FieldName!.Trim();
if (fieldName.Length <= 0)
{
return new();
}

return new ExtensionFieldInformation
{
ExtensionName = this.ExtensionName,
EncodedExtensionName = JsonEncodedText.Encode(this.ExtensionName!),
FieldName = fieldName,
EncodedFieldName = JsonEncodedText.Encode(fieldName),
};
}
}

private static ExtensionFieldInformation BuildFieldInformationFromFullFieldName(string fullFieldName)
{
Debug.Assert(fullFieldName.Length >= 4, "fullFieldName length was invalid");
Debug.Assert(fullFieldName.StartsWith("ext.", StringComparison.OrdinalIgnoreCase), "fullFieldName did not start with 'ext.'");

var extensionName = fullFieldName.AsSpan().Slice(4).Trim();
var locationOfDot = extensionName.IndexOf('.');
if (locationOfDot < 0)
{
if (extensionName.Length <= 0)
{
return new();
}

return new ExtensionFieldInformation
{
ExtensionName = extensionName.ToString(),
EncodedExtensionName = JsonEncodedText.Encode(extensionName),
};
}

var fieldName = extensionName.Slice(locationOfDot + 1).Trim();
if (fieldName.Length <= 0)
{
return new();
}

extensionName = extensionName.Slice(0, locationOfDot).Trim();
if (extensionName.Length <= 0)
{
return new();
}

return new ExtensionFieldInformation
{
ExtensionName = extensionName.ToString(),
EncodedExtensionName = JsonEncodedText.Encode(extensionName),
FieldName = fieldName.ToString(),
EncodedFieldName = JsonEncodedText.Encode(fieldName),
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ public static void SerializeValueToJson(object? value, Utf8JsonWriter writer)
SerializeArrayValueToJson(v, writer);
return;

case IReadOnlyList<KeyValuePair<string, object?>> v:
SerializeMapValueToJson(v, writer);
return;

case IEnumerable<KeyValuePair<string, object?>> v:
SerializeMapValueToJson(v, writer);
return;
Expand All @@ -180,6 +184,25 @@ private static void SerializeArrayValueToJson(Array value, Utf8JsonWriter writer
writer.WriteEndArray();
}

private static void SerializeMapValueToJson(IReadOnlyList<KeyValuePair<string, object?>> value, Utf8JsonWriter writer)
{
writer.WriteStartObject();

for (int i = 0; i < value.Count; i++)
{
var element = value[i];

if (string.IsNullOrEmpty(element.Key))
{
continue;
}

SerializeKeyValueToJson(element.Key, element.Value, writer);
}

writer.WriteEndObject();
}

private static void SerializeMapValueToJson(IEnumerable<KeyValuePair<string, object?>> value, Utf8JsonWriter writer)
{
writer.WriteStartObject();
Expand Down
Loading