diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs index d21d3b20585..406b9768dd7 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs @@ -9,7 +9,10 @@ namespace Microsoft.Extensions.AI; public class ChatClientMetadata { /// Initializes a new instance of the class. - /// The name of the chat completion provider, if applicable. + /// + /// 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. + /// /// The URL for accessing the chat completion provider, if applicable. /// The ID of the chat completion model used, if applicable. public ChatClientMetadata(string? providerName = null, Uri? providerUri = null, string? modelId = null) @@ -20,12 +23,19 @@ public ChatClientMetadata(string? providerName = null, Uri? providerUri = null, } /// Gets the name of the chat completion provider. + /// + /// Where possible, this maps to the appropriate name defined in the + /// OpenTelemetry Semantic Conventions for Generative AI systems. + /// public string? ProviderName { get; } /// Gets the URL for accessing the chat completion provider. public Uri? ProviderUri { get; } /// Gets the ID of the model used by this chat completion provider. - /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance. + /// + /// 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 . + /// public string? ModelId { get; } } diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs index 0f2f7b23af5..a3f5181648b 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs @@ -9,7 +9,11 @@ namespace Microsoft.Extensions.AI; public class EmbeddingGeneratorMetadata { /// Initializes a new instance of the class. - /// The name of the embedding generation provider, if applicable. + + /// + /// 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. + /// /// The URL for accessing the embedding generation provider, if applicable. /// The ID of the embedding generation model used, if applicable. /// The number of dimensions in vectors produced by this generator, if applicable. @@ -22,15 +26,26 @@ public EmbeddingGeneratorMetadata(string? providerName = null, Uri? providerUri } /// Gets the name of the embedding generation provider. + /// + /// Where possible, this maps to the appropriate name defined in the + /// OpenTelemetry Semantic Conventions for Generative AI systems. + /// public string? ProviderName { get; } /// Gets the URL for accessing the embedding generation provider. public Uri? ProviderUri { get; } /// Gets the ID of the model used by this embedding generation provider. - /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance. + /// + /// 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 . + /// public string? ModelId { get; } /// Gets the number of dimensions in the embeddings produced by this instance. + /// + /// 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 . + /// public int? Dimensions { get; } } diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs index 193006780a2..e20a485a02a 100644 --- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs @@ -23,7 +23,7 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// The draft specification this follows is available at . +/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.29, defined at . /// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// public sealed partial class OpenTelemetryChatClient : DelegatingChatClient @@ -288,6 +288,29 @@ public override async IAsyncEnumerable 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 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; + } + } + } } } } @@ -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 prop in props) + { + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), + prop.Value); + } + } + } } } diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs index 09f762d33d0..8bb38bf2e07 100644 --- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs +++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs @@ -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; @@ -15,8 +16,8 @@ namespace Microsoft.Extensions.AI; /// Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems. /// -/// The draft specification this follows is available at . -/// 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 . +/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change. /// /// The type of input used to produce embeddings. /// The type of embedding generated. @@ -29,6 +30,7 @@ public sealed class OpenTelemetryEmbeddingGenerator : Delega private readonly Histogram _tokenUsageHistogram; private readonly Histogram _operationDurationHistogram; + private readonly string? _system; private readonly string? _modelId; private readonly string? _modelProvider; private readonly string? _endpointAddress; @@ -49,6 +51,7 @@ public OpenTelemetryEmbeddingGenerator(IEmbeddingGenerator 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); @@ -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), ]); @@ -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 prop in props) + { + _ = activity.AddTag( + OpenTelemetryConsts.GenAI.Request.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)), + prop.Value); + } + } + } } } @@ -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 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) { diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs index 27a543705ba..4c40c04c236 100644 --- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs +++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs @@ -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 { @@ -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