openai/openai-dotnet

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fe4d6cf1ccfea48fa5c2baf67103495ac9b459dd

Branches

Tags

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

Clone

HTTPS

Download ZIP

OpenAI/src/Utility/Telemetry/OpenTelemetryScope.cs

222lines · modecode

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