Skip to content

Commit ef255fe

Browse files
Make OpenAITelemetryPlugin record report metrics only if 'recording' is on (#1450)
* Use the empty event args instance * Add AfterRecordingStartAsync handler/event * Add tracking flag of the recording start * Collect model usage info if the recording is on * Remove redundant initialization * Revert all related new AfterRecordingStartAsync handler * Add param null check * Remove modelUsage storage to collect data afterward * Add modelUsage collection from RequestLogs after recording is stopped * Removes unused object --------- Co-authored-by: Waldek Mastykarz <[email protected]>
1 parent 110ac9b commit ef255fe

File tree

2 files changed

+99
-16
lines changed

2 files changed

+99
-16
lines changed

DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
using Microsoft.Extensions.Configuration;
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Logging.Abstractions;
1415
using OpenTelemetry;
1516
using OpenTelemetry.Exporter;
1617
using OpenTelemetry.Metrics;
1718
using OpenTelemetry.Resources;
1819
using OpenTelemetry.Trace;
19-
using System.Collections.Concurrent;
2020
using System.Diagnostics;
2121
using System.Diagnostics.Metrics;
2222
using System.Text.Json;
@@ -67,7 +67,6 @@ public sealed class OpenAITelemetryPlugin(
6767
private LanguageModelPricesLoader? _loader;
6868
private MeterProvider? _meterProvider;
6969
private TracerProvider? _tracerProvider;
70-
private readonly ConcurrentDictionary<string, List<OpenAITelemetryPluginReportModelUsageInformation>> _modelUsage = [];
7170

7271
public override string Name => nameof(OpenAITelemetryPlugin);
7372

@@ -196,17 +195,18 @@ public override Task AfterRecordingStopAsync(RecordingArgs e, CancellationToken
196195
{
197196
Logger.LogTrace("{Method} called", nameof(AfterRecordingStopAsync));
198197

198+
ArgumentNullException.ThrowIfNull(e);
199+
199200
var report = new OpenAITelemetryPluginReport
200201
{
201202
Application = Configuration.Application,
202203
Environment = Configuration.Environment,
203204
Currency = Configuration.Currency,
204205
IncludeCosts = Configuration.IncludeCosts,
205-
ModelUsage = _modelUsage.ToDictionary()
206+
ModelUsage = GetOpenAIModelUsage(e.RequestLogs)
206207
};
207208

208209
StoreReport(report, e);
209-
_modelUsage.Clear();
210210

211211
Logger.LogTrace("Left {Name}", nameof(AfterRecordingStopAsync));
212212
return Task.CompletedTask;
@@ -849,16 +849,6 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
849849
.SetTag(SemanticConvention.GEN_AI_USAGE_OUTPUT_TOKENS, usage.CompletionTokens)
850850
.SetTag(SemanticConvention.GEN_AI_USAGE_TOTAL_TOKENS, usage.TotalTokens);
851851

852-
var reportModelUsageInformation = new OpenAITelemetryPluginReportModelUsageInformation
853-
{
854-
Model = response.Model,
855-
PromptTokens = usage.PromptTokens,
856-
CompletionTokens = usage.CompletionTokens,
857-
CachedTokens = usage.PromptTokensDetails?.CachedTokens ?? 0L
858-
};
859-
var usagePerModel = _modelUsage.GetOrAdd(response.Model, model => []);
860-
usagePerModel.Add(reportModelUsageInformation);
861-
862852
if (!Configuration.IncludeCosts || Configuration.Prices is null)
863853
{
864854
Logger.LogDebug("Cost tracking is disabled or prices data is not available");
@@ -895,7 +885,6 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
895885
new(SemanticConvention.GEN_AI_REQUEST_MODEL, request.Model),
896886
new(SemanticConvention.GEN_AI_RESPONSE_MODEL, response.Model)
897887
]);
898-
reportModelUsageInformation.Cost = totalCost;
899888
}
900889
else
901890
{
@@ -905,6 +894,100 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
905894
Logger.LogTrace("RecordUsageMetrics() finished");
906895
}
907896

897+
private Dictionary<string, List<OpenAITelemetryPluginReportModelUsageInformation>> GetOpenAIModelUsage(IEnumerable<RequestLog> requestLogs)
898+
{
899+
var modelUsage = new Dictionary<string, List<OpenAITelemetryPluginReportModelUsageInformation>>();
900+
var openAIRequestLogs = requestLogs.Where(r =>
901+
r is not null &&
902+
r.Context is not null &&
903+
r.Context.Session is not null &&
904+
r.MessageType == MessageType.InterceptedResponse &&
905+
string.Equals("POST", r.Context.Session.HttpClient.Request.Method, StringComparison.OrdinalIgnoreCase) &&
906+
r.Context.Session.HttpClient.Response.StatusCode >= 200 &&
907+
r.Context.Session.HttpClient.Response.StatusCode < 300 &&
908+
r.Context.Session.HttpClient.Response.HasBody &&
909+
!string.IsNullOrEmpty(r.Context.Session.HttpClient.Response.BodyString) &&
910+
ProxyUtils.MatchesUrlToWatch(UrlsToWatch, r.Context.Session.HttpClient.Request.RequestUri.AbsoluteUri) &&
911+
OpenAIRequest.TryGetOpenAIRequest(r.Context.Session.HttpClient.Request.BodyString, NullLogger.Instance, out var openAiRequest) &&
912+
openAiRequest is not null
913+
);
914+
915+
foreach (var requestLog in openAIRequestLogs)
916+
{
917+
try
918+
{
919+
var response = JsonSerializer.Deserialize<OpenAIResponse>(requestLog.Context!.Session.HttpClient.Response.BodyString, ProxyUtils.JsonSerializerOptions);
920+
if (response is null)
921+
{
922+
continue;
923+
}
924+
925+
var reportModelUsageInfo = GetReportModelUsageInfo(response);
926+
if (modelUsage.TryGetValue(response.Model, out var usagePerModel))
927+
{
928+
usagePerModel.AddRange(reportModelUsageInfo);
929+
}
930+
else
931+
{
932+
modelUsage.Add(response.Model, reportModelUsageInfo);
933+
}
934+
}
935+
catch (JsonException ex)
936+
{
937+
Logger.LogError(ex, "Failed to deserialize OpenAI response");
938+
}
939+
}
940+
941+
return modelUsage;
942+
}
943+
944+
private List<OpenAITelemetryPluginReportModelUsageInformation> GetReportModelUsageInfo(OpenAIResponse response)
945+
{
946+
Logger.LogTrace("GetReportModelUsageInfo() called");
947+
var usagePerModel = new List<OpenAITelemetryPluginReportModelUsageInformation>();
948+
var usage = response.Usage;
949+
if (usage is null)
950+
{
951+
return usagePerModel;
952+
}
953+
954+
var reportModelUsageInformation = new OpenAITelemetryPluginReportModelUsageInformation
955+
{
956+
Model = response.Model,
957+
PromptTokens = usage.PromptTokens,
958+
CompletionTokens = usage.CompletionTokens,
959+
CachedTokens = usage.PromptTokensDetails?.CachedTokens ?? 0L
960+
};
961+
usagePerModel.Add(reportModelUsageInformation);
962+
963+
if (!Configuration.IncludeCosts || Configuration.Prices is null)
964+
{
965+
Logger.LogDebug("Cost tracking is disabled or prices data is not available");
966+
return usagePerModel;
967+
}
968+
969+
if (string.IsNullOrEmpty(response.Model))
970+
{
971+
Logger.LogDebug("Response model is empty or null");
972+
return usagePerModel;
973+
}
974+
975+
var (inputCost, outputCost) = Configuration.Prices.CalculateCost(response.Model, usage.PromptTokens, usage.CompletionTokens);
976+
977+
if (inputCost > 0)
978+
{
979+
var totalCost = inputCost + outputCost;
980+
reportModelUsageInformation.Cost = totalCost;
981+
}
982+
else
983+
{
984+
Logger.LogDebug("Input cost is zero, skipping cost metrics recording");
985+
}
986+
987+
Logger.LogTrace("GetReportModelUsageInfo() finished");
988+
return usagePerModel;
989+
}
990+
908991
private static string GetOperationName(OpenAIRequest request)
909992
{
910993
if (request == null)

DevProxy/Proxy/ProxyStateController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public async Task StopRecordingAsync(CancellationToken cancellationToken)
6969

7070
public async Task MockRequestAsync(CancellationToken cancellationToken)
7171
{
72-
var eventArgs = new EventArgs();
72+
var eventArgs = EventArgs.Empty;
7373

7474
foreach (var plugin in _plugins.Where(p => p.Enabled))
7575
{

0 commit comments

Comments
 (0)