Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ protected AIFunctionDeclaration()
/// </para>
/// <code>
/// {
/// "title" : "addNumbers",
/// "description": "A simple function that adds two numbers together.",
/// "type": "object",
/// "properties": {
/// "a" : { "type": "number" },
/// "b" : { "type": "number", "default": 1 }
/// "b" : { "type": ["number","null"], "default": 1 }
/// },
/// "required" : ["a"]
/// }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,31 +1113,43 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
_ = Throw.IfNull(context);

using Activity? activity = _activitySource?.StartActivity(
$"{OpenTelemetryConsts.GenAI.ExecuteTool} {context.Function.Name}",
$"{OpenTelemetryConsts.GenAI.ExecuteToolName} {context.Function.Name}",
ActivityKind.Internal,
default(ActivityContext),
[
new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ExecuteTool),
new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ExecuteToolName),
new(OpenTelemetryConsts.GenAI.Tool.Type, OpenTelemetryConsts.ToolTypeFunction),
new(OpenTelemetryConsts.GenAI.Tool.Call.Id, context.CallContent.CallId),
new(OpenTelemetryConsts.GenAI.Tool.Name, context.Function.Name),
new(OpenTelemetryConsts.GenAI.Tool.Description, context.Function.Description),
]);

long startingTimestamp = 0;
if (_logger.IsEnabled(LogLevel.Debug))
long startingTimestamp = Stopwatch.GetTimestamp();

bool enableSensitiveData = activity is { IsAllDataRequested: true } && InnerClient.GetService<OpenTelemetryChatClient>()?.EnableSensitiveData is true;
bool traceLoggingEnabled = _logger.IsEnabled(LogLevel.Trace);
bool loggedInvoke = false;
if (enableSensitiveData || traceLoggingEnabled)
{
startingTimestamp = Stopwatch.GetTimestamp();
if (_logger.IsEnabled(LogLevel.Trace))
string functionArguments = TelemetryHelpers.AsJson(context.Arguments, context.Function.JsonSerializerOptions);

if (enableSensitiveData)
{
LogInvokingSensitive(context.Function.Name, TelemetryHelpers.AsJson(context.Arguments, context.Function.JsonSerializerOptions));
_ = activity?.SetTag(OpenTelemetryConsts.GenAI.Tool.Call.Arguments, functionArguments);
}
else

if (traceLoggingEnabled)
{
LogInvoking(context.Function.Name);
LogInvokingSensitive(context.Function.Name, functionArguments);
loggedInvoke = true;
}
}

if (!loggedInvoke && _logger.IsEnabled(LogLevel.Debug))
{
LogInvoking(context.Function.Name);
}

object? result = null;
try
{
Expand Down Expand Up @@ -1165,19 +1177,27 @@ FunctionResultContent CreateFunctionResultContent(FunctionInvocationResult resul
}
finally
{
if (_logger.IsEnabled(LogLevel.Debug))
bool loggedResult = false;
if (enableSensitiveData || traceLoggingEnabled)
{
TimeSpan elapsed = GetElapsedTime(startingTimestamp);
string functionResult = TelemetryHelpers.AsJson(result, context.Function.JsonSerializerOptions);

if (result is not null && _logger.IsEnabled(LogLevel.Trace))
if (enableSensitiveData)
{
LogInvocationCompletedSensitive(context.Function.Name, elapsed, TelemetryHelpers.AsJson(result, context.Function.JsonSerializerOptions));
_ = activity?.SetTag(OpenTelemetryConsts.GenAI.Tool.Call.Result, functionResult);
}
else

if (traceLoggingEnabled)
{
LogInvocationCompleted(context.Function.Name, elapsed);
LogInvocationCompletedSensitive(context.Function.Name, GetElapsedTime(startingTimestamp), functionResult);
loggedResult = true;
}
}

if (!loggedResult && _logger.IsEnabled(LogLevel.Debug))
{
LogInvocationCompleted(context.Function.Name, GetElapsedTime(startingTimestamp));
}
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,13 @@ internal static string SerializeChatMessages(IEnumerable<ChatMessage> messages,
string? modelId = options?.ModelId ?? _defaultModelId;

activity = _activitySource.StartActivity(
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Chat : $"{OpenTelemetryConsts.GenAI.Chat} {modelId}",
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.ChatName : $"{OpenTelemetryConsts.GenAI.ChatName} {modelId}",
ActivityKind.Client);

if (activity is { IsAllDataRequested: true })
{
_ = activity
.AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Chat)
.AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ChatName)
.AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId)
.AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName);

Expand Down Expand Up @@ -395,13 +395,28 @@ internal static string SerializeChatMessages(IEnumerable<ChatMessage> messages,
}
}

// Log all additional request options as raw values on the span.
// Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data.
if (EnableSensitiveData && options.AdditionalProperties is { } props)
if (EnableSensitiveData)
{
foreach (KeyValuePair<string, object?> prop in props)
if (options.Tools?.Any(t => t is AIFunctionDeclaration) is true)
{
_ = activity.AddTag(prop.Key, prop.Value);
_ = activity.AddTag(
OpenTelemetryConsts.GenAI.Tool.Definitions,
JsonSerializer.Serialize(options.Tools.OfType<AIFunctionDeclaration>().Select(t => new OtelFunction
{
Name = t.Name,
Description = t.Description,
Parameters = t.JsonSchema,
}), OtelContext.Default.IEnumerableOtelFunction));
}

// Log all additional request options as raw values on the span.
// Since AdditionalProperties has undefined meaning, we treat it as potentially sensitive data.
if (options.AdditionalProperties is { } props)
{
foreach (KeyValuePair<string, object?> prop in props)
{
_ = activity.AddTag(prop.Key, prop.Value);
}
}
}
}
Expand Down Expand Up @@ -505,7 +520,7 @@ private void TraceResponse(

void AddMetricTags(ref TagList tags, string? requestModelId, ChatResponse? response)
{
tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Chat);
tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.ChatName);

if (requestModelId is not null)
{
Expand Down Expand Up @@ -582,6 +597,14 @@ private sealed class OtelToolCallResponsePart
public object? Response { get; set; }
}

private sealed class OtelFunction
{
public string Type { get; set; } = "function";
public string? Name { get; set; }
public string? Description { get; set; }
public JsonElement Parameters { get; set; }
}

private static readonly JsonSerializerOptions _defaultOptions = CreateDefaultOptions();

private static JsonSerializerOptions CreateDefaultOptions()
Expand All @@ -606,5 +629,6 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(OtelGenericPart))]
[JsonSerializable(typeof(OtelToolCallRequestPart))]
[JsonSerializable(typeof(OtelToolCallResponsePart))]
[JsonSerializable(typeof(IEnumerable<OtelFunction>))]
private sealed partial class OtelContext : JsonSerializerContext;
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,13 @@ public async override Task<ImageGenerationResponse> GenerateAsync(
string? modelId = options?.ModelId ?? _defaultModelId;

activity = _activitySource.StartActivity(
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.GenerateContent : $"{OpenTelemetryConsts.GenAI.GenerateContent} {modelId}",
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.GenerateContentName : $"{OpenTelemetryConsts.GenAI.GenerateContentName} {modelId}",
ActivityKind.Client);

if (activity is { IsAllDataRequested: true })
{
_ = activity
.AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContent)
.AddTag(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContentName)
.AddTag(OpenTelemetryConsts.GenAI.Output.Type, OpenTelemetryConsts.TypeImage)
.AddTag(OpenTelemetryConsts.GenAI.Request.Model, modelId)
.AddTag(OpenTelemetryConsts.GenAI.Provider.Name, _providerName);
Expand Down Expand Up @@ -294,7 +294,7 @@ private void TraceResponse(

void AddMetricTags(ref TagList tags, string? requestModelId)
{
tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContent);
tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.GenerateContentName);

if (requestModelId is not null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,11 @@ protected override void Dispose(bool disposing)
string? modelId = options?.ModelId ?? _defaultModelId;

activity = _activitySource.StartActivity(
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Embeddings : $"{OpenTelemetryConsts.GenAI.Embeddings} {modelId}",
string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.EmbeddingsName : $"{OpenTelemetryConsts.GenAI.EmbeddingsName} {modelId}",
ActivityKind.Client,
default(ActivityContext),
[
new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings),
new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.EmbeddingsName),
new(OpenTelemetryConsts.GenAI.Request.Model, modelId),
new(OpenTelemetryConsts.GenAI.Provider.Name, _providerName),
]);
Expand All @@ -174,7 +174,7 @@ protected override void Dispose(bool disposing)

if ((options?.Dimensions ?? _defaultModelDimensions) is int dimensionsValue)
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.EmbeddingDimensions, dimensionsValue);
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Embeddings.Dimension.Count, dimensionsValue);
}

// Log all additional request options as raw values on the span.
Expand Down Expand Up @@ -265,7 +265,7 @@ private void TraceResponse(

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

if (requestModelId is not null)
{
Expand Down
20 changes: 15 additions & 5 deletions src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ public static class Error

public static class GenAI
{
public const string Chat = "chat";
public const string Embeddings = "embeddings";
public const string ExecuteTool = "execute_tool";
public const string GenerateContent = "generate_content";
public const string ChatName = "chat";
public const string EmbeddingsName = "embeddings";
public const string ExecuteToolName = "execute_tool";
public const string GenerateContentName = "generate_content";

public const string SystemInstructions = "gen_ai.system_instructions";

Expand All @@ -62,6 +62,14 @@ public static class Conversation
public const string Id = "gen_ai.conversation.id";
}

public static class Embeddings
{
public static class Dimension
{
public const string Count = "gen_ai.embeddings.dimension.count";
}
}

public static class Input
{
public const string Messages = "gen_ai.input.messages";
Expand All @@ -86,7 +94,6 @@ public static class Provider
public static class Request
{
public const string ChoiceCount = "gen_ai.request.choice.count";
public const string EmbeddingDimensions = "gen_ai.request.embedding.dimensions";
public const string FrequencyPenalty = "gen_ai.request.frequency_penalty";
public const string Model = "gen_ai.request.model";
public const string MaxTokens = "gen_ai.request.max_tokens";
Expand Down Expand Up @@ -116,10 +123,13 @@ public static class Tool
public const string Description = "gen_ai.tool.description";
public const string Message = "gen_ai.tool.message";
public const string Type = "gen_ai.tool.type";
public const string Definitions = "gen_ai.tool.definitions";

public static class Call
{
public const string Id = "gen_ai.tool.call.id";
public const string Arguments = "gen_ai.tool.call.arguments";
public const string Result = "gen_ai.tool.call.result";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public virtual async Task OpenTelemetry_CanEmitTracesAndMetrics()
Assert.Single(activities);
var activity = activities.Single();
Assert.StartsWith("embed", activity.DisplayName);
Assert.StartsWith("http", (string)activity.GetTagItem("server.address")!);
Assert.Contains(".", (string)activity.GetTagItem("server.address")!);
Assert.Equal(embeddingGenerator.GetService<EmbeddingGeneratorMetadata>()?.ProviderUri?.Port, (int)activity.GetTagItem("server.port")!);
Assert.NotNull(activity.Id);
Assert.NotEmpty(activity.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -655,9 +655,10 @@ async Task InvokeAsync(Func<IServiceProvider, Task> work)
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry)
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(true, true)]
public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry, bool enableSensitiveData)
{
string sourceName = Guid.NewGuid().ToString();

Expand All @@ -675,7 +676,7 @@ public async Task FunctionInvocationTrackedWithActivity(bool enableTelemetry)
};

Func<ChatClientBuilder, ChatClientBuilder> configure = b => b.Use(c =>
new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: sourceName)));
new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: sourceName) { EnableSensitiveData = enableSensitiveData }));

await InvokeAsync(() => InvokeAndAssertAsync(options, plan, configurePipeline: configure), streaming: false);

Expand All @@ -701,6 +702,23 @@ async Task InvokeAsync(Func<Task> work, bool streaming)
activity => Assert.Equal("chat", activity.DisplayName),
activity => Assert.Equal(streaming ? "FunctionInvokingChatClient.GetStreamingResponseAsync" : "FunctionInvokingChatClient.GetResponseAsync", activity.DisplayName));

var executeTool = activities[1];
if (enableSensitiveData)
{
var args = Assert.Single(executeTool.Tags, t => t.Key == "gen_ai.tool.call.arguments");
Assert.Equal(
JsonSerializer.Serialize(new Dictionary<string, object?> { ["arg1"] = "value1" }, AIJsonUtilities.DefaultOptions),
args.Value);

var result = Assert.Single(executeTool.Tags, t => t.Key == "gen_ai.tool.call.result");
Assert.Equal("Result 1", JsonSerializer.Deserialize<string>(result.Value!, AIJsonUtilities.DefaultOptions));
}
else
{
Assert.DoesNotContain(executeTool.Tags, t => t.Key == "gen_ai.tool.call.arguments");
Assert.DoesNotContain(executeTool.Tags, t => t.Key == "gen_ai.tool.call.result");
}

for (int i = 0; i < activities.Count - 1; i++)
{
// Activities are exported in the order of completion, so all except the last are children of the last (i.e., outer)
Expand Down
Loading
Loading