Skip to content

Commit 25685eb

Browse files
committed
Fix OpenTelemetryChatClient failing on unknown content types
It should fail gracefully by ignoring any content it can't output telemetry for rather than throwing.
1 parent 1eb963e commit 25685eb

File tree

2 files changed

+96
-5
lines changed

2 files changed

+96
-5
lines changed

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Text.Encodings.Web;
1111
using System.Text.Json;
1212
using System.Text.Json.Serialization;
13+
using System.Text.Json.Serialization.Metadata;
1314
using System.Threading;
1415
using System.Threading.Tasks;
1516
using Microsoft.Extensions.Logging;
@@ -216,7 +217,8 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
216217
}
217218
}
218219

219-
internal static string SerializeChatMessages(IEnumerable<ChatMessage> messages, ChatFinishReason? chatFinishReason = null)
220+
internal static string SerializeChatMessages(
221+
IEnumerable<ChatMessage> messages, ChatFinishReason? chatFinishReason = null, JsonSerializerOptions? customContentSerializerOptions = null)
220222
{
221223
List<object> output = [];
222224

@@ -293,10 +295,28 @@ internal static string SerializeChatMessages(IEnumerable<ChatMessage> messages,
293295
break;
294296

295297
default:
298+
JsonElement element = _emptyObject;
299+
try
300+
{
301+
JsonTypeInfo? unknownContentTypeInfo =
302+
customContentSerializerOptions?.TryGetTypeInfo(content.GetType(), out JsonTypeInfo? ctsi) is true ? ctsi :
303+
_defaultOptions.TryGetTypeInfo(content.GetType(), out JsonTypeInfo? dtsi) ? dtsi :
304+
null;
305+
306+
if (unknownContentTypeInfo is not null)
307+
{
308+
element = JsonSerializer.SerializeToElement(content, unknownContentTypeInfo);
309+
}
310+
}
311+
catch
312+
{
313+
// Ignore the contents of any parts that can't be serialized.
314+
}
315+
296316
m.Parts.Add(new OtelGenericPart
297317
{
298318
Type = content.GetType().FullName!,
299-
Content = content,
319+
Content = element,
300320
});
301321
break;
302322
}
@@ -558,7 +578,7 @@ private void AddInputMessagesTags(IEnumerable<ChatMessage> messages, ChatOptions
558578

559579
_ = activity.AddTag(
560580
OpenTelemetryConsts.GenAI.Input.Messages,
561-
SerializeChatMessages(messages));
581+
SerializeChatMessages(messages, customContentSerializerOptions: _jsonSerializerOptions));
562582
}
563583
}
564584

@@ -568,7 +588,7 @@ private void AddOutputMessagesTags(ChatResponse response, Activity? activity)
568588
{
569589
_ = activity.AddTag(
570590
OpenTelemetryConsts.GenAI.Output.Messages,
571-
SerializeChatMessages(response.Messages, response.FinishReason));
591+
SerializeChatMessages(response.Messages, response.FinishReason, customContentSerializerOptions: _jsonSerializerOptions));
572592
}
573593
}
574594

@@ -609,6 +629,7 @@ private sealed class OtelFunction
609629
}
610630

611631
private static readonly JsonSerializerOptions _defaultOptions = CreateDefaultOptions();
632+
private static readonly JsonElement _emptyObject = JsonSerializer.SerializeToElement(new object(), _defaultOptions.GetTypeInfo(typeof(object)));
612633

613634
private static JsonSerializerOptions CreateDefaultOptions()
614635
{

test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/OpenTelemetryChatClientTests.cs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,77 @@ async static IAsyncEnumerable<ChatResponseUpdate> CallbackAsync(
329329
Assert.False(tags.ContainsKey("gen_ai.system_instructions"));
330330
Assert.False(tags.ContainsKey("gen_ai.tool.definitions"));
331331
}
332+
}
333+
334+
[Fact]
335+
public async Task UnknownContentTypes_Ignored()
336+
{
337+
var sourceName = Guid.NewGuid().ToString();
338+
var activities = new List<Activity>();
339+
using var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
340+
.AddSource(sourceName)
341+
.AddInMemoryExporter(activities)
342+
.Build();
343+
344+
using var innerClient = new TestChatClient
345+
{
346+
GetResponseAsyncCallback = async (messages, options, cancellationToken) =>
347+
{
348+
await Task.Yield();
349+
return new ChatResponse(new ChatMessage(ChatRole.Assistant, "The blue whale, I think."));
350+
},
351+
};
352+
353+
using var chatClient = innerClient
354+
.AsBuilder()
355+
.UseOpenTelemetry(null, sourceName, configure: instance =>
356+
{
357+
instance.EnableSensitiveData = true;
358+
instance.JsonSerializerOptions = TestJsonSerializerContext.Default.Options;
359+
})
360+
.Build();
332361

333-
static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim();
362+
List<ChatMessage> messages =
363+
[
364+
new(ChatRole.User,
365+
[
366+
new TextContent("Hello!"),
367+
new NonSerializableAIContent(),
368+
new TextContent("How are you?"),
369+
]),
370+
];
371+
372+
var response = await chatClient.GetResponseAsync(messages);
373+
Assert.NotNull(response);
374+
375+
var activity = Assert.Single(activities);
376+
Assert.NotNull(activity);
377+
378+
var inputMessages = activity.Tags.First(kvp => kvp.Key == "gen_ai.input.messages").Value;
379+
Assert.Equal(ReplaceWhitespace("""
380+
[
381+
{
382+
"role": "user",
383+
"parts": [
384+
{
385+
"type": "text",
386+
"content": "Hello!"
387+
},
388+
{
389+
"type": "Microsoft.Extensions.AI.OpenTelemetryChatClientTests+NonSerializableAIContent",
390+
"content": {}
391+
},
392+
{
393+
"type": "text",
394+
"content": "How are you?"
395+
}
396+
]
397+
}
398+
]
399+
"""), ReplaceWhitespace(inputMessages));
334400
}
401+
402+
private sealed class NonSerializableAIContent : AIContent;
403+
404+
private static string ReplaceWhitespace(string? input) => Regex.Replace(input ?? "", @"\s+", " ").Trim();
335405
}

0 commit comments

Comments
 (0)