Skip to content

Commit

Permalink
Update otel chat client / embedding generator for 1.29
Browse files Browse the repository at this point in the history
Also address feedback to include additional properties as tags.
  • Loading branch information
stephentoub committed Dec 2, 2024
1 parent 5161cb9 commit debb76c
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ namespace Microsoft.Extensions.AI;
public class ChatClientMetadata
{
/// <summary>Initializes a new instance of the <see cref="ChatClientMetadata"/> class.</summary>
/// <param name="providerName">The name of the chat completion provider, if applicable.</param>
/// <param name="providerName">
/// The name of the chat completion provider, if applicable. Where possible, this should map to the
/// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
/// </param>
/// <param name="providerUri">The URL for accessing the chat completion provider, if applicable.</param>
/// <param name="modelId">The ID of the chat completion model used, if applicable.</param>
public ChatClientMetadata(string? providerName = null, Uri? providerUri = null, string? modelId = null)
Expand All @@ -20,12 +23,19 @@ public ChatClientMetadata(string? providerName = null, Uri? providerUri = null,
}

/// <summary>Gets the name of the chat completion provider.</summary>
/// <remarks>
/// Where possible, this maps to the appropriate name defined in the
/// OpenTelemetry Semantic Conventions for Generative AI systems.
/// </remarks>
public string? ProviderName { get; }

/// <summary>Gets the URL for accessing the chat completion provider.</summary>
public Uri? ProviderUri { get; }

/// <summary>Gets the ID of the model used by this chat completion provider.</summary>
/// <remarks>This value can be null if either the name is unknown or there are multiple possible models associated with this instance.</remarks>
/// <remarks>
/// This value can be null if either the name is unknown or there are multiple possible models associated with this instance.
/// An individual request may override this value via <see cref="ChatOptions.ModelId"/>.
/// </remarks>
public string? ModelId { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ namespace Microsoft.Extensions.AI;
public class EmbeddingGeneratorMetadata
{
/// <summary>Initializes a new instance of the <see cref="EmbeddingGeneratorMetadata"/> class.</summary>
/// <param name="providerName">The name of the embedding generation provider, if applicable.</param>

/// <param name="providerName">
/// The name of the embedding generation provider, if applicable. Where possible, this should map to the
/// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
/// </param>
/// <param name="providerUri">The URL for accessing the embedding generation provider, if applicable.</param>
/// <param name="modelId">The ID of the embedding generation model used, if applicable.</param>
/// <param name="dimensions">The number of dimensions in vectors produced by this generator, if applicable.</param>
Expand All @@ -22,15 +26,26 @@ public EmbeddingGeneratorMetadata(string? providerName = null, Uri? providerUri
}

/// <summary>Gets the name of the embedding generation provider.</summary>
/// <remarks>
/// Where possible, this maps to the appropriate name defined in the
/// OpenTelemetry Semantic Conventions for Generative AI systems.
/// </remarks>
public string? ProviderName { get; }

/// <summary>Gets the URL for accessing the embedding generation provider.</summary>
public Uri? ProviderUri { get; }

/// <summary>Gets the ID of the model used by this embedding generation provider.</summary>
/// <remarks>This value can be null if either the name is unknown or there are multiple possible models associated with this instance.</remarks>
/// <remarks>
/// This value can be null if either the name is unknown or there are multiple possible models associated with this instance.
/// An individual request may override this value via <see cref="EmbeddingGenerationOptions.ModelId"/>.
/// </remarks>
public string? ModelId { get; }

/// <summary>Gets the number of dimensions in the embeddings produced by this instance.</summary>
/// <remarks>
/// This value can be null if either the number of dimensions is unknown or there are multiple possible lengths associated with this instance.
/// An individual request may override this value via <see cref="EmbeddingGenerationOptions.Dimensions"/>.
/// </remarks>
public int? Dimensions { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace Microsoft.Extensions.AI;

/// <summary>Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems.</summary>
/// <remarks>
/// The draft specification this follows is available at <see href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" />.
/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.29, defined at <see href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" />.
/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change.
/// </remarks>
public sealed partial class OpenTelemetryChatClient : DelegatingChatClient
Expand Down Expand Up @@ -288,6 +288,29 @@ public override async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteSt
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.PerProvider(_system, "seed"), seed);
}

if (options.AdditionalProperties is { } props)
{
// Log all additional request options as per-provider tags. This is non-normative, but it covers cases where
// there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.request.service_tier),
// and more generally cases where there's additional useful information to be logged.
foreach (KeyValuePair<string, object?> prop in props)
{
string name = JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key);
switch (name)
{
// Skip known properties handled above so as to avoid possible conflicts.
case "response_format":
case "seed":
break;

// Handle all others by adding the snake_lower_case variant of the property name.
default:
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.PerProvider(_system, name), prop.Value);
break;
}
}
}
}
}
}
Expand Down Expand Up @@ -375,6 +398,22 @@ private void TraceCompletion(
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.OutputTokens, outputTokens);
}

if (_system is not null)
{
// Log all additional response properties as per-provider tags. This is non-normative, but it covers cases where
// there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.response.system_fingerprint),
// and more generally cases where there's additional useful information to be logged.
if (completion.AdditionalProperties is { } props)
{
foreach (KeyValuePair<string, object?> prop in props)
{
_ = activity.AddTag(
OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)),
prop.Value);
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
Expand All @@ -15,8 +16,8 @@ namespace Microsoft.Extensions.AI;

/// <summary>Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems.</summary>
/// <remarks>
/// The draft specification this follows is available at <see href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" />.
/// The specification is still experimental and subject to change; as such, the telemetry output by this generator is also subject to change.
/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.29, defined at <see href="https://opentelemetry.io/docs/specs/semconv/gen-ai/" />.
/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change.
/// </remarks>
/// <typeparam name="TInput">The type of input used to produce embeddings.</typeparam>
/// <typeparam name="TEmbedding">The type of embedding generated.</typeparam>
Expand All @@ -29,6 +30,7 @@ public sealed class OpenTelemetryEmbeddingGenerator<TInput, TEmbedding> : Delega
private readonly Histogram<int> _tokenUsageHistogram;
private readonly Histogram<double> _operationDurationHistogram;

private readonly string? _system;
private readonly string? _modelId;
private readonly string? _modelProvider;
private readonly string? _endpointAddress;
Expand All @@ -49,6 +51,7 @@ public OpenTelemetryEmbeddingGenerator(IEmbeddingGenerator<TInput, TEmbedding> i
Debug.Assert(innerGenerator is not null, "Should have been validated by the base ctor.");

EmbeddingGeneratorMetadata metadata = innerGenerator!.Metadata;
_system = metadata.ProviderName;
_modelId = metadata.ModelId;
_modelProvider = metadata.ProviderName;
_endpointAddress = metadata.ProviderUri?.GetLeftPart(UriPartial.Path);
Expand Down Expand Up @@ -126,11 +129,11 @@ protected override void Dispose(bool disposing)
string? modelId = options?.ModelId ?? _modelId;

activity = _activitySource.StartActivity(
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Embed : $"{OpenTelemetryConsts.GenAI.Embed} {modelId}",
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Embeddings : $"{OpenTelemetryConsts.GenAI.Embeddings} {modelId}",
ActivityKind.Client,
default(ActivityContext),
[
new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embed),
new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings),
new(OpenTelemetryConsts.GenAI.Request.Model, modelId),
new(OpenTelemetryConsts.GenAI.SystemName, _modelProvider),
]);
Expand All @@ -148,6 +151,23 @@ protected override void Dispose(bool disposing)
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.EmbeddingDimensions, dimensions);
}

if (options is not null &&
_system is not null)
{
// Log all additional request options as per-provider tags. This is non-normative, but it covers cases where
// there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.request.service_tier),
// and more generally cases where there's additional useful information to be logged.
if (options.AdditionalProperties is { } props)
{
foreach (KeyValuePair<string, object?> prop in props)
{
_ = activity.AddTag(
OpenTelemetryConsts.GenAI.Request.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)),
prop.Value);
}
}
}
}
}

Expand Down Expand Up @@ -212,12 +232,26 @@ private void TraceCompletion(
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.Model, responseModelId);
}

// Log all additional response properties as per-provider tags. This is non-normative, but it covers cases where
// there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.response.system_fingerprint),
// and more generally cases where there's additional useful information to be logged.
if (_system is not null &&
embeddings?.AdditionalProperties is { } props)
{
foreach (KeyValuePair<string, object?> prop in props)
{
_ = activity.AddTag(
OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)),
prop.Value);
}
}
}
}

private void AddMetricTags(ref TagList tags, string? requestModelId, string? responseModelId)
{
tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embed);
tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings);

if (requestModelId is not null)
{
Expand Down
4 changes: 3 additions & 1 deletion src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static class GenAI
public const string SystemName = "gen_ai.system";

public const string Chat = "chat";
public const string Embed = "embed";
public const string Embeddings = "embeddings";

public static class Assistant
{
Expand Down Expand Up @@ -81,6 +81,8 @@ public static class Response
public const string InputTokens = "gen_ai.response.input_tokens";
public const string Model = "gen_ai.response.model";
public const string OutputTokens = "gen_ai.response.output_tokens";

public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.response.{parameterName}";
}

public static class System
Expand Down

0 comments on commit debb76c

Please sign in to comment.