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
@@ -0,0 +1,167 @@
using System.Diagnostics;

namespace ANcpSdk.AspNetCore.ServiceDefaults.Instrumentation.GenAi;

/// <summary>
/// Extension methods for setting GenAI semantic convention attributes on Activities.
/// </summary>
/// <remarks>
/// Provides fluent API for OTel 1.39 GenAI semantic conventions.
/// </remarks>
public static class GenAiActivityExtensions
{
/// <summary>
/// Sets GenAI request attributes on the activity.
/// </summary>
/// <param name="activity">The activity to set tags on.</param>
/// <param name="model">The model requested (e.g., "gpt-4o", "claude-3-opus").</param>
/// <param name="temperature">Temperature setting (0.0-2.0).</param>
/// <param name="maxTokens">Maximum tokens to generate.</param>
/// <param name="topP">Nucleus sampling threshold.</param>
/// <param name="topK">Top-k sampling parameter.</param>
/// <returns>The activity for fluent chaining.</returns>
public static Activity SetGenAiRequest(
this Activity activity,
string? model = null,
double? temperature = null,
int? maxTokens = null,
double? topP = null,
int? topK = null)
{
ArgumentNullException.ThrowIfNull(activity);

if (model is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.RequestModel, model);

if (temperature.HasValue)
activity.SetTag(SemanticConventions.GenAi.RequestTemperature, temperature.Value);

if (maxTokens.HasValue)
activity.SetTag(SemanticConventions.GenAi.RequestMaxTokens, maxTokens.Value);

if (topP.HasValue)
activity.SetTag(SemanticConventions.GenAi.RequestTopP, topP.Value);

if (topK.HasValue)
activity.SetTag(SemanticConventions.GenAi.RequestTopK, topK.Value);

return activity;
}

/// <summary>
/// Sets GenAI token usage attributes on the activity.
/// </summary>
/// <param name="activity">The activity to set tags on.</param>
/// <param name="inputTokens">Number of input/prompt tokens.</param>
/// <param name="outputTokens">Number of output/completion tokens.</param>
/// <param name="cachedTokens">Number of cached input tokens (Anthropic).</param>
/// <param name="reasoningTokens">Number of reasoning tokens (o1-style models).</param>
/// <returns>The activity for fluent chaining.</returns>
public static Activity SetGenAiUsage(
this Activity activity,
long? inputTokens = null,
long? outputTokens = null,
long? cachedTokens = null,
long? reasoningTokens = null)
{
ArgumentNullException.ThrowIfNull(activity);

if (inputTokens.HasValue)
activity.SetTag(SemanticConventions.GenAi.UsageInputTokens, inputTokens.Value);

if (outputTokens.HasValue)
activity.SetTag(SemanticConventions.GenAi.UsageOutputTokens, outputTokens.Value);

if (cachedTokens.HasValue)
activity.SetTag(SemanticConventions.GenAi.UsageInputTokensCached, cachedTokens.Value);

if (reasoningTokens.HasValue)
activity.SetTag(SemanticConventions.GenAi.UsageOutputTokensReasoning, reasoningTokens.Value);

return activity;
}

/// <summary>
/// Sets GenAI response attributes on the activity.
/// </summary>
/// <param name="activity">The activity to set tags on.</param>
/// <param name="model">The model that generated the response.</param>
/// <param name="responseId">Unique completion identifier.</param>
/// <param name="finishReasons">Reasons the model stopped generating.</param>
/// <returns>The activity for fluent chaining.</returns>
public static Activity SetGenAiResponse(
this Activity activity,
string? model = null,
string? responseId = null,
string[]? finishReasons = null)
{
ArgumentNullException.ThrowIfNull(activity);

if (model is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.ResponseModel, model);

if (responseId is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.ResponseId, responseId);

if (finishReasons is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.ResponseFinishReasons, finishReasons);

return activity;
}

/// <summary>
/// Sets GenAI agent attributes on the activity (OTel 1.38+).
/// </summary>
/// <param name="activity">The activity to set tags on.</param>
/// <param name="agentId">Unique agent identifier.</param>
/// <param name="agentName">Human-readable agent name.</param>
/// <param name="agentDescription">Agent description.</param>
/// <returns>The activity for fluent chaining.</returns>
public static Activity SetGenAiAgent(
this Activity activity,
string? agentId = null,
string? agentName = null,
string? agentDescription = null)
{
ArgumentNullException.ThrowIfNull(activity);

if (agentId is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.AgentId, agentId);

if (agentName is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.AgentName, agentName);

if (agentDescription is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.AgentDescription, agentDescription);

return activity;
}

/// <summary>
/// Sets GenAI tool attributes on the activity (OTel 1.39).
/// </summary>
/// <param name="activity">The activity to set tags on.</param>
/// <param name="toolName">Tool name.</param>
/// <param name="toolCallId">Tool call identifier.</param>
/// <param name="conversationId">Session/thread identifier for multi-turn conversations.</param>
/// <returns>The activity for fluent chaining.</returns>
public static Activity SetGenAiTool(
this Activity activity,
string? toolName = null,
string? toolCallId = null,
string? conversationId = null)
{
ArgumentNullException.ThrowIfNull(activity);

if (toolName is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.ToolName, toolName);

if (toolCallId is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.ToolCallId, toolCallId);

if (conversationId is { Length: > 0 })
activity.SetTag(SemanticConventions.GenAi.ConversationId, conversationId);

return activity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ public static TResponse Execute<TResponse>(

private static void SetRequestTags(Activity activity, string provider, string operation, string? model)
{
activity.SetTag(SemanticConventions.GenAi.System, provider);
// OTel 1.37+: gen_ai.system → gen_ai.provider.name
activity.SetTag(SemanticConventions.GenAi.ProviderName, provider);
activity.SetTag(SemanticConventions.GenAi.OperationName, operation);

if (model is { Length: > 0 })
Expand All @@ -108,11 +109,15 @@ private static void SetResponseTags<TResponse>(
try
{
var usage = extractUsage(response);
activity.SetTag(SemanticConventions.GenAi.InputTokens, usage.InputTokens);
activity.SetTag(SemanticConventions.GenAi.OutputTokens, usage.OutputTokens);
// OTel 1.37+: Explicit Usage prefix
activity.SetTag(SemanticConventions.GenAi.UsageInputTokens, usage.InputTokens);
activity.SetTag(SemanticConventions.GenAi.UsageOutputTokens, usage.OutputTokens);
}
catch
catch (Exception ex)
{
// Record extraction failure as event for debugging
activity.AddEvent(new ActivityEvent("gen_ai.usage.extraction_failed",
tags: new ActivityTagsCollection { ["exception.message"] = ex.Message }));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,131 @@ namespace ANcpSdk.AspNetCore.ServiceDefaults.Instrumentation;
internal static class SemanticConventions
{
/// <summary>
/// GenAI semantic conventions.
/// GenAI semantic conventions (OTel 1.39).
/// </summary>
/// <remarks>
/// See: https://opentelemetry.io/docs/specs/semconv/gen-ai/
/// </remarks>
public static class GenAi
{
/// <summary>The GenAI system (e.g., "openai", "anthropic", "ollama").</summary>
public const string System = "gen_ai.system";
/// <summary>Schema URL for OTel 1.39 semantic conventions.</summary>
public const string SchemaUrl = "https://opentelemetry.io/schemas/1.39.0";

/// <summary>The operation name (e.g., "chat", "embeddings").</summary>
// =====================================================================
// CORE ATTRIBUTES (Required/Conditionally Required)
// =====================================================================

/// <summary>gen_ai.provider.name - The GenAI provider (e.g., "openai", "anthropic").</summary>
/// <remarks>OTel 1.37+: Replaces deprecated gen_ai.system</remarks>
public const string ProviderName = "gen_ai.provider.name";

/// <summary>gen_ai.operation.name - The operation being performed (e.g., "chat", "embeddings").</summary>
public const string OperationName = "gen_ai.operation.name";

/// <summary>The model requested (e.g., "gpt-4o", "claude-3-opus").</summary>
// =====================================================================
// REQUEST PARAMETERS
// =====================================================================

/// <summary>gen_ai.request.model - The model requested (e.g., "gpt-4o", "claude-3-opus").</summary>
public const string RequestModel = "gen_ai.request.model";

/// <summary>The model that generated the response.</summary>
/// <summary>gen_ai.request.temperature - Temperature setting (0.0-2.0).</summary>
public const string RequestTemperature = "gen_ai.request.temperature";

/// <summary>gen_ai.request.max_tokens - Maximum tokens to generate.</summary>
public const string RequestMaxTokens = "gen_ai.request.max_tokens";

/// <summary>gen_ai.request.top_p - Nucleus sampling threshold.</summary>
public const string RequestTopP = "gen_ai.request.top_p";

/// <summary>gen_ai.request.top_k - Top-k sampling parameter.</summary>
public const string RequestTopK = "gen_ai.request.top_k";

/// <summary>gen_ai.request.stop_sequences - Stop sequence array.</summary>
public const string RequestStopSequences = "gen_ai.request.stop_sequences";

/// <summary>gen_ai.request.frequency_penalty - Frequency penalty (-2.0 to 2.0).</summary>
public const string RequestFrequencyPenalty = "gen_ai.request.frequency_penalty";

/// <summary>gen_ai.request.presence_penalty - Presence penalty (-2.0 to 2.0).</summary>
public const string RequestPresencePenalty = "gen_ai.request.presence_penalty";

/// <summary>gen_ai.request.seed - Reproducibility seed.</summary>
public const string RequestSeed = "gen_ai.request.seed";

// =====================================================================
// RESPONSE ATTRIBUTES
// =====================================================================

/// <summary>gen_ai.response.model - The model that generated the response.</summary>
public const string ResponseModel = "gen_ai.response.model";

/// <summary>Number of tokens in the input/prompt.</summary>
public const string InputTokens = "gen_ai.usage.input_tokens";
/// <summary>gen_ai.response.id - Unique completion identifier.</summary>
public const string ResponseId = "gen_ai.response.id";

/// <summary>gen_ai.response.finish_reasons - Reasons the model stopped generating.</summary>
public const string ResponseFinishReasons = "gen_ai.response.finish_reasons";

// =====================================================================
// USAGE/TOKENS (OTel 1.37+ naming)
// =====================================================================

/// <summary>gen_ai.usage.input_tokens - Number of tokens in the input/prompt.</summary>
public const string UsageInputTokens = "gen_ai.usage.input_tokens";

/// <summary>gen_ai.usage.output_tokens - Number of tokens in the output/completion.</summary>
public const string UsageOutputTokens = "gen_ai.usage.output_tokens";

/// <summary>gen_ai.usage.input_tokens.cached - Cached input tokens (Anthropic).</summary>
public const string UsageInputTokensCached = "gen_ai.usage.input_tokens.cached";

/// <summary>gen_ai.usage.output_tokens.reasoning - Reasoning output tokens (o1-style models).</summary>
public const string UsageOutputTokensReasoning = "gen_ai.usage.output_tokens.reasoning";

// =====================================================================
// AGENT ATTRIBUTES (OTel 1.38+)
// =====================================================================

/// <summary>gen_ai.agent.id - Unique agent identifier.</summary>
public const string AgentId = "gen_ai.agent.id";

/// <summary>gen_ai.agent.name - Human-readable agent name.</summary>
public const string AgentName = "gen_ai.agent.name";

/// <summary>gen_ai.agent.description - Agent description.</summary>
public const string AgentDescription = "gen_ai.agent.description";

// =====================================================================
// TOOL ATTRIBUTES (OTel 1.39)
// =====================================================================

/// <summary>gen_ai.tool.name - Tool name.</summary>
public const string ToolName = "gen_ai.tool.name";

/// <summary>gen_ai.tool.call.id - Tool call identifier.</summary>
public const string ToolCallId = "gen_ai.tool.call.id";

/// <summary>gen_ai.conversation.id - Session/thread identifier.</summary>
public const string ConversationId = "gen_ai.conversation.id";
Comment on lines +113 to +117

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The semantic conventions gen_ai.tool.call.id and gen_ai.conversation.id are not part of the official OTel 1.39 GenAI specification.

According to the OTel GenAI Semantic Conventions v1.39.0:

  • For tool calls, the spec defines gen_ai.tool.call.function.name and gen_ai.tool.call.function.arguments, but not gen_ai.tool.call.id.
  • gen_ai.conversation.id is also not a standard attribute.

To ensure compliance with the OTel specification, I recommend removing these non-standard attributes. If they are required for custom use cases, they should be named outside the gen_ai. namespace to avoid confusion.


// =====================================================================
// DEPRECATED (for backward compatibility)
// =====================================================================

/// <summary>
/// Deprecated attribute names (pre-1.37). Use for migration/normalization.
/// </summary>
public static class Deprecated
{
/// <summary>gen_ai.system - DEPRECATED: Use ProviderName instead.</summary>
public const string System = "gen_ai.system";

/// <summary>Number of tokens in the output/completion.</summary>
public const string OutputTokens = "gen_ai.usage.output_tokens";
/// <summary>gen_ai.usage.prompt_tokens - DEPRECATED: Use UsageInputTokens instead.</summary>
public const string UsagePromptTokens = "gen_ai.usage.prompt_tokens";

/// <summary>Reasons the model stopped generating (e.g., "stop", "length").</summary>
public const string FinishReasons = "gen_ai.response.finish_reasons";
/// <summary>gen_ai.usage.completion_tokens - DEPRECATED: Use UsageOutputTokens instead.</summary>
public const string UsageCompletionTokens = "gen_ai.usage.completion_tokens";
}
}

/// <summary>
Expand Down