From e1e7580437a430a333d88079745e2497af49c059 Mon Sep 17 00:00:00 2001 From: Bruce Irschick Date: Mon, 23 Feb 2026 14:12:31 -0800 Subject: [PATCH 1/3] feat(csharp/src/Telemetry): renaable compile-time JSON serializer context for trace activity --- .../Exporters/FileExporter/FileExporter.cs | 11 +- .../FileListener/ActivityProcessor.cs | 11 +- .../SerializableActivitySerializerContext.cs | 58 ++++++ .../FileListener/SerializableActivityTests.cs | 187 ++++++++++++++++++ 4 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs create mode 100644 csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs diff --git a/csharp/src/Telemetry/Traces/Exporters/FileExporter/FileExporter.cs b/csharp/src/Telemetry/Traces/Exporters/FileExporter/FileExporter.cs index 08d4336ab1..90c69abd9a 100644 --- a/csharp/src/Telemetry/Traces/Exporters/FileExporter/FileExporter.cs +++ b/csharp/src/Telemetry/Traces/Exporters/FileExporter/FileExporter.cs @@ -21,6 +21,7 @@ using System.IO; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -38,6 +39,12 @@ internal class FileExporter : BaseExporter private static readonly ConcurrentDictionary> s_fileExporters = new(); private static readonly byte[] s_newLine = Encoding.UTF8.GetBytes(Environment.NewLine); + private static readonly JsonSerializerOptions s_serializerOptions = new() + { + TypeInfoResolver = JsonTypeInfoResolver.Combine( + SerializableActivitySerializerContext.Default, + new DefaultJsonTypeInfoResolver()) + }; private readonly TracingFile _tracingFile; private readonly string _fileBaseName; @@ -151,7 +158,9 @@ private static async Task ProcessActivitiesAsync(FileExporter fileExporter, Canc SerializableActivity serializableActivity = new(activity); await JsonSerializer.SerializeAsync( stream, - serializableActivity, cancellationToken: cancellationToken).ConfigureAwait(false); + serializableActivity, + s_serializerOptions, + cancellationToken: cancellationToken).ConfigureAwait(false); stream.Write(s_newLine, 0, s_newLine.Length); stream.Position = 0; diff --git a/csharp/src/Telemetry/Traces/Listeners/FileListener/ActivityProcessor.cs b/csharp/src/Telemetry/Traces/Listeners/FileListener/ActivityProcessor.cs index 20fff58408..f34c79bfc5 100644 --- a/csharp/src/Telemetry/Traces/Listeners/FileListener/ActivityProcessor.cs +++ b/csharp/src/Telemetry/Traces/Listeners/FileListener/ActivityProcessor.cs @@ -20,6 +20,7 @@ using System.IO; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -29,6 +30,12 @@ namespace Apache.Arrow.Adbc.Telemetry.Traces.Listeners.FileListener internal sealed class ActivityProcessor : IDisposable { private static readonly byte[] s_newLine = Encoding.UTF8.GetBytes(Environment.NewLine); + private static readonly JsonSerializerOptions s_serializerOptions = new() + { + TypeInfoResolver = JsonTypeInfoResolver.Combine( + SerializableActivitySerializerContext.Default, + new DefaultJsonTypeInfoResolver()) + }; private Task? _processingTask; private readonly Channel _channel; private readonly Func _streamWriterFunc; @@ -91,7 +98,9 @@ private async Task ProcessActivitiesAsync(CancellationToken cancellationToken) SerializableActivity serializableActivity = new(activity); await JsonSerializer.SerializeAsync( stream, - serializableActivity, cancellationToken: cancellationToken).ConfigureAwait(false); + serializableActivity, + s_serializerOptions, + cancellationToken: cancellationToken).ConfigureAwait(false); stream.Write(s_newLine, 0, s_newLine.Length); stream.Position = 0; diff --git a/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs b/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs new file mode 100644 index 0000000000..d569a6f21d --- /dev/null +++ b/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.Text.Json.Serialization; + +namespace Apache.Arrow.Adbc.Telemetry.Traces.Listeners.FileListener +{ + [JsonSerializable(typeof(SerializableActivity))] + + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(byte))] + [JsonSerializable(typeof(sbyte))] + [JsonSerializable(typeof(ushort))] + [JsonSerializable(typeof(short))] + [JsonSerializable(typeof(uint))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(ulong))] + [JsonSerializable(typeof(long))] + [JsonSerializable(typeof(ulong))] + [JsonSerializable(typeof(float))] + [JsonSerializable(typeof(double))] + [JsonSerializable(typeof(decimal))] + [JsonSerializable(typeof(char))] + [JsonSerializable(typeof(bool))] + + [JsonSerializable(typeof(string[]))] + [JsonSerializable(typeof(byte[]))] + [JsonSerializable(typeof(sbyte[]))] + [JsonSerializable(typeof(ushort[]))] + [JsonSerializable(typeof(short[]))] + [JsonSerializable(typeof(uint[]))] + [JsonSerializable(typeof(int[]))] + [JsonSerializable(typeof(ulong[]))] + [JsonSerializable(typeof(long[]))] + [JsonSerializable(typeof(ulong[]))] + [JsonSerializable(typeof(float[]))] + [JsonSerializable(typeof(double[]))] + [JsonSerializable(typeof(decimal[]))] + [JsonSerializable(typeof(char[]))] + [JsonSerializable(typeof(bool[]))] + internal partial class SerializableActivitySerializerContext : JsonSerializerContext + { + } +} diff --git a/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs b/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs new file mode 100644 index 0000000000..8938c58c9c --- /dev/null +++ b/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using Apache.Arrow.Adbc.Telemetry.Traces.Listeners.FileListener; +using Xunit.Abstractions; + +namespace Apache.Arrow.Adbc.Tests.Telemetry.Traces.Listeners.FileListener +{ + public class SerializableActivityTests + { + private readonly ITestOutputHelper _output; + + public class SerializableActivityTestData : TheoryData + { + public SerializableActivityTestData() + { + var activityWithTags = new Activity("TestActivityWithTags"); + int index = 0; + + activityWithTags.AddTag("key" + index++, "value1"); + activityWithTags.AddTag("key" + index++, (sbyte)123); + activityWithTags.AddTag("key" + index++, (byte)123); + activityWithTags.AddTag("key" + index++, (short)123); + activityWithTags.AddTag("key" + index++, (ushort)123); + activityWithTags.AddTag("key" + index++, (int)123); + activityWithTags.AddTag("key" + index++, (uint)123); + activityWithTags.AddTag("key" + index++, (long)123); + activityWithTags.AddTag("key" + index++, (ulong)123); + activityWithTags.AddTag("key" + index++, (float)123); + activityWithTags.AddTag("key" + index++, (double)123); + activityWithTags.AddTag("key" + index++, (decimal)123); + activityWithTags.AddTag("key" + index++, true); + activityWithTags.AddTag("key" + index++, 'A'); + + activityWithTags.AddTag("key" + index++, new string[] { "val1" }); + activityWithTags.AddTag("key" + index++, new byte[] { 123 }); + activityWithTags.AddTag("key" + index++, new sbyte[] { 123 }); + activityWithTags.AddTag("key" + index++, new ushort[] { 123 }); + activityWithTags.AddTag("key" + index++, new short[] { 123 }); + activityWithTags.AddTag("key" + index++, new uint[] { 123 }); + activityWithTags.AddTag("key" + index++, new int[] { 123 }); + activityWithTags.AddTag("key" + index++, new ulong[] { 123 }); + activityWithTags.AddTag("key" + index++, new long[] { 123 }); + activityWithTags.AddTag("key" + index++, new float[] { 123 }); + activityWithTags.AddTag("key" + index++, new double[] { 123 }); + activityWithTags.AddTag("key" + index++, new decimal[] { 123 }); + activityWithTags.AddTag("key" + index++, new bool[] { true }); + activityWithTags.AddTag("key" + index++, new char[] { 'A' }); + + activityWithTags.AddTag("key" + index++, new string[] { "val1" }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new byte[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new sbyte[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new ushort[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new short[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new uint[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new int[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new ulong[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new long[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new float[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new double[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new decimal[] { 123 }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new bool[] { true }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new char[] { 'A' }.AsEnumerable()); + + Add(activityWithTags); + } + } + + public SerializableActivityTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public async Task CannnotSerializeAnonymousObjectWithSerializerContext() + { + Activity activity = new Activity("activity"); + using (activity.Start()) + { + activity.AddTag("key1", new { Field1 = "value1" }); + SerializableActivity serializableActivity = new(activity); + var stream = new MemoryStream(); + var serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IncludeFields = true, + TypeInfoResolver = SerializableActivitySerializerContext.Default, + }; + await Assert.ThrowsAnyAsync(async () => await JsonSerializer.SerializeAsync( + stream, + serializableActivity, + serializerOptions)); + } + } + + [Theory] + [ClassData(typeof(SerializableActivityTestData))] + public async Task CanSerializeWithNoDefaultTypeInfoResolver(Activity activity) + { + using (activity.Start()) + { + SerializableActivity serializableActivity = new(activity); + var stream = new MemoryStream(); + var serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IncludeFields = true, + TypeInfoResolver = SerializableActivitySerializerContext.Default, + }; + await JsonSerializer.SerializeAsync( + stream, + serializableActivity, + serializerOptions); + Assert.NotNull(stream); + _output.WriteLine("Serialized Activity: {0}", Encoding.UTF8.GetString(stream.ToArray())); + } + } + + [Theory] + [ClassData(typeof(SerializableActivityTestData))] + public async Task CanSerializeWithDefaultTypeInfoResolver(Activity activity) + { + using (activity.Start()) + { + SerializableActivity serializableActivity = new(activity); + var stream = new MemoryStream(); + var serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IncludeFields = true, + TypeInfoResolver = JsonTypeInfoResolver.Combine( + SerializableActivitySerializerContext.Default, + new DefaultJsonTypeInfoResolver()), + }; + await JsonSerializer.SerializeAsync( + stream, + serializableActivity, + serializerOptions); + Assert.NotNull(stream); + _output.WriteLine("Serialized Activity: {0}", Encoding.UTF8.GetString(stream.ToArray())); + } + } + + [Fact] + public async Task CanSerializeAnonymousObjectWithDefaultTypeInfoResolver() + { + Activity activity = new Activity("activity"); + using (activity.Start()) + { + activity.AddTag("key1", new { Field1 = "value1" }); + SerializableActivity serializableActivity = new(activity); + var stream = new MemoryStream(); + var serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + IncludeFields = true, + TypeInfoResolver = JsonTypeInfoResolver.Combine( + SerializableActivitySerializerContext.Default, + new DefaultJsonTypeInfoResolver()), + }; + await JsonSerializer.SerializeAsync( + stream, + serializableActivity, + serializerOptions); + _output.WriteLine("Serialized Activity: {0}", Encoding.UTF8.GetString(stream.ToArray())); + } + } + } +} From 1ac9f952397df4ac3fc8c09660c10d1c5f9da128 Mon Sep 17 00:00:00 2001 From: Bruce Irschick Date: Mon, 23 Feb 2026 15:51:46 -0800 Subject: [PATCH 2/3] add support for Uri type From 0854ec9f46c3d88bc36baede0e327d974844e755 Mon Sep 17 00:00:00 2001 From: Bruce Irschick Date: Mon, 23 Feb 2026 15:52:04 -0800 Subject: [PATCH 3/3] add support for Uri type --- .../FileListener/SerializableActivitySerializerContext.cs | 3 +++ .../Traces/Listeners/FileListener/SerializableActivityTests.cs | 1 + 2 files changed, 4 insertions(+) diff --git a/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs b/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs index d569a6f21d..7e503bb52e 100644 --- a/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs +++ b/csharp/src/Telemetry/Traces/Listeners/FileListener/SerializableActivitySerializerContext.cs @@ -15,6 +15,7 @@ * limitations under the License. */ +using System; using System.Text.Json.Serialization; namespace Apache.Arrow.Adbc.Telemetry.Traces.Listeners.FileListener @@ -52,6 +53,8 @@ namespace Apache.Arrow.Adbc.Telemetry.Traces.Listeners.FileListener [JsonSerializable(typeof(decimal[]))] [JsonSerializable(typeof(char[]))] [JsonSerializable(typeof(bool[]))] + + [JsonSerializable(typeof(Uri))] internal partial class SerializableActivitySerializerContext : JsonSerializerContext { } diff --git a/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs b/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs index 8938c58c9c..d8e03c6fa9 100644 --- a/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs +++ b/csharp/test/Telemetry/Traces/Listeners/FileListener/SerializableActivityTests.cs @@ -80,6 +80,7 @@ public SerializableActivityTestData() activityWithTags.AddTag("key" + index++, new bool[] { true }.AsEnumerable()); activityWithTags.AddTag("key" + index++, new char[] { 'A' }.AsEnumerable()); + activityWithTags.AddTag("key" + index++, new Uri("http://example.com")); Add(activityWithTags); } }