diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index b941bc96ac5..9f8349ef6c5 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -348,6 +348,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configurat src\Shared\Configuration\OpenTelemetryConfigurationExtensions.cs = src\Shared\Configuration\OpenTelemetryConfigurationExtensions.cs EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TagWriter", "TagWriter", "{993E65E5-E71B-40FD-871C-60A9EBD59724}" + ProjectSection(SolutionItems) = preProject + src\Shared\TagWriter\ArrayTagWriter.cs = src\Shared\TagWriter\ArrayTagWriter.cs + src\Shared\TagWriter\JsonStringArrayTagWriter.cs = src\Shared\TagWriter\JsonStringArrayTagWriter.cs + src\Shared\TagWriter\TagWriter.cs = src\Shared\TagWriter\TagWriter.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -694,6 +701,7 @@ Global {7BE494FC-4B0D-4340-A62A-9C9F3E7389FE} = {A115CE4C-71A8-4B95-96A5-C1DF46FD94C2} {19545B37-8518-4BDD-AD49-00C031FB3C2A} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} {87A20A76-D524-4AAC-AF92-8725BFED0415} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} + {993E65E5-E71B-40FD-871C-60A9EBD59724} = {A49299FB-C5CD-4E0E-B7E1-B7867BBD67CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/src/Shared/TagWriter/ArrayTagWriter.cs b/src/Shared/TagWriter/ArrayTagWriter.cs new file mode 100644 index 00000000000..b5eec4e2f0c --- /dev/null +++ b/src/Shared/TagWriter/ArrayTagWriter.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +namespace OpenTelemetry.Internal; + +internal abstract class ArrayTagWriter + where TArrayState : notnull +{ + public abstract TArrayState BeginWriteArray(); + + public abstract void WriteNullValue(ref TArrayState state); + + public abstract void WriteIntegralValue(ref TArrayState state, long value); + + public abstract void WriteFloatingPointValue(ref TArrayState state, double value); + + public abstract void WriteBooleanValue(ref TArrayState state, bool value); + + public abstract void WriteStringValue(ref TArrayState state, string value); + + public abstract void EndWriteArray(ref TArrayState state); +} diff --git a/src/Shared/TagWriter/JsonStringArrayTagWriter.cs b/src/Shared/TagWriter/JsonStringArrayTagWriter.cs new file mode 100644 index 00000000000..21addd5ba5b --- /dev/null +++ b/src/Shared/TagWriter/JsonStringArrayTagWriter.cs @@ -0,0 +1,99 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Diagnostics; +using System.Text.Json; + +namespace OpenTelemetry.Internal; + +internal abstract class JsonStringArrayTagWriter : TagWriter.JsonArrayTagWriterState> + where TTagState : notnull +{ + protected JsonStringArrayTagWriter() + : base(new JsonArrayTagWriter()) + { + } + + protected sealed override void WriteArrayTag(ref TTagState writer, string key, ref JsonArrayTagWriterState array) + { + var result = array.Stream.TryGetBuffer(out var buffer); + + Debug.Assert(result, "result was false"); + + this.WriteArrayTag(ref writer, key, buffer); + } + + protected abstract void WriteArrayTag(ref TTagState writer, string key, ArraySegment arrayUtf8JsonBytes); + + internal readonly struct JsonArrayTagWriterState(MemoryStream stream, Utf8JsonWriter writer) + { + public MemoryStream Stream { get; } = stream; + + public Utf8JsonWriter Writer { get; } = writer; + } + + internal sealed class JsonArrayTagWriter : ArrayTagWriter + { + [ThreadStatic] + private static MemoryStream? threadStream; + + [ThreadStatic] + private static Utf8JsonWriter? threadWriter; + + public override JsonArrayTagWriterState BeginWriteArray() + { + var state = EnsureWriter(); + state.Writer.WriteStartArray(); + return state; + } + + public override void EndWriteArray(ref JsonArrayTagWriterState state) + { + state.Writer.WriteEndArray(); + state.Writer.Flush(); + } + + public override void WriteBooleanValue(ref JsonArrayTagWriterState state, bool value) + { + state.Writer.WriteBooleanValue(value); + } + + public override void WriteFloatingPointValue(ref JsonArrayTagWriterState state, double value) + { + state.Writer.WriteNumberValue(value); + } + + public override void WriteIntegralValue(ref JsonArrayTagWriterState state, long value) + { + state.Writer.WriteNumberValue(value); + } + + public override void WriteNullValue(ref JsonArrayTagWriterState state) + { + state.Writer.WriteNullValue(); + } + + public override void WriteStringValue(ref JsonArrayTagWriterState state, string value) + { + state.Writer.WriteStringValue(value); + } + + private static JsonArrayTagWriterState EnsureWriter() + { + if (threadStream == null) + { + threadStream = new MemoryStream(); + threadWriter = new Utf8JsonWriter(threadStream); + return new(threadStream, threadWriter); + } + else + { + threadStream.SetLength(0); + threadWriter!.Reset(threadStream); + return new(threadStream, threadWriter); + } + } + } +} diff --git a/src/Shared/TagWriter/TagWriter.cs b/src/Shared/TagWriter/TagWriter.cs new file mode 100644 index 00000000000..14bccf236cf --- /dev/null +++ b/src/Shared/TagWriter/TagWriter.cs @@ -0,0 +1,298 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#nullable enable + +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace OpenTelemetry.Internal; + +internal abstract class TagWriter + where TTagState : notnull + where TArrayState : notnull +{ + private readonly ArrayTagWriter arrayWriter; + + protected TagWriter( + ArrayTagWriter arrayTagWriter) + { + Guard.ThrowIfNull(arrayTagWriter); + + this.arrayWriter = arrayTagWriter; + } + + public bool TryWriteTag( + ref TTagState state, + KeyValuePair tag, + int? tagValueMaxLength = null) + { + if (tag.Value == null) + { + return false; + } + + switch (tag.Value) + { + case char: + case string: + this.WriteStringTag(ref state, tag.Key, TruncateString(Convert.ToString(tag.Value)!, tagValueMaxLength)); + break; + case bool b: + this.WriteBooleanTag(ref state, tag.Key, b); + break; + case byte: + case sbyte: + case short: + case ushort: + case int: + case uint: + case long: + this.WriteIntegralTag(ref state, tag.Key, Convert.ToInt64(tag.Value)); + break; + case float: + case double: + this.WriteFloatingPointTag(ref state, tag.Key, Convert.ToDouble(tag.Value)); + break; + case Array array: + try + { + this.WriteArrayTagInternal(ref state, tag.Key, array, tagValueMaxLength); + } + catch + { + // If an exception is thrown when calling ToString + // on any element of the array, then the entire array value + // is ignored. + return this.LogUnsupportedTagTypeAndReturnFalse(tag.Key, tag.Value); + } + + break; + + // All other types are converted to strings including the following + // built-in value types: + // case nint: Pointer type. + // case nuint: Pointer type. + // case ulong: May throw an exception on overflow. + // case decimal: Converting to double produces rounding errors. + default: + try + { + var stringValue = TruncateString(Convert.ToString(tag.Value, CultureInfo.InvariantCulture), tagValueMaxLength); + if (stringValue == null) + { + return this.LogUnsupportedTagTypeAndReturnFalse(tag.Key, tag.Value); + } + + this.WriteStringTag(ref state, tag.Key, stringValue); + } + catch + { + // If ToString throws an exception then the tag is ignored. + return this.LogUnsupportedTagTypeAndReturnFalse(tag.Key, tag.Value); + } + + break; + } + + return true; + } + + protected abstract void WriteIntegralTag(ref TTagState state, string key, long value); + + protected abstract void WriteFloatingPointTag(ref TTagState state, string key, double value); + + protected abstract void WriteBooleanTag(ref TTagState state, string key, bool value); + + protected abstract void WriteStringTag(ref TTagState state, string key, string value); + + protected abstract void WriteArrayTag(ref TTagState state, string key, ref TArrayState value); + + protected abstract void OnUnsupportedTagDropped( + string tagKey, + string tagValueTypeFullName); + + [return: NotNullIfNotNull(nameof(value))] + private static string? TruncateString(string? value, int? maxLength) + { + return maxLength.HasValue && value?.Length > maxLength + ? value.Substring(0, maxLength.Value) + : value; + } + + private void WriteArrayTagInternal(ref TTagState state, string key, Array array, int? tagValueMaxLength) + { + var arrayState = this.arrayWriter.BeginWriteArray(); + + // This switch ensures the values of the resultant array-valued tag are of the same type. + switch (array) + { + case char[] charArray: this.WriteToArray(ref arrayState, charArray); break; + case string[]: this.ConvertToStringArrayThenWriteArrayTag(ref arrayState, array, tagValueMaxLength); break; + case bool[] boolArray: this.WriteToArray(ref arrayState, boolArray); break; + case byte[] byteArray: this.WriteToArray(ref arrayState, byteArray); break; + case sbyte[] sbyteArray: this.WriteToArray(ref arrayState, sbyteArray); break; + case short[] shortArray: this.WriteToArray(ref arrayState, shortArray); break; + case ushort[] ushortArray: this.WriteToArray(ref arrayState, ushortArray); break; + case uint[] uintArray: this.WriteToArray(ref arrayState, uintArray); break; +#if NETFRAMEWORK + case int[]: this.WriteArrayTagIntNetFramework(ref arrayState, array, tagValueMaxLength); break; + case long[]: this.WriteArrayTagLongNetFramework(ref arrayState, array, tagValueMaxLength); break; +#else + case int[] intArray: this.WriteToArray(ref arrayState, intArray); break; + case long[] longArray: this.WriteToArray(ref arrayState, longArray); break; +#endif + case float[] floatArray: this.WriteToArray(ref arrayState, floatArray); break; + case double[] doubleArray: this.WriteToArray(ref arrayState, doubleArray); break; + default: this.ConvertToStringArrayThenWriteArrayTag(ref arrayState, array, tagValueMaxLength); break; + } + + this.arrayWriter.EndWriteArray(ref arrayState); + + this.WriteArrayTag(ref state, key, ref arrayState); + } + +#if NETFRAMEWORK + private void WriteArrayTagIntNetFramework(ref TArrayState arrayState, Array array, int? tagValueMaxLength) + { + // Note: On .NET Framework x86 nint[] & nuint[] fall into int[] case + + var arrayType = array.GetType(); + if (arrayType == typeof(nint[]) + || arrayType == typeof(nuint[])) + { + this.ConvertToStringArrayThenWriteArrayTag(ref arrayState, array, tagValueMaxLength); + return; + } + + this.WriteToArray(ref arrayState, (int[])array); + } + + private void WriteArrayTagLongNetFramework(ref TArrayState arrayState, Array array, int? tagValueMaxLength) + { + // Note: On .NET Framework x64 nint[] & nuint[] fall into long[] case + + var arrayType = array.GetType(); + if (arrayType == typeof(nint[]) + || arrayType == typeof(nuint[])) + { + this.ConvertToStringArrayThenWriteArrayTag(ref arrayState, array, tagValueMaxLength); + return; + } + + this.WriteToArray(ref arrayState, (long[])array); + } +#endif + + private void ConvertToStringArrayThenWriteArrayTag(ref TArrayState arrayState, Array array, int? tagValueMaxLength) + { + if (array is string?[] arrayAsStringArray + && (!tagValueMaxLength.HasValue || !arrayAsStringArray.Any(s => s?.Length > tagValueMaxLength))) + { + this.WriteStringsToArray(ref arrayState, arrayAsStringArray); + } + else + { + string?[] stringArray = ArrayPool.Shared.Rent(array.Length); + try + { + for (var i = 0; i < array.Length; ++i) + { + var item = array.GetValue(i); + stringArray[i] = item == null + ? null + : TruncateString(Convert.ToString(item, CultureInfo.InvariantCulture), tagValueMaxLength); + } + + this.WriteStringsToArray(ref arrayState, new(stringArray, 0, array.Length)); + } + finally + { + ArrayPool.Shared.Return(stringArray); + } + } + } + + private void WriteToArray(ref TArrayState arrayState, TItem[] array) + where TItem : struct + { + foreach (TItem item in array) + { + if (typeof(TItem) == typeof(char)) + { + this.arrayWriter.WriteStringTag(ref arrayState, Convert.ToString((char)(object)item)!); + } + else if (typeof(TItem) == typeof(bool)) + { + this.arrayWriter.WriteBooleanTag(ref arrayState, (bool)(object)item); + } + else if (typeof(TItem) == typeof(byte)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (byte)(object)item); + } + else if (typeof(TItem) == typeof(sbyte)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (sbyte)(object)item); + } + else if (typeof(TItem) == typeof(short)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (short)(object)item); + } + else if (typeof(TItem) == typeof(ushort)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (ushort)(object)item); + } + else if (typeof(TItem) == typeof(int)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (int)(object)item); + } + else if (typeof(TItem) == typeof(uint)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (uint)(object)item); + } + else if (typeof(TItem) == typeof(long)) + { + this.arrayWriter.WriteIntegralTag(ref arrayState, (long)(object)item); + } + else if (typeof(TItem) == typeof(float)) + { + this.arrayWriter.WriteFloatingPointTag(ref arrayState, (float)(object)item); + } + else if (typeof(TItem) == typeof(double)) + { + this.arrayWriter.WriteFloatingPointTag(ref arrayState, (double)(object)item); + } + else + { + Debug.Fail("Unexpected type encountered"); + + throw new NotSupportedException(); + } + } + } + + private void WriteStringsToArray(ref TArrayState arrayState, ReadOnlySpan data) + { + foreach (var item in data) + { + if (item == null) + { + this.arrayWriter.WriteNullTag(ref arrayState); + } + else + { + this.arrayWriter.WriteStringTag(ref arrayState, item); + } + } + } + + private bool LogUnsupportedTagTypeAndReturnFalse(string key, object value) + { + Debug.Assert(value != null, "value was null"); + + this.OnUnsupportedTagDropped(key, value!.GetType().ToString()); + return false; + } +}