// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; /// A delegating chat client that logs chat operations to an . /// /// /// The provided implementation of is thread-safe for concurrent use so long as the /// employed is also thread-safe for concurrent use. /// /// /// When the employed enables , the contents of /// chat messages and options are logged. These messages and options may contain sensitive application data. /// is disabled by default and should never be enabled in a production environment. /// Messages and options are not logged at other logging levels. /// /// public partial class LoggingChatClient : DelegatingChatClient { /// An instance used for all logging. private readonly ILogger _logger; /// The to use for serialization of state written to the logger. private JsonSerializerOptions _jsonSerializerOptions; /// Initializes a new instance of the class. /// The underlying . /// An instance that will be used for all logging. public LoggingChatClient(IChatClient innerClient, ILogger logger) : base(innerClient) { _logger = Throw.IfNull(logger); _jsonSerializerOptions = AIJsonUtilities.DefaultOptions; } /// Gets or sets JSON serialization options to use when serializing logging data. public JsonSerializerOptions JsonSerializerOptions { get => _jsonSerializerOptions; set => _jsonSerializerOptions = Throw.IfNull(value); } /// public override async Task GetResponseAsync( IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { LogInvokedSensitive(nameof(GetResponseAsync), AsJson(messages), AsJson(options), AsJson(this.GetService())); } else { LogInvoked(nameof(GetResponseAsync)); } } try { var response = await base.GetResponseAsync(messages, options, cancellationToken); if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { LogCompletedSensitive(nameof(GetResponseAsync), AsJson(response)); } else { LogCompleted(nameof(GetResponseAsync)); } } return response; } catch (OperationCanceledException) { LogInvocationCanceled(nameof(GetResponseAsync)); throw; } catch (Exception ex) { LogInvocationFailed(nameof(GetResponseAsync), ex); throw; } } /// public override async IAsyncEnumerable GetStreamingResponseAsync( IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) { if (_logger.IsEnabled(LogLevel.Trace)) { LogInvokedSensitive(nameof(GetStreamingResponseAsync), AsJson(messages), AsJson(options), AsJson(this.GetService())); } else { LogInvoked(nameof(GetStreamingResponseAsync)); } } IAsyncEnumerator e; try { e = base.GetStreamingResponseAsync(messages, options, cancellationToken).GetAsyncEnumerator(cancellationToken); } catch (OperationCanceledException) { LogInvocationCanceled(nameof(GetStreamingResponseAsync)); throw; } catch (Exception ex) { LogInvocationFailed(nameof(GetStreamingResponseAsync), ex); throw; } try { ChatResponseUpdate? update = null; while (true) { try { if (!await e.MoveNextAsync()) { break; } update = e.Current; } catch (OperationCanceledException) { LogInvocationCanceled(nameof(GetStreamingResponseAsync)); throw; } catch (Exception ex) { LogInvocationFailed(nameof(GetStreamingResponseAsync), ex); throw; } if (_logger.IsEnabled(LogLevel.Trace)) { LogStreamingUpdateSensitive(AsJson(update)); } yield return update; } LogCompleted(nameof(GetStreamingResponseAsync)); } finally { await e.DisposeAsync(); } } private string AsJson(T value) => TelemetryHelpers.AsJson(value, _jsonSerializerOptions); [LoggerMessage(LogLevel.Debug, "{MethodName} invoked.")] private partial void LogInvoked(string methodName); [LoggerMessage(LogLevel.Trace, "{MethodName} invoked: {Messages}. Options: {ChatOptions}. Metadata: {ChatClientMetadata}.")] private partial void LogInvokedSensitive(string methodName, string messages, string chatOptions, string chatClientMetadata); [LoggerMessage(LogLevel.Debug, "{MethodName} completed.")] private partial void LogCompleted(string methodName); [LoggerMessage(LogLevel.Trace, "{MethodName} completed: {ChatResponse}.")] private partial void LogCompletedSensitive(string methodName, string chatResponse); [LoggerMessage(LogLevel.Trace, "GetStreamingResponseAsync received update: {ChatResponseUpdate}")] private partial void LogStreamingUpdateSensitive(string chatResponseUpdate); [LoggerMessage(LogLevel.Debug, "{MethodName} canceled.")] private partial void LogInvocationCanceled(string methodName); [LoggerMessage(LogLevel.Error, "{MethodName} failed.")] private partial void LogInvocationFailed(string methodName, Exception error); }