Skip to content
Closed
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
5 changes: 3 additions & 2 deletions src/Observability/Extensions/OpenAI/BuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ public static class BuilderExtensions
/// </summary>
/// <param name="builder">The builder to configure.</param>
/// <param name="enableRelatedSources">Whether to enable related tracing sources for OpenTelemetry.</param>
/// <param name="options">Configuration options for OpenAI span processing. If null, default options will be used.</param>
/// <returns>The configured builder for method chaining.</returns>
public static Builder WithOpenAI(this Builder builder, bool enableRelatedSources = true)
public static Builder WithOpenAI(this Builder builder, bool enableRelatedSources = true, OpenAISpanProcessorOptions? options = null)
{
if (enableRelatedSources)
{
AppContext.SetSwitch("OpenAI.Experimental.EnableOpenTelemetry", true);
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource(OpenAITelemetryConstants.OpenAISourceWildcard)
.AddProcessor(new OpenAISpanProcessor()));
.AddProcessor(new OpenAISpanProcessor(options ?? new OpenAISpanProcessorOptions())));
}

return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<ItemGroup>
<ProjectReference Include="..\..\Runtime\Microsoft.Agents.A365.Observability.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.A365.Observability.Extension.Tests" />
</ItemGroup>
</Project>


Expand Down
20 changes: 20 additions & 0 deletions src/Observability/Extensions/OpenAI/OpenAISpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ namespace Microsoft.Agents.A365.Observability.Extensions.OpenAI;
internal class OpenAISpanProcessor : BaseProcessor<Activity>
{
private static readonly string TargetSourceName = OpenAITelemetryConstants.OpenAISource;
private readonly OpenAISpanProcessorOptions _options;

public OpenAISpanProcessor(OpenAISpanProcessorOptions options)
{
_options = options ?? new OpenAISpanProcessorOptions();
}

public override void OnStart(Activity activity)
{
Expand All @@ -32,5 +38,19 @@ public override void OnEnd(Activity activity)
}
}
}

// Remove prompt data from InvokeAgent scopes if configured
if (!_options.SendPromptInInvokeAgentScopes)
{
if (activity.OperationName == InvokeAgentScope.OperationName ||
(activity.DisplayName != null && activity.DisplayName.StartsWith(InvokeAgentScope.OperationName)))
{
// Remove the gen_ai.input.messages tag to prevent sending prompt content
if (activity.Tags.Any(tag => tag.Key == OpenTelemetryConstants.GenAiInputMessagesKey))
{
activity.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, null);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Agents.A365.Observability.Extensions.OpenAI;

/// <summary>
/// Configuration options for OpenAI span processing.
/// </summary>
public class OpenAISpanProcessorOptions
{
/// <summary>
/// Gets or sets a value indicating whether to send LLM prompt content in InvokeAgent scopes.
/// When set to false, the gen_ai.input.messages tag will be removed from InvokeAgent spans
/// to prevent sensitive prompt data from being recorded in telemetry.
/// Defaults to true for backward compatibility.
/// </summary>
public bool SendPromptInInvokeAgentScopes { get; set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@

<ItemGroup>
<ProjectReference Include="..\..\Observability\Extensions\SemanticKernel\Microsoft.Agents.A365.Observability.Extensions.SemanticKernel.csproj" />
<ProjectReference Include="..\..\Observability\Extensions\OpenAI\Microsoft.Agents.A365.Observability.Extensions.OpenAI.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics;
using Microsoft.Agents.A365.Observability.Extensions.OpenAI;
using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;

namespace Microsoft.Agents.A365.Observability.Extension.Tests
{
[TestClass]
public class OpenAISpanProcessorTests
{
private const string InvokeAgentOperationName = "invoke_agent";
private const string OpenAISourceName = "OpenAI.Test";

[TestMethod]
public void OpenAISpanProcessor_SendPromptInInvokeAgentScopes_DefaultsToTrue()
{
// Arrange
var options = new OpenAISpanProcessorOptions();

// Assert
Assert.IsTrue(options.SendPromptInInvokeAgentScopes, "SendPromptInInvokeAgentScopes should default to true for backward compatibility");
}

[TestMethod]
public void OpenAISpanProcessor_WithDefaultOptions_PreservesPromptInInvokeAgentScope()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = true };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Test prompt content");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNotNull(promptTag.Value, "Prompt should be preserved when SendPromptInInvokeAgentScopes is true");
Assert.AreEqual("Test prompt content", promptTag.Value);
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_RemovesPromptFromInvokeAgentScope()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Sensitive prompt content");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNull(promptTag.Value, "Prompt should be removed when SendPromptInInvokeAgentScopes is false");
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_RemovesPromptFromInvokeAgentScopeWithAgentName()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity("invoke_agent MyAgent")
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Sensitive prompt content");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNull(promptTag.Value, "Prompt should be removed from invoke_agent scope with agent name");
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_PreservesOtherTags()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Sensitive prompt")
.SetTag(OpenTelemetryConstants.GenAiOutputMessagesKey, "Response content")
.SetTag(OpenTelemetryConstants.GenAiAgentIdKey, "agent-123")
.SetTag(OpenTelemetryConstants.GenAiConversationIdKey, "conv-456");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
var outputTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiOutputMessagesKey);
var agentIdTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiAgentIdKey);
var conversationIdTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiConversationIdKey);

Assert.IsNull(promptTag.Value, "Prompt should be removed");
Assert.AreEqual("Response content", outputTag.Value, "Output messages should be preserved");
Assert.AreEqual("agent-123", agentIdTag.Value, "Agent ID should be preserved");
Assert.AreEqual("conv-456", conversationIdTag.Value, "Conversation ID should be preserved");
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_DoesNotAffectNonInvokeAgentScopes()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity("execute_inference")
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Inference prompt content");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNotNull(promptTag.Value, "Prompt should be preserved for non-InvokeAgent scopes");
Assert.AreEqual("Inference prompt content", promptTag.Value);
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_DoesNotAffectExecuteToolScopes()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity("execute_tool")
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Tool input content");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNotNull(promptTag.Value, "Prompt should be preserved for execute_tool scopes");
Assert.AreEqual("Tool input content", promptTag.Value);
}

[TestMethod]
public void OpenAISpanProcessor_WithNullOptions_UsesDefaultBehavior()
{
// Arrange
var processor = new OpenAISpanProcessor(null!);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Test prompt");
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNotNull(promptTag.Value, "Prompt should be preserved with null options (defaults to true)");
Assert.AreEqual("Test prompt", promptTag.Value);
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_HandlesActivityWithoutPromptTag()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiAgentIdKey, "agent-123");
activity.Start();

// Act - should not throw
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNull(promptTag.Value, "Prompt tag should remain null when it was never set");
}

[TestMethod]
public void OpenAISpanProcessor_WithSuppressOption_HandlesEmptyPromptTag()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, string.Empty);
activity.Start();

// Act
processor.OnEnd(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
// After setting to null, the tag might still exist but with null value
Assert.IsTrue(promptTag.Value == null || string.IsNullOrEmpty(promptTag.Value as string),
"Empty prompt should be removed or set to null");
}

[TestMethod]
public void OpenAISpanProcessor_OnStart_DoesNotModifyActivity()
{
// Arrange
var options = new OpenAISpanProcessorOptions { SendPromptInInvokeAgentScopes = false };
var processor = new OpenAISpanProcessor(options);

using var activity = new Activity(InvokeAgentOperationName)
.SetTag(OpenTelemetryConstants.GenAiInputMessagesKey, "Test prompt");
activity.Start();

// Act
processor.OnStart(activity);

// Assert
var promptTag = activity.Tags.FirstOrDefault(t => t.Key == OpenTelemetryConstants.GenAiInputMessagesKey);
Assert.IsNotNull(promptTag.Value, "OnStart should not modify the activity");
Assert.AreEqual("Test prompt", promptTag.Value);
}
}
}