Skip to content

Commit 4e71161

Browse files
authored
Fix OpenTelemetryChatClient failing on unknown content types (#6915)
It should fail gracefully by ignoring any content it can't output telemetry for rather than throwing.
1 parent aead2b6 commit 4e71161

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)