Skip to content

Commit d5b5c60

Browse files
author
Liudmila Molkova
authored
Add instrumentation for simple convenience chat calls (#107)
This PR builds foundation for OpenAI SDK tracing and metrics instrumentation (using Otel-compatible .NET primitives). It's limited to convenience `ChatClient` methods without streaming. The PR implements instrumentation according to [OpenTelemetry GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/gen-ai). The intention is to add instrumentation to other methods and client types and evolve it along with OTel GenAI semantic conventions. TODO (in this PR): - [x] add samples/docs - [x] add experimental feature-flag required to enable instrumentation - we don't know when OTel semantic conventions will be stable and expect breaking changes. TODO (in next PRs): - [ ] add instrumentation to streaming calls and protocol methods - [ ] track prompts and completions in events - ...
1 parent 3284295 commit d5b5c60

15 files changed

+966
-18
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![NuGet version](https://img.shields.io/nuget/vpre/openai.svg)](https://www.nuget.org/packages/OpenAI/absoluteLatest)
44

5-
The OpenAI .NET library provides convenient access to the OpenAI REST API from .NET applications.
5+
The OpenAI .NET library provides convenient access to the OpenAI REST API from .NET applications.
66

77
It is generated from our [OpenAPI specification](https://github.com/openai/openai-openapi) in collaboration with Microsoft.
88

@@ -26,6 +26,7 @@ It is generated from our [OpenAPI specification](https://github.com/openai/opena
2626
- [Advanced scenarios](#advanced-scenarios)
2727
- [Using protocol methods](#using-protocol-methods)
2828
- [Automatically retrying errors](#automatically-retrying-errors)
29+
- [Observability](#observability)
2930

3031
## Getting started
3132

@@ -714,7 +715,7 @@ For example, to use the protocol method variant of the `ChatClient`'s `CompleteC
714715
ChatClient client = new("gpt-4o", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
715716

716717
BinaryData input = BinaryData.FromBytes("""
717-
{
718+
{
718719
"model": "gpt-4o",
719720
"messages": [
720721
{
@@ -749,3 +750,7 @@ By default, the client classes will automatically retry the following errors up
749750
- 502 Bad Gateway
750751
- 503 Service Unavailable
751752
- 504 Gateway Timeout
753+
754+
## Observability
755+
756+
OpenAI .NET library supports experimental distributed tracing and metrics with OpenTelemetry. Check out [Observability with OpenTelemetry](./docs/observability.md) for more details.

docs/observability.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
## Observability with OpenTelemetry
2+
3+
> Note:
4+
> OpenAI .NET SDK instrumentation is in development and is not complete. See [Available sources and meters](#available-sources-and-meters) section for the list of covered operations.
5+
6+
OpenAI .NET library is instrumented with distributed tracing and metrics using .NET [tracing](https://learn.microsoft.com/dotnet/core/diagnostics/distributed-tracing)
7+
and [metrics](https://learn.microsoft.com/dotnet/core/diagnostics/metrics-instrumentation) API and supports [OpenTelemetry](https://learn.microsoft.com/dotnet/core/diagnostics/observability-with-otel).
8+
9+
OpenAI .NET instrumentation follows [OpenTelemetry Semantic Conventions for Generative AI systems](https://github.com/open-telemetry/semantic-conventions/tree/main/docs/gen-ai).
10+
11+
### How to enable
12+
13+
The instrumentation is **experimental** - volume and semantics of the telemetry items may change.
14+
15+
To enable the instrumentation:
16+
17+
1. Set instrumentation feature-flag using one of the following options:
18+
19+
- set the `OPENAI_EXPERIMENTAL_ENABLE_OPEN_TELEMETRY` environment variable to `"true"`
20+
- set the `OpenAI.Experimental.EnableOpenTelemetry` context switch to true in your application code when application
21+
is starting and before initializing any OpenAI clients. For example:
22+
23+
```csharp
24+
AppContext.SetSwitch("OpenAI.Experimental.EnableOpenTelemetry", true);
25+
```
26+
27+
2. Enable OpenAI telemetry:
28+
29+
```csharp
30+
builder.Services.AddOpenTelemetry()
31+
.WithTracing(b =>
32+
{
33+
b.AddSource("OpenAI.*")
34+
...
35+
.AddOtlpExporter();
36+
})
37+
.WithMetrics(b =>
38+
{
39+
b.AddMeter("OpenAI.*")
40+
...
41+
.AddOtlpExporter();
42+
});
43+
```
44+
45+
Distributed tracing is enabled with `AddSource("OpenAI.*")` which tells OpenTelemetry to listen to all [ActivitySources](https://learn.microsoft.com/dotnet/api/system.diagnostics.activitysource) with names starting with `OpenAI.*`.
46+
47+
Similarly, metrics are configured with `AddMeter("OpenAI.*")` which enables all OpenAI-related [Meters](https://learn.microsoft.com/dotnet/api/system.diagnostics.metrics.meter).
48+
49+
Consider enabling [HTTP client instrumentation](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Http) to see all HTTP client
50+
calls made by your application including those done by the OpenAI SDK.
51+
Check out [OpenTelemetry documentation](https://opentelemetry.io/docs/languages/net/getting-started/) for more details.
52+
53+
### Available sources and meters
54+
55+
The following sources and meters are available:
56+
57+
- `OpenAI.ChatClient` - records traces and metrics for `ChatClient` operations (except streaming and protocol methods which are not instrumented yet)

src/Custom/Chat/ChatClient.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using OpenAI.Telemetry;
12
using System;
23
using System.ClientModel;
34
using System.ClientModel.Primitives;
@@ -14,6 +15,7 @@ namespace OpenAI.Chat;
1415
public partial class ChatClient
1516
{
1617
private readonly string _model;
18+
private readonly OpenTelemetrySource _telemetry;
1719

1820
/// <summary>
1921
/// Initializes a new instance of <see cref="ChatClient"/> that will use an API key when authenticating.
@@ -62,6 +64,7 @@ protected internal ChatClient(ClientPipeline pipeline, string model, Uri endpoin
6264
_model = model;
6365
_pipeline = pipeline;
6466
_endpoint = endpoint;
67+
_telemetry = new OpenTelemetrySource(model, endpoint);
6568
}
6669

6770
/// <summary>
@@ -77,11 +80,22 @@ public virtual async Task<ClientResult<ChatCompletion>> CompleteChatAsync(IEnume
7780

7881
options ??= new();
7982
CreateChatCompletionOptions(messages, ref options);
80-
81-
using BinaryContent content = options.ToBinaryContent();
82-
83-
ClientResult result = await CompleteChatAsync(content, cancellationToken.ToRequestOptions()).ConfigureAwait(false);
84-
return ClientResult.FromValue(ChatCompletion.FromResponse(result.GetRawResponse()), result.GetRawResponse());
83+
using OpenTelemetryScope scope = _telemetry.StartChatScope(options);
84+
85+
try
86+
{
87+
using BinaryContent content = options.ToBinaryContent();
88+
89+
ClientResult result = await CompleteChatAsync(content, cancellationToken.ToRequestOptions()).ConfigureAwait(false);
90+
ChatCompletion chatCompletion = ChatCompletion.FromResponse(result.GetRawResponse());
91+
scope?.RecordChatCompletion(chatCompletion);
92+
return ClientResult.FromValue(chatCompletion, result.GetRawResponse());
93+
}
94+
catch (Exception ex)
95+
{
96+
scope?.RecordException(ex);
97+
throw;
98+
}
8599
}
86100

87101
/// <summary>
@@ -105,11 +119,22 @@ public virtual ClientResult<ChatCompletion> CompleteChat(IEnumerable<ChatMessage
105119

106120
options ??= new();
107121
CreateChatCompletionOptions(messages, ref options);
108-
109-
using BinaryContent content = options.ToBinaryContent();
110-
ClientResult result = CompleteChat(content, cancellationToken.ToRequestOptions());
111-
return ClientResult.FromValue(ChatCompletion.FromResponse(result.GetRawResponse()), result.GetRawResponse());
112-
122+
using OpenTelemetryScope scope = _telemetry.StartChatScope(options);
123+
124+
try
125+
{
126+
using BinaryContent content = options.ToBinaryContent();
127+
ClientResult result = CompleteChat(content, cancellationToken.ToRequestOptions());
128+
ChatCompletion chatCompletion = ChatCompletion.FromResponse(result.GetRawResponse());
129+
130+
scope?.RecordChatCompletion(chatCompletion);
131+
return ClientResult.FromValue(chatCompletion, result.GetRawResponse());
132+
}
133+
catch (Exception ex)
134+
{
135+
scope?.RecordException(ex);
136+
throw;
137+
}
113138
}
114139

115140
/// <summary>
@@ -200,7 +225,7 @@ private void CreateChatCompletionOptions(IEnumerable<ChatMessage> messages, ref
200225
{
201226
options.Messages = messages.ToList();
202227
options.Model = _model;
203-
options.Stream = stream
228+
options.Stream = stream
204229
? true
205230
: null;
206231
options.StreamOptions = stream ? options.StreamOptions : null;

src/OpenAI.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<!-- Generate an XML documentation file for the project. -->
1414
<GenerateDocumentationFile>true</GenerateDocumentationFile>
15-
15+
1616
<!-- Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
1717
<PublishRepositoryUrl>true</PublishRepositoryUrl>
1818
<PackageIcon>OpenAI.png</PackageIcon>
@@ -21,15 +21,15 @@
2121
<!-- Create a .snupkg file in addition to the .nupkg file. -->
2222
<IncludeSymbols>true</IncludeSymbols>
2323
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
24-
24+
2525
<!-- Embed source files that are not tracked by the source control manager in the PDB -->
2626
<EmbedUntrackedSources>true</EmbedUntrackedSources>
2727

2828
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
2929

3030
<!-- Disable missing XML documentation warnings -->
3131
<NoWarn>$(NoWarn),1570,1573,1574,1591</NoWarn>
32-
32+
3333
<!-- Disable obsolete warnings -->
3434
<NoWarn>$(NoWarn),0618</NoWarn>
3535

@@ -63,7 +63,6 @@
6363
<!-- Normalize stored file paths in symbols when in a CI build. -->
6464
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
6565
</PropertyGroup>
66-
6766
<ItemGroup>
6867
<None Include="OpenAI.png" Pack="true" PackagePath="\" />
6968
<None Include="..\CHANGELOG.md" Pack="true" PackagePath="\" />
@@ -73,5 +72,6 @@
7372
<ItemGroup>
7473
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
7574
<PackageReference Include="System.ClientModel" Version="1.1.0-beta.5" />
75+
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
7676
</ItemGroup>
7777
</Project>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
3+
namespace OpenAI;
4+
5+
internal static class AppContextSwitchHelper
6+
{
7+
/// <summary>
8+
/// Determines if either an AppContext switch or its corresponding Environment Variable is set
9+
/// </summary>
10+
/// <param name="appContexSwitchName">Name of the AppContext switch.</param>
11+
/// <param name="environmentVariableName">Name of the Environment variable.</param>
12+
/// <returns>If the AppContext switch has been set, returns the value of the switch.
13+
/// If the AppContext switch has not been set, returns the value of the environment variable.
14+
/// False if neither is set.
15+
/// </returns>
16+
public static bool GetConfigValue(string appContexSwitchName, string environmentVariableName)
17+
{
18+
// First check for the AppContext switch, giving it priority over the environment variable.
19+
if (AppContext.TryGetSwitch(appContexSwitchName, out bool value))
20+
{
21+
return value;
22+
}
23+
// AppContext switch wasn't used. Check the environment variable.
24+
string envVar = Environment.GetEnvironmentVariable(environmentVariableName);
25+
if (envVar != null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1")))
26+
{
27+
return true;
28+
}
29+
30+
// Default to false.
31+
return false;
32+
}
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace OpenAI.Telemetry;
2+
3+
internal class OpenTelemetryConstants
4+
{
5+
// follow OpenTelemetry GenAI semantic conventions:
6+
// https://github.com/open-telemetry/semantic-conventions/tree/v1.27.0/docs/gen-ai
7+
8+
public const string ErrorTypeKey = "error.type";
9+
public const string ServerAddressKey = "server.address";
10+
public const string ServerPortKey = "server.port";
11+
12+
public const string GenAiClientOperationDurationMetricName = "gen_ai.client.operation.duration";
13+
public const string GenAiClientTokenUsageMetricName = "gen_ai.client.token.usage";
14+
15+
public const string GenAiOperationNameKey = "gen_ai.operation.name";
16+
17+
public const string GenAiRequestMaxTokensKey = "gen_ai.request.max_tokens";
18+
public const string GenAiRequestModelKey = "gen_ai.request.model";
19+
public const string GenAiRequestTemperatureKey = "gen_ai.request.temperature";
20+
public const string GenAiRequestTopPKey = "gen_ai.request.top_p";
21+
22+
public const string GenAiResponseIdKey = "gen_ai.response.id";
23+
public const string GenAiResponseFinishReasonKey = "gen_ai.response.finish_reasons";
24+
public const string GenAiResponseModelKey = "gen_ai.response.model";
25+
26+
public const string GenAiSystemKey = "gen_ai.system";
27+
public const string GenAiSystemValue = "openai";
28+
29+
public const string GenAiTokenTypeKey = "gen_ai.token.type";
30+
31+
public const string GenAiUsageInputTokensKey = "gen_ai.usage.input_tokens";
32+
public const string GenAiUsageOutputTokensKey = "gen_ai.usage.output_tokens";
33+
}

0 commit comments

Comments
 (0)