openai/openai-dotnet

Public

mirrored from https://github.com/openai/openai-dotnetAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
achandmsft-patch-2

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/Utility/Telemetry/OpenTelemetryScope.cs

222lines · modeblame

d5b5c604Liudmila Molkova2 years ago1using OpenAI.Chat;
2using System;
3using System.ClientModel;
4using System.Diagnostics;
5using System.Diagnostics.Metrics;
6
7using static OpenAI.Telemetry.OpenTelemetryConstants;
8
9namespace OpenAI.Telemetry;
10
11internal class OpenTelemetryScope : IDisposable
12{
13private static readonly ActivitySource s_chatSource = new ActivitySource("OpenAI.ChatClient");
14private static readonly Meter s_chatMeter = new Meter("OpenAI.ChatClient");
15
16// TODO: add explicit histogram buckets once System.Diagnostics.DiagnosticSource 9.0 is used
17private static readonly Histogram<double> s_duration = s_chatMeter.CreateHistogram<double>(GenAiClientOperationDurationMetricName, "s", "Measures GenAI operation duration.");
18private static readonly Histogram<long> s_tokens = s_chatMeter.CreateHistogram<long>(GenAiClientTokenUsageMetricName, "{token}", "Measures the number of input and output token used.");
19
20private readonly string _operationName;
21private readonly string _serverAddress;
22private readonly int _serverPort;
23private readonly string _requestModel;
24
25private Stopwatch _duration;
26private Activity _activity;
27private TagList _commonTags;
28
29private OpenTelemetryScope(
30string model, string operationName,
31string serverAddress, int serverPort)
32{
33_requestModel = model;
34_operationName = operationName;
35_serverAddress = serverAddress;
36_serverPort = serverPort;
37}
38
39private static bool IsChatEnabled => s_chatSource.HasListeners() || s_tokens.Enabled || s_duration.Enabled;
40
41public static OpenTelemetryScope StartChat(string model, string operationName,
42string serverAddress, int serverPort, ChatCompletionOptions options)
43{
44if (IsChatEnabled)
45{
46var scope = new OpenTelemetryScope(model, operationName, serverAddress, serverPort);
47scope.StartChat(options);
48return scope;
49}
50
51return null;
52}
53
54private void StartChat(ChatCompletionOptions options)
55{
56_duration = Stopwatch.StartNew();
57_commonTags = new TagList
58{
59{ GenAiSystemKey, GenAiSystemValue },
60{ GenAiRequestModelKey, _requestModel },
61{ ServerAddressKey, _serverAddress },
62{ ServerPortKey, _serverPort },
63{ GenAiOperationNameKey, _operationName },
64};
65
66_activity = s_chatSource.StartActivity(string.Concat(_operationName, " ", _requestModel), ActivityKind.Client);
67if (_activity?.IsAllDataRequested == true)
68{
69RecordCommonAttributes();
2ab1a942Jose Arriaga Maldonado1 years ago70SetActivityTagIfNotNull(GenAiRequestMaxTokensKey, options?.MaxOutputTokenCount);
d5b5c604Liudmila Molkova2 years ago71SetActivityTagIfNotNull(GenAiRequestTemperatureKey, options?.Temperature);
72SetActivityTagIfNotNull(GenAiRequestTopPKey, options?.TopP);
73}
74
75return;
76}
77
78public void RecordChatCompletion(ChatCompletion completion)
79{
2ab1a942Jose Arriaga Maldonado1 years ago80RecordMetrics(completion.Model, null, completion.Usage?.InputTokenCount, completion.Usage?.OutputTokenCount);
d5b5c604Liudmila Molkova2 years ago81
82if (_activity?.IsAllDataRequested == true)
83{
84RecordResponseAttributes(completion.Id, completion.Model, completion.FinishReason, completion.Usage);
85}
86}
87
88public void RecordException(Exception ex)
89{
90var errorType = GetErrorType(ex);
91RecordMetrics(null, errorType, null, null);
92if (_activity?.IsAllDataRequested == true)
93{
94_activity?.SetTag(OpenTelemetryConstants.ErrorTypeKey, errorType);
95_activity?.SetStatus(ActivityStatusCode.Error, ex?.Message ?? errorType);
96}
97}
98
99public void Dispose()
100{
101_activity?.Stop();
102}
103
104private void RecordCommonAttributes()
105{
106_activity.SetTag(GenAiSystemKey, GenAiSystemValue);
107_activity.SetTag(GenAiRequestModelKey, _requestModel);
108_activity.SetTag(ServerAddressKey, _serverAddress);
109_activity.SetTag(ServerPortKey, _serverPort);
110_activity.SetTag(GenAiOperationNameKey, _operationName);
111}
112
113private void RecordMetrics(string responseModel, string errorType, int? inputTokensUsage, int? outputTokensUsage)
114{
115// tags is a struct, let's copy and modify them
116var tags = _commonTags;
117
118if (responseModel != null)
119{
120tags.Add(GenAiResponseModelKey, responseModel);
121}
122
123if (inputTokensUsage != null)
124{
125var inputUsageTags = tags;
126inputUsageTags.Add(GenAiTokenTypeKey, "input");
127s_tokens.Record(inputTokensUsage.Value, inputUsageTags);
128}
129
130if (outputTokensUsage != null)
131{
132var outputUsageTags = tags;
133outputUsageTags.Add(GenAiTokenTypeKey, "output");
134s_tokens.Record(outputTokensUsage.Value, outputUsageTags);
135}
136
137if (errorType != null)
138{
139tags.Add(ErrorTypeKey, errorType);
140}
141
142s_duration.Record(_duration.Elapsed.TotalSeconds, tags);
143}
144
145private void RecordResponseAttributes(string responseId, string model, ChatFinishReason? finishReason, ChatTokenUsage usage)
146{
147SetActivityTagIfNotNull(GenAiResponseIdKey, responseId);
148SetActivityTagIfNotNull(GenAiResponseModelKey, model);
2ab1a942Jose Arriaga Maldonado1 years ago149SetActivityTagIfNotNull(GenAiUsageInputTokensKey, usage?.InputTokenCount);
150SetActivityTagIfNotNull(GenAiUsageOutputTokensKey, usage?.OutputTokenCount);
d5b5c604Liudmila Molkova2 years ago151SetFinishReasonAttribute(finishReason);
152}
153
154private void SetFinishReasonAttribute(ChatFinishReason? finishReason)
155{
156if (finishReason == null)
157{
158return;
159}
160
161var reasonStr = finishReason switch
162{
163ChatFinishReason.ContentFilter => "content_filter",
164ChatFinishReason.FunctionCall => "function_call",
165ChatFinishReason.Length => "length",
166ChatFinishReason.Stop => "stop",
167ChatFinishReason.ToolCalls => "tool_calls",
168_ => finishReason.ToString(),
169};
170
171// There could be multiple finish reasons, so semantic conventions use array type for the corrresponding attribute.
172// It's likely to change, but for now let's report it as array.
173_activity.SetTag(GenAiResponseFinishReasonKey, new[] { reasonStr });
174}
175
176private string GetChatMessageRole(ChatMessageRole? role) =>
177role switch
178{
179ChatMessageRole.Assistant => "assistant",
180ChatMessageRole.Function => "function",
181ChatMessageRole.System => "system",
182ChatMessageRole.Tool => "tool",
183ChatMessageRole.User => "user",
184_ => role?.ToString(),
185};
186
187private string GetErrorType(Exception exception)
188{
189if (exception is ClientResultException requestFailedException)
190{
191// TODO (lmolkova) when we start targeting .NET 8 we should put
192// requestFailedException.InnerException.HttpRequestError into error.type
193return requestFailedException.Status.ToString();
194}
195
196return exception?.GetType()?.FullName;
197}
198
199private void SetActivityTagIfNotNull(string name, object value)
200{
201if (value != null)
202{
203_activity.SetTag(name, value);
204}
205}
206
207private void SetActivityTagIfNotNull(string name, int? value)
208{
209if (value.HasValue)
210{
211_activity.SetTag(name, value.Value);
212}
213}
214
215private void SetActivityTagIfNotNull(string name, float? value)
216{
217if (value.HasValue)
218{
219_activity.SetTag(name, value.Value);
220}
221}
222}