Add observability to codebase#23
Conversation
Add full-featured observability to OpenRouter.NET using OpenTelemetry,
enabling tracking of LLM API calls, token usage, performance metrics,
and tool execution with platforms like Arize Phoenix, Jaeger, or any
OTLP-compatible backend.
## Key Features
- **Opt-in telemetry**: All telemetry disabled by default for privacy
- **Request/Response tracking**: Capture prompts, completions, raw JSON
- **Token usage**: Track input/output tokens for cost monitoring
- **Streaming metrics**: TTFT, tokens/sec, chunk count, duration
- **Tool execution spans**: Child spans for tool calls with args/results
- **Error tracking**: Automatic exception recording with stack traces
- **Privacy controls**: Sanitization callbacks for PII redaction
- **Zero overhead**: No performance impact when disabled
- **Standards compliant**: Follows OpenTelemetry GenAI semantic conventions
## Implementation Details
### New Components
- `OpenRouterActivitySource`: Singleton ActivitySource for tracing
- `GenAiSemanticConventions`: Constants for OTel GenAI attributes
- `OpenRouterTelemetryOptions`: Comprehensive opt-in configuration
- `TelemetryHelper`: Enricher methods for spans and events
- `OpenTelemetryExtensions`: Extension method for easy setup
### Instrumented Classes
- `OpenRouterClient.CreateChatCompletionAsync()`: Chat completion spans
- `StreamingHandler`: Streaming metrics (TTFT, throughput)
- `ToolManager.ExecuteTool()`: Tool execution child spans
- `ObjectGenerator.GenerateObjectAsync()`: Schema validation tracking
- `HttpRequestHandler`: HTTP-level attributes
### Configuration Options
- `EnableTelemetry`: Master switch (default: false)
- `CaptureRawRequests/Responses`: Log full HTTP payloads
- `CapturePrompts/Completions`: Log structured messages
- `CaptureStreamChunks`: Log individual chunks (high volume)
- `CaptureToolDetails`: Log tool arguments and results
- `MaxEventBodySize`: Size limit before truncation (32KB default)
- `SanitizePrompt/Completion/ToolArguments`: PII redaction callbacks
### Presets
- `OpenRouterTelemetryOptions.Default`: All disabled (opt-in)
- `OpenRouterTelemetryOptions.Production`: Metrics only, no content
- `OpenRouterTelemetryOptions.Debug`: Full capture for debugging
- `OpenRouterTelemetryOptions.FullTelemetry`: Everything enabled
## Documentation
- Comprehensive observability guide: `docs/OBSERVABILITY.md`
- Setup examples for Arize Phoenix, Jaeger, console exporter
- Privacy best practices and PII sanitization examples
- ASP.NET Core integration examples
- Performance impact details and sampling strategies
## Dependencies Added
- OpenTelemetry.Api 1.10.0 (lightweight, core instrumentation)
- OpenTelemetry 1.10.0 (for TracerProviderBuilder extension)
- System.Diagnostics.DiagnosticSource 9.0.0 (Activity support)
## Usage Example
```csharp
// Configure OpenTelemetry
var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddOpenRouterInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://localhost:4317");
})
.Build();
// Enable telemetry in client
var client = new OpenRouterClient(new OpenRouterClientOptions
{
ApiKey = "your-api-key",
Telemetry = new OpenRouterTelemetryOptions
{
EnableTelemetry = true,
CapturePrompts = true,
CaptureCompletions = true,
CaptureToolDetails = true
}
});
// All API calls are now automatically traced!
```
## Breaking Changes
None - all changes are additive and opt-in by default.
Resolves: #[issue-number] (if applicable)
WalkthroughAdds OpenTelemetry-based observability: new telemetry types, ActivitySource, semantic conventions, helpers and extensions; injects telemetry into OpenRouterClient and internal components, updates constructors to accept telemetry options, adds NuGet telemetry dependencies, and expands docs and samples with observability guides and examples. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App
participant Client as OpenRouterClient
participant ActSrc as OpenRouterActivitySource
participant Http as HttpClient
participant API as OpenRouter API
App->>Client: CreateChatCompletionAsync(request)
Note over Client,ActSrc `#E8F5E9`: If telemetry enabled → Start "gen_ai.client.chat" activity
Client->>ActSrc: StartActivity() + add request attrs
Client->>Http: POST /chat (serialized request)
activate Http
Http-->>Client: HTTP response
deactivate Http
Client->>ActSrc: EnrichWithHttp + EnrichWithResponse
alt Success
Client-->>App: ChatCompletionResponse
else Error
Client->>ActSrc: RecordException(), set status
Client-->>App: throw error
end
Note over Client,ActSrc `#E8F5E9`: Stop Activity
sequenceDiagram
autonumber
participant Client
participant ToolMgr as ToolManager
participant ActSrc as OpenRouterActivitySource
participant Tool as ExternalTool
Client->>ToolMgr: ExecuteTool(name, args)
Note over ToolMgr,ActSrc `#FFF3E0`: If telemetry enabled → Start "gen_ai.client.tool" activity
ToolMgr->>ActSrc: Tag tool name/mode/sanitized args
ToolMgr->>Tool: Invoke(args)
Tool-->>ToolMgr: Result / Error
alt Success
ToolMgr->>ActSrc: Enrich result, mark success
ToolMgr-->>Client: Result
else Failure
ToolMgr->>ActSrc: RecordException(), mark failure
ToolMgr-->>Client: throw
end
Note over ToolMgr,ActSrc `#FFF3E0`: Stop Activity
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (1)
84-161: Clamp retries before usinglastExceptionIf
GenerateObjectOptions.MaxRetriesis 0 (a valid public configuration), the loop never runs,lastExceptionstays null, and the subsequentTelemetryHelper.RecordException(activity, lastException!)will throw aNullReferenceExceptionbefore we emit the intendedOpenRouterException. We also never even attempt a single call. Please ensure we always execute at least one attempt (e.g., clamp toMath.Max(1, options.MaxRetries)and reuse that value for logging/throw) and only record telemetry when there is an exception instance.Apply this diff:
- options ??= new GenerateObjectOptions(); - - var schemaJson = schema.GetRawText(); + options ??= new GenerateObjectOptions(); + var maxRetries = Math.Max(1, options.MaxRetries); + + var schemaJson = schema.GetRawText(); @@ - for (int attempt = 0; attempt < options.MaxRetries; attempt++) + for (int attempt = 0; attempt < maxRetries; attempt++) @@ - Log($"GenerateObject attempt {attempt + 1}/{options.MaxRetries}"); + Log($"GenerateObject attempt {attempt + 1}/{maxRetries}"); @@ - activity.SetTag(GenAiSemanticConventions.AttributeValidationSuccess, false); - TelemetryHelper.RecordException(activity, lastException!); + activity.SetTag(GenAiSemanticConventions.AttributeValidationSuccess, false); + if (lastException != null) + { + TelemetryHelper.RecordException(activity, lastException); + } } throw new OpenRouterException( - $"Failed to generate structured object after {options.MaxRetries} attempts. Last error: {lastException?.Message}", - lastException!); + $"Failed to generate structured object after {maxRetries} attempts. Last error: {lastException?.Message}", + lastException ?? new OpenRouterException("No generation attempts were executed because MaxRetries was less than 1."));
🧹 Nitpick comments (7)
docs/OBSERVABILITY.md (1)
235-245: Add a language hint to the fenced blockmarkdownlint (MD040) is flagging the “Example Trace Hierarchy” block because it lacks a language identifier. Please add something like ```text (or another appropriate label) to keep the ASCII tree while satisfying the linter.
packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (6)
21-35: Complete request/response attributes (optional).Consider adding seed, choice count, and encoding formats to cover common knobs. (opentelemetry.io)
public const string AttributeRequestPresencePenalty = "gen_ai.request.presence_penalty"; public const string AttributeRequestSizeBytes = "gen_ai.request.size_bytes"; + public const string AttributeRequestSeed = "gen_ai.request.seed"; + public const string AttributeRequestChoiceCount = "gen_ai.request.choice.count"; + public const string AttributeRequestEncodingFormats = "gen_ai.request.encoding_formats";
40-45: HTTP attributes look good; add server.port for completeness.server.port is commonly recommended alongside server.address. (opentelemetry.io)
public const string AttributeHttpRetryAfter = "http.response.retry_after"; + public const string AttributeServerPort = "server.port";
47-52: Streaming fields are custom; prefer metrics for TTFT.Spec models TTFT as metric gen_ai.server.time_to_first_token (seconds). Keep attributes if useful, but also emit the metric and use seconds for consistency. (opentelemetry.io)
63-68: Non‑standard schema/validation keys—prefix as extension or document.Either prefix with openrouter.* or document as extensions to avoid confusion with spec keys. (opentelemetry.netlify.app)
76-80: Event attribute names OK; consider adding input/output messages.Add gen_ai.input.messages and gen_ai.output.messages for structured capture when enabled. (opentelemetry.io)
public const string EventAttributeRawResponse = "gen_ai.response.raw_json"; + public const string EventAttributeInputMessages = "gen_ai.input.messages"; + public const string EventAttributeOutputMessages = "gen_ai.output.messages";
7-92: Visibility nit.Class is internal, so public const fields are effectively internal. For clarity, consider making them internal const.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
README.md(2 hunks)docs/OBSERVABILITY.md(1 hunks)packages/dotnet-sdk/OpenRouter.NET.csproj(1 hunks)packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs(2 hunks)packages/dotnet-sdk/src/Internal/ObjectGenerator.cs(6 hunks)packages/dotnet-sdk/src/Internal/StreamingHandler.cs(2 hunks)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs(1 hunks)packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs(1 hunks)packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs(1 hunks)packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs(1 hunks)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs(1 hunks)packages/dotnet-sdk/src/OpenRouterClient.cs(5 hunks)packages/dotnet-sdk/src/OpenRouterClientOptions.cs(2 hunks)packages/dotnet-sdk/src/Tools/ToolManager.cs(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (3)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2)
HttpRequestHandler(11-108)HttpRequestHandler(21-37)packages/dotnet-sdk/src/Tools/ToolManager.cs (2)
ToolManager(11-174)ToolManager(19-23)
packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (6)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/OpenRouterClient.cs (7)
Task(99-193)Task(205-228)Task(230-260)Task(262-292)Task(315-323)Task(328-335)Log(337-340)packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
OpenRouterActivitySource(9-26)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Models/GenerateObjectModels.cs (1)
GenerateObjectOptions(42-48)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (2)
TelemetryHelper(11-314)RecordException(214-226)
packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs (1)
packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
OpenRouterActivitySource(9-26)
packages/dotnet-sdk/src/Tools/ToolManager.cs (4)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
OpenRouterActivitySource(9-26)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (2)
TelemetryHelper(11-314)RecordException(214-226)
packages/dotnet-sdk/src/OpenRouterClient.cs (9)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Tools/ToolManager.cs (2)
ToolManager(11-174)ToolManager(19-23)packages/dotnet-sdk/src/Internal/StreamingHandler.cs (3)
StreamingHandler(15-596)StreamingHandler(22-32)Log(592-595)packages/dotnet-sdk/src/Internal/ObjectGenerator.cs (3)
ObjectGenerator(15-265)ObjectGenerator(23-33)Log(261-264)packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
OpenRouterActivitySource(9-26)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (5)
TelemetryHelper(11-314)EnrichWithRequest(16-88)EnrichWithHttp(168-185)EnrichWithResponse(93-163)RecordException(214-226)packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (1)
AddHeaders(42-56)packages/dotnet-sdk/src/Exceptions.cs (4)
OpenRouterException(3-21)OpenRouterException(8-10)OpenRouterException(12-14)OpenRouterException(16-20)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (4)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Models/ChatCompletionResponse.cs (1)
ChatCompletionResponse(5-27)packages/dotnet-sdk/src/Models/Message.cs (1)
ContentPart(48-52)
packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (1)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)
🪛 markdownlint-cli2 (0.18.1)
docs/OBSERVABILITY.md
107-107: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
| namespace OpenRouter.NET.Observability; | ||
|
|
||
| /// <summary> | ||
| /// OpenTelemetry semantic conventions for Generative AI operations. | ||
| /// Based on: https://opentelemetry.io/docs/specs/semconv/gen-ai/ | ||
| /// </summary> | ||
| internal static class GenAiSemanticConventions | ||
| { | ||
| // ========== Span Names ========== | ||
| public const string SpanNameChat = "gen_ai.client.chat"; | ||
| public const string SpanNameStream = "gen_ai.client.stream"; | ||
| public const string SpanNameGenerateObject = "gen_ai.client.generate_object"; | ||
| public const string SpanNameToolCall = "gen_ai.tool.call"; | ||
| public const string SpanNameHttp = "http"; | ||
|
|
||
| // ========== General Attributes ========== | ||
| public const string AttributeSystem = "gen_ai.system"; | ||
| public const string AttributeOperationName = "gen_ai.operation.name"; | ||
| public const string AttributeServerAddress = "server.address"; | ||
|
|
||
| // ========== Request Attributes ========== | ||
| public const string AttributeRequestModel = "gen_ai.request.model"; | ||
| public const string AttributeRequestMaxTokens = "gen_ai.request.max_tokens"; | ||
| public const string AttributeRequestTemperature = "gen_ai.request.temperature"; | ||
| public const string AttributeRequestTopP = "gen_ai.request.top_p"; | ||
| public const string AttributeRequestFrequencyPenalty = "gen_ai.request.frequency_penalty"; | ||
| public const string AttributeRequestPresencePenalty = "gen_ai.request.presence_penalty"; | ||
| public const string AttributeRequestSizeBytes = "gen_ai.request.size_bytes"; | ||
|
|
||
| // ========== Response Attributes ========== | ||
| public const string AttributeResponseId = "gen_ai.response.id"; | ||
| public const string AttributeResponseModel = "gen_ai.response.model"; | ||
| public const string AttributeResponseFinishReasons = "gen_ai.response.finish_reasons"; | ||
| public const string AttributeResponseSizeBytes = "gen_ai.response.size_bytes"; | ||
|
|
||
| // ========== Usage/Token Attributes ========== | ||
| public const string AttributeUsageInputTokens = "gen_ai.usage.input_tokens"; | ||
| public const string AttributeUsageOutputTokens = "gen_ai.usage.output_tokens"; | ||
|
|
||
| // ========== HTTP Attributes ========== | ||
| public const string AttributeHttpStatusCode = "http.response.status_code"; | ||
| public const string AttributeHttpMethod = "http.request.method"; | ||
| public const string AttributeHttpUrl = "url.full"; | ||
| public const string AttributeHttpRetryAfter = "http.response.retry_after"; | ||
|
|
||
| // ========== Streaming Attributes ========== | ||
| public const string AttributeStreamTimeToFirstToken = "gen_ai.stream.time_to_first_token_ms"; | ||
| public const string AttributeStreamTotalChunks = "gen_ai.stream.total_chunks"; | ||
| public const string AttributeStreamDuration = "gen_ai.stream.duration_ms"; | ||
| public const string AttributeStreamTokensPerSecond = "gen_ai.stream.tokens_per_second"; | ||
| public const string AttributeStreamFinishReason = "gen_ai.stream.finish_reason"; | ||
|
|
||
| // ========== Tool Attributes ========== | ||
| public const string AttributeToolName = "gen_ai.tool.name"; | ||
| public const string AttributeToolId = "gen_ai.tool.id"; | ||
| public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; | ||
| public const string AttributeToolArguments = "gen_ai.tool.arguments"; | ||
| public const string AttributeToolResult = "gen_ai.tool.result"; | ||
| public const string AttributeToolError = "gen_ai.tool.error"; | ||
| public const string AttributeToolDuration = "gen_ai.tool.duration_ms"; | ||
| public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; | ||
|
|
||
| // ========== Schema/Validation Attributes ========== | ||
| public const string AttributeSchemaSizeBytes = "gen_ai.schema.size_bytes"; | ||
| public const string AttributeValidationAttempt = "gen_ai.validation.attempt"; | ||
| public const string AttributeValidationSuccess = "gen_ai.validation.success"; | ||
| public const string AttributeValidationError = "gen_ai.validation.error"; | ||
|
|
||
| // ========== Event Names ========== | ||
| public const string EventPrompt = "gen_ai.client.input"; | ||
| public const string EventCompletion = "gen_ai.client.output"; | ||
| public const string EventStreamChunk = "gen_ai.client.stream.chunk"; | ||
| public const string EventException = "exception"; | ||
|
|
||
| // ========== Event Attribute Names ========== | ||
| public const string EventAttributePrompt = "gen_ai.prompt"; | ||
| public const string EventAttributeCompletion = "gen_ai.completion"; | ||
| public const string EventAttributeRawRequest = "gen_ai.request.raw_json"; | ||
| public const string EventAttributeRawResponse = "gen_ai.response.raw_json"; | ||
|
|
||
| // ========== Exception Attributes ========== | ||
| public const string AttributeExceptionType = "exception.type"; | ||
| public const string AttributeExceptionMessage = "exception.message"; | ||
| public const string AttributeExceptionStacktrace = "exception.stacktrace"; | ||
|
|
||
| // ========== Constant Values ========== | ||
| public const string SystemValue = "openrouter"; | ||
| public const string OperationChat = "chat"; | ||
| public const string OperationStream = "stream"; | ||
| public const string OperationGenerateObject = "generate_object"; | ||
| public const string ServerAddressValue = "openrouter.ai"; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Repo‑wide verification.
Search and update call sites for renamed keys/events/span names.
Run:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find non‑standard or deprecated keys/usages to update
rg -n --hidden -S -e 'gen_ai\.client\.input|gen_ai\.client\.output|gen_ai\.tool\.arguments|gen_ai\.tool\.result|gen_ai\.tool\.id|SpanNameToolCall|SpanNameChat|SpanNameStream|OperationStream|OperationGenerateObject'
# Check presence of required provider attribute usage
rg -n --hidden -S -e 'gen_ai\.provider\.name'Length of output: 2299
Add missing gen_ai.provider.name semantic attribute and set it on all GenAI spans
The OpenTelemetry GenAI spec mandates gen_ai.provider.name on every GenAI span (chat, stream, generate_object, tool calls) (opentelemetry.io). Define
public const string AttributeProviderName = "gen_ai.provider.name";in GenAiSemanticConventions.cs and update OpenRouterClient.cs, ToolManager.cs, and ObjectGenerator.cs to set this tag to the appropriate provider identifier (e.g. "openrouter").
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 1-92, add a new constant string AttributeProviderName =
"gen_ai.provider.name"; then in packages/dotnet-sdk/src/OpenRouterClient.cs,
ToolManager.cs, and ObjectGenerator.cs update the GenAI-related span creation
logic to set the attribute "gen_ai.provider.name" to the provider identifier
(e.g. "openrouter") on every GenAI span (chat, stream, generate_object,
tool.call) so the provider name is included per the OpenTelemetry GenAI semantic
conventions.
| public const string SpanNameChat = "gen_ai.client.chat"; | ||
| public const string SpanNameStream = "gen_ai.client.stream"; | ||
| public const string SpanNameGenerateObject = "gen_ai.client.generate_object"; | ||
| public const string SpanNameToolCall = "gen_ai.tool.call"; | ||
| public const string SpanNameHttp = "http"; | ||
|
|
There was a problem hiding this comment.
Align span naming with OTel guidance; avoid fixed names.
Client span name SHOULD be "{gen_ai.operation.name} {gen_ai.request.model}". Tool span SHOULD be "execute_tool {gen_ai.tool.name}". Fixed names like "gen_ai.client.chat" and "gen_ai.tool.call" deviate. Replace with a formatter in TelemetryHelper and drop these constants. (opentelemetry.io)
Apply:
- public const string SpanNameChat = "gen_ai.client.chat";
- public const string SpanNameStream = "gen_ai.client.stream";
- public const string SpanNameGenerateObject = "gen_ai.client.generate_object";
- public const string SpanNameToolCall = "gen_ai.tool.call";
- public const string SpanNameHttp = "http";
+ // Span names should be constructed dynamically:
+ // "{gen_ai.operation.name} {gen_ai.request.model}" and "execute_tool {gen_ai.tool.name}"
+ // Use TelemetryHelper to format; no fixed constants needed.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public const string SpanNameChat = "gen_ai.client.chat"; | |
| public const string SpanNameStream = "gen_ai.client.stream"; | |
| public const string SpanNameGenerateObject = "gen_ai.client.generate_object"; | |
| public const string SpanNameToolCall = "gen_ai.tool.call"; | |
| public const string SpanNameHttp = "http"; | |
| - public const string SpanNameChat = "gen_ai.client.chat"; | |
| - public const string SpanNameStream = "gen_ai.client.stream"; | |
| - public const string SpanNameGenerateObject = "gen_ai.client.generate_object"; | |
| - public const string SpanNameToolCall = "gen_ai.tool.call"; | |
| // Span names should be constructed dynamically: | |
| // "{gen_ai.operation.name} {gen_ai.request.model}" | |
| // and for tools: | |
| // "execute_tool {gen_ai.tool.name}" | |
| // Use TelemetryHelper to format these at runtime instead of fixed constants. |
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 10-15, the file currently defines fixed span name constants (SpanNameChat,
SpanNameStream, SpanNameGenerateObject, SpanNameToolCall, SpanNameHttp) which
conflict with OTel guidance; remove these fixed constants and instead implement
span-name formatting in TelemetryHelper so client spans are generated as
"{gen_ai.operation.name} {gen_ai.request.model}" and tool spans as "execute_tool
{gen_ai.tool.name}". Update TelemetryHelper to expose methods that accept
operation name, request model, and tool name and return the correctly formatted
span names, and refactor call sites to use those formatter methods rather than
the removed constants.
| // ========== General Attributes ========== | ||
| public const string AttributeSystem = "gen_ai.system"; | ||
| public const string AttributeOperationName = "gen_ai.operation.name"; | ||
| public const string AttributeServerAddress = "server.address"; | ||
|
|
||
| // ========== Request Attributes ========== |
There was a problem hiding this comment.
Add required provider attribute.
gen_ai.provider.name is Required for GenAI client spans. Add a constant. (opentelemetry.io)
public const string AttributeSystem = "gen_ai.system";
public const string AttributeOperationName = "gen_ai.operation.name";
public const string AttributeServerAddress = "server.address";
+ public const string AttributeProviderName = "gen_ai.provider.name";🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 16 to 21, the OpenTelemetry GenAI required provider attribute is missing;
add a new public const string for "gen_ai.provider.name" (e.g., public const
string AttributeProviderName = "gen_ai.provider.name";) alongside the other
general attributes so spans include the required provider attribute.
| // ========== Tool Attributes ========== | ||
| public const string AttributeToolName = "gen_ai.tool.name"; | ||
| public const string AttributeToolId = "gen_ai.tool.id"; | ||
| public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; | ||
| public const string AttributeToolArguments = "gen_ai.tool.arguments"; | ||
| public const string AttributeToolResult = "gen_ai.tool.result"; | ||
| public const string AttributeToolError = "gen_ai.tool.error"; | ||
| public const string AttributeToolDuration = "gen_ai.tool.duration_ms"; | ||
| public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; | ||
|
|
There was a problem hiding this comment.
Use spec tool call keys and standard error.
Rename to gen_ai.tool.call.arguments/result/id and use error.type for failures. Execution duration should be the span duration, not a custom tag. (opentelemetry.io)
- public const string AttributeToolName = "gen_ai.tool.name";
- public const string AttributeToolId = "gen_ai.tool.id";
- public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode";
- public const string AttributeToolArguments = "gen_ai.tool.arguments";
- public const string AttributeToolResult = "gen_ai.tool.result";
- public const string AttributeToolError = "gen_ai.tool.error";
- public const string AttributeToolDuration = "gen_ai.tool.duration_ms";
- public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration";
+ public const string AttributeToolName = "gen_ai.tool.name";
+ public const string AttributeToolCallId = "gen_ai.tool.call.id";
+ public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; // extension
+ public const string AttributeToolCallArguments = "gen_ai.tool.call.arguments";
+ public const string AttributeToolCallResult = "gen_ai.tool.call.result";
+ public const string AttributeErrorType = "error.type";
+ // Prefer span duration over a duration attribute
+ public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; // extension📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ========== Tool Attributes ========== | |
| public const string AttributeToolName = "gen_ai.tool.name"; | |
| public const string AttributeToolId = "gen_ai.tool.id"; | |
| public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; | |
| public const string AttributeToolArguments = "gen_ai.tool.arguments"; | |
| public const string AttributeToolResult = "gen_ai.tool.result"; | |
| public const string AttributeToolError = "gen_ai.tool.error"; | |
| public const string AttributeToolDuration = "gen_ai.tool.duration_ms"; | |
| public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; | |
| // ========== Tool Attributes ========== | |
| public const string AttributeToolName = "gen_ai.tool.name"; | |
| public const string AttributeToolCallId = "gen_ai.tool.call.id"; | |
| public const string AttributeToolExecutionMode = "gen_ai.tool.execution_mode"; // extension | |
| public const string AttributeToolCallArguments = "gen_ai.tool.call.arguments"; | |
| public const string AttributeToolCallResult = "gen_ai.tool.call.result"; | |
| public const string AttributeErrorType = "error.type"; | |
| // Prefer span duration over a duration attribute | |
| public const string AttributeToolLoopIteration = "gen_ai.tool.loop_iteration"; // extension |
| // ========== Event Names ========== | ||
| public const string EventPrompt = "gen_ai.client.input"; | ||
| public const string EventCompletion = "gen_ai.client.output"; | ||
| public const string EventStreamChunk = "gen_ai.client.stream.chunk"; | ||
| public const string EventException = "exception"; | ||
|
|
There was a problem hiding this comment.
Use standard content event names.
Rename events to gen_ai.content.prompt and gen_ai.content.completion. Keep exception as is. (hexdocs.pm)
- public const string EventPrompt = "gen_ai.client.input";
- public const string EventCompletion = "gen_ai.client.output";
+ public const string EventPrompt = "gen_ai.content.prompt";
+ public const string EventCompletion = "gen_ai.content.completion";
public const string EventStreamChunk = "gen_ai.client.stream.chunk";
public const string EventException = "exception";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ========== Event Names ========== | |
| public const string EventPrompt = "gen_ai.client.input"; | |
| public const string EventCompletion = "gen_ai.client.output"; | |
| public const string EventStreamChunk = "gen_ai.client.stream.chunk"; | |
| public const string EventException = "exception"; | |
| // ========== Event Names ========== | |
| public const string EventPrompt = "gen_ai.content.prompt"; | |
| public const string EventCompletion = "gen_ai.content.completion"; | |
| public const string EventStreamChunk = "gen_ai.client.stream.chunk"; | |
| public const string EventException = "exception"; |
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 69 to 74, the event constant names and values should follow the standard
content event names; rename the two constants so their identifiers and string
values reflect "gen_ai.content.prompt" and "gen_ai.content.completion" (keep the
EventException constant and its value "exception" unchanged). Update any
references in the project to use the new constant names if you change the
identifiers, or if you only change the string values, replace the literal values
accordingly so the exported semantic convention matches the referenced spec.
| // ========== Constant Values ========== | ||
| public const string SystemValue = "openrouter"; | ||
| public const string OperationChat = "chat"; | ||
| public const string OperationStream = "stream"; | ||
| public const string OperationGenerateObject = "generate_object"; | ||
| public const string ServerAddressValue = "openrouter.ai"; | ||
| } |
There was a problem hiding this comment.
Normalize operation names.
"stream" and "generate_object" aren’t standard operation names; prefer chat/generate_content/etc., and use events/metrics to indicate streaming or JSON output. Also add execute_tool/invoke_agent constants if used. (opentelemetry.io)
public const string SystemValue = "openrouter";
public const string OperationChat = "chat";
- public const string OperationStream = "stream";
- public const string OperationGenerateObject = "generate_object";
+ public const string OperationGenerateContent = "generate_content";
+ public const string OperationExecuteTool = "execute_tool";
+ public const string OperationInvokeAgent = "invoke_agent";
public const string ServerAddressValue = "openrouter.ai";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ========== Constant Values ========== | |
| public const string SystemValue = "openrouter"; | |
| public const string OperationChat = "chat"; | |
| public const string OperationStream = "stream"; | |
| public const string OperationGenerateObject = "generate_object"; | |
| public const string ServerAddressValue = "openrouter.ai"; | |
| } | |
| // ========== Constant Values ========== | |
| public const string SystemValue = "openrouter"; | |
| public const string OperationChat = "chat"; | |
| public const string OperationGenerateContent = "generate_content"; | |
| public const string OperationExecuteTool = "execute_tool"; | |
| public const string OperationInvokeAgent = "invoke_agent"; | |
| public const string ServerAddressValue = "openrouter.ai"; | |
| } |
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs around
lines 86 to 92, normalize the operation name constants to follow the Gen‑AI
semantic conventions: rename OperationStream to OperationGenerateContent (or
OperationGenerate if your project prefers "generate_content") and rename
OperationGenerateObject to OperationGenerateContent/Generate as appropriate so
operation names use chat/generate_content conventions; keep OperationChat as
"chat"; add new public const string OperationExecuteTool = "execute_tool" and
public const string OperationInvokeAgent = "invoke_agent" if those actions are
used in the codebase, and update any references throughout the code to use the
new constant names. Ensure the string values match the OpenTelemetry spec (e.g.,
"generate_content", "execute_tool", "invoke_agent") and run a find/replace to
update usages.
Add comprehensive OpenTelemetry observability to the streaming web API sample,
demonstrating real-world usage of the observability features with console exporter.
## Changes
### OpenTelemetry Setup (Program.cs)
- Added OpenTelemetry with service name "openrouter-streaming-api"
- Configured ASP.NET Core instrumentation (HTTP requests)
- Configured HttpClient instrumentation (outgoing HTTP)
- Added OpenRouter instrumentation with .AddOpenRouterInstrumentation()
- Using ConsoleExporter for easy local debugging
### Telemetry-Enabled Endpoints
**`/api/stream`** - Main streaming chat
- Full telemetry: prompts, completions, tool details, stream chunks
- CaptureStreamChunks = true (demonstrates high-volume logging)
**`/api/dashboard/stream`** - Dashboard builder
- Full telemetry: prompts, completions, tool details
**`/api/generate-object`** - Structured output
- Telemetry: prompts, completions
- Tracks schema validation
**`/api/triage-bug`** - Bug analysis
- Telemetry: prompts, completions
**`/api/demo-typed-tools`** - Typed tools demo
- Full telemetry: prompts, completions, tool details
### Documentation
Created OBSERVABILITY.md with:
- What's being logged and where
- How to view traces in console output
- Switching to Phoenix/Jaeger instructions
- Configuration options and presets
- Performance impact notes
- Privacy considerations
- Example trace output
### Dependencies Added
- OpenTelemetry 1.10.0
- OpenTelemetry.Exporter.Console 1.10.0
- OpenTelemetry.Extensions.Hosting 1.10.0
- OpenTelemetry.Instrumentation.AspNetCore 1.10.0
## Usage
```bash
export OPENROUTER_API_KEY="your-key"
cd samples/StreamingWebApiSample
dotnet run
# Make request, see traces in console
curl -X POST http://localhost:5000/api/stream \
-H "Content-Type: application/json" \
-d '{"message":"Calculate 15 * 23"}'
```
## Notes
- Console output shows full OpenTelemetry traces with all attributes and events
- /api/stream has CaptureStreamChunks enabled (high volume) for demonstration
- Easy to switch to Phoenix by changing exporter to OTLP
- All clients use opt-in telemetry configuration
Add OpenTelemetry.Instrumentation.Http 1.10.0 package to resolve build error. The AddHttpClientInstrumentation() extension method requires this package.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
samples/StreamingWebApiSample/OBSERVABILITY.md (2)
76-103: Specify language identifier for code block.The code block showing telemetry output lacks a language identifier, which violates markdown linting standards and prevents syntax highlighting.
Apply this diff to add the language identifier:
-``` +```text Activity.TraceId: 8a1f3c2e9b7d4a5f6e8c9b0a1d2e3f4g Activity.SpanId: 7b6c5d4e3f2a
156-156: Wrap bare URLs in angle brackets or format as markdown links.Bare URLs at lines 156, 167, 237, and 238 should be either wrapped in angle brackets (
<url>) or formatted as markdown links ([text](url)) to comply with markdown standards.Apply these diffs:
4. **View traces**: Open http://localhost:6006 +4. **View traces**: Open <http://localhost:6006>3. **View traces**: Open http://localhost:16686 +3. **View traces**: Open <http://localhost:16686>-## Resources - -- **Full Guide**: [../../docs/OBSERVABILITY.md](../../docs/OBSERVABILITY.md) -- **Arize Phoenix**: https://docs.arize.com/phoenix -- **OpenTelemetry .NET**: https://opentelemetry.io/docs/languages/dotnet/ +## Resources + +- **Full Guide**: [../../docs/OBSERVABILITY.md](../../docs/OBSERVABILITY.md) +- **Arize Phoenix**: <https://docs.arize.com/phoenix> +- **OpenTelemetry .NET**: <https://opentelemetry.io/docs/languages/dotnet/>Also applies to: 167-167, 237-237, 238-238
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
samples/StreamingWebApiSample/OBSERVABILITY.md(1 hunks)samples/StreamingWebApiSample/Program.cs(7 hunks)samples/StreamingWebApiSample/samples.csproj(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
samples/StreamingWebApiSample/Program.cs (4)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
OpenRouterClient(14-349)OpenRouterClient(34-36)OpenRouterClient(38-79)OpenRouterClient(81-90)packages/dotnet-sdk/src/Tools/ToolRegistrationExtensions.cs (8)
OpenRouterClient(10-16)OpenRouterClient(18-24)OpenRouterClient(26-32)OpenRouterClient(34-40)OpenRouterClient(42-55)OpenRouterClient(57-66)OpenRouterClient(72-147)OpenRouterClient(149-213)packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
OpenRouterClientOptions(5-18)packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)
🪛 markdownlint-cli2 (0.18.1)
samples/StreamingWebApiSample/OBSERVABILITY.md
76-76: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
156-156: Bare URL used
(MD034, no-bare-urls)
167-167: Bare URL used
(MD034, no-bare-urls)
237-237: Bare URL used
(MD034, no-bare-urls)
238-238: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (1)
samples/StreamingWebApiSample/samples.csproj (1)
11-14: Appropriate OpenTelemetry dependencies for the sample.The added packages are well-chosen for demonstrating observability in an ASP.NET Core context: the console exporter enables quick testing, hosting extensions simplify integration, and ASP.NET Core instrumentation automatically captures HTTP spans. All versions align with the PR objectives.
| app.MapPost("/api/stream", async (ChatRequest chatRequest, HttpContext context) => | ||
| { | ||
| var client = new OpenRouterClient(apiKey); | ||
| // ✨ Create client with observability enabled | ||
| var client = new OpenRouterClient(new OpenRouterClientOptions | ||
| { | ||
| ApiKey = apiKey, | ||
| Telemetry = new OpenRouterTelemetryOptions | ||
| { | ||
| EnableTelemetry = true, | ||
| CapturePrompts = true, // Log input prompts | ||
| CaptureCompletions = true, // Log outputs | ||
| CaptureToolDetails = true, // Log tool arguments/results | ||
| CaptureStreamChunks = true, // Log streaming chunks (high volume!) | ||
| MaxEventBodySize = 32_000 // 32KB limit | ||
| } | ||
| }); |
There was a problem hiding this comment.
Do not emit raw prompts/completions/tool payloads without sanitization.
Enabling CapturePrompts, CaptureCompletions, CaptureStreamChunks, and CaptureToolDetails in these sample clients causes user-entered content, model output, and tool arguments/results (including search queries or other PII) to be exported verbatim via the configured Console exporter. That is a high-risk privacy/compliance issue for anyone running the sample as-is in a shared environment. Please either disable these capture flags by default or wire up the SanitizePrompt / SanitizeCompletion / SanitizeToolArguments callbacks so sensitive data is redacted before it leaves the process.
Also applies to: 201-212, 441-451, 489-499, 523-533
🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/Program.cs around lines 84 to 99 (and also
apply same changes to 201-212, 441-451, 489-499, 523-533): the telemetry options
currently capture raw prompts, completions, stream chunks and tool details which
can leak PII; either set CapturePrompts, CaptureCompletions,
CaptureStreamChunks, and CaptureToolDetails to false by default, or wire the
SanitizePrompt, SanitizeCompletion and SanitizeToolArguments callbacks to redact
sensitive fields before export (implement and reference a sanitizer that strips
PII and truncates large bodies, and ensure MaxEventBodySize is enforced), and
update comments to warn contributors about enabling raw capture in production.
Add full OpenTelemetry tracing to StreamingHandler to capture streaming metrics, prompts, completions, and tool execution for the /api/stream endpoint. ## Changes ### StreamingHandler.StreamAsync() - Create parent span 'gen_ai.client.stream' when telemetry enabled - Set core attributes: operation, system, server, model - Capture input prompts via TelemetryHelper.EnrichWithRequest() - Error tracking with TelemetryHelper.RecordException() ### StreamAsyncInternal() - Track streaming metrics in real-time: - Time to first token (TTFT) - Total chunks processed - Finish reason - Response model - Input/output token counts - Optionally log individual stream chunks (if CaptureStreamChunks enabled) - Calculate and report tokens per second - Enrich activity with final metrics after streaming completes ### Metrics Captured **Always (when telemetry enabled):** - gen_ai.request.model: Requested model - gen_ai.response.model: Actual model used - gen_ai.stream.time_to_first_token_ms: TTFT metric - gen_ai.stream.total_chunks: Number of chunks - gen_ai.stream.duration_ms: Total streaming time - gen_ai.stream.tokens_per_second: Throughput - gen_ai.stream.finish_reason: How stream ended - gen_ai.usage.input_tokens: Prompt tokens - gen_ai.usage.output_tokens: Completion tokens - http.response.status_code: 200 **Opt-in (CaptureStreamChunks):** - gen_ai.client.stream.chunk events with chunk_index, text_delta, elapsed_ms **Opt-in (CapturePrompts):** - gen_ai.client.input event with full conversation history ## Impact Now when hitting /api/stream, you'll see: This matches the console output pattern users expect and enables full observability of streaming LLM operations in Phoenix/Jaeger.
| <PackageReference Include="OpenTelemetry" Version="1.10.0" /> | ||
| <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.10.0" /> | ||
| <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.10.0" /> | ||
| <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" /> |
There was a problem hiding this comment.
🧩 Analysis chain
Verify that System.Diagnostics.DiagnosticSource is accounted for.
The PR objectives mention System.Diagnostics.DiagnosticSource 9.0.0 as a core dependency, but this package is not explicitly referenced in the sample project. Ensure it's either included here, transitively available from the SDK dependencies, or verify it's not needed for this sample.
Additionally, confirm that all OpenTelemetry package versions (1.10.0) are current and aligned with the main SDK package versions.
🏁 Script executed:
#!/bin/bash
# Verify System.Diagnostics.DiagnosticSource is included in the main SDK and check package versions
# Check main SDK csproj for System.Diagnostics.DiagnosticSource and OpenTelemetry versions
fd -e csproj packages/dotnet-sdk -x grep -n "System.Diagnostics.DiagnosticSource\|OpenTelemetry.Api\|<PackageReference" {} +
# List all PackageReferences in main SDK for comparison
echo "=== Main SDK dependencies ==="
fd packages/dotnet-sdk -name "*.csproj" -x cat {} \; | grep -A 1 "<PackageReference"Length of output: 642
🏁 Script executed:
#!/bin/bash
# Correctly search for csproj files in packages/dotnet-sdk directory
echo "=== Finding .csproj files in packages/dotnet-sdk ==="
fd . packages/dotnet-sdk -name "*.csproj"
echo ""
echo "=== Searching for System.Diagnostics.DiagnosticSource and OpenTelemetry references in main SDK ==="
fd . packages/dotnet-sdk -name "*.csproj" -exec grep -n "System.Diagnostics.DiagnosticSource\|OpenTelemetry" {} +
echo ""
echo "=== Sample project dependencies ==="
cat samples/StreamingWebApiSample/samples.csproj | grep -n "PackageReference"Length of output: 1168
🏁 Script executed:
#!/bin/bash
# Find all csproj files
echo "=== Finding .csproj files in packages/dotnet-sdk ==="
find packages/dotnet-sdk -name "*.csproj" -type f
echo ""
echo "=== Searching main SDK csproj for System.Diagnostics.DiagnosticSource and OpenTelemetry ==="
find packages/dotnet-sdk -name "*.csproj" -type f -exec grep -H "System.Diagnostics.DiagnosticSource\|OpenTelemetry" {} \;
echo ""
echo "=== Full PackageReference list from main SDK ==="
find packages/dotnet-sdk -name "*.csproj" -type f -exec grep "PackageReference" {} + | head -20Length of output: 875
🌐 Web query:
OpenTelemetry 1.10.0 latest version 2025
💡 Result:
Short answer: No — OpenTelemetry 1.10.0 is not the latest in 2025.
Details:
- core 1.10.0 was released Nov 12, 2024. [1]
- The project published newer releases in 2025 (e.g., core 1.11.x in Jan–Mar 2025), and ecosystem packages reached 1.12/1.13 in 2025 (NuGet shows 1.13.0 published Oct 22, 2025). [2][3]
If you want, I can list the latest stable release for a specific language (Java, .NET, Python, JS) as of today (Nov 8, 2025).
🌐 Web query:
OpenTelemetry.Instrumentation.AspNetCore latest version
💡 Result:
Latest NuGet version: OpenTelemetry.Instrumentation.AspNetCore 1.13.0 — published 2025-10-22. [1][2]
Sources:
[1] NuGet package page (OpenTelemetry.Instrumentation.AspNetCore).
[2] NuGet package page (opentelemetry.instrumentation.aspnetcore).
🌐 Web query:
System.Diagnostics.DiagnosticSource 9.0.0 transitive dependency OpenTelemetry
💡 Result:
Short answer: No — System.Diagnostics.DiagnosticSource 9.0.0 does not transitively depend on OpenTelemetry. The DiagnosticSource NuGet 9.0.0 package has no OpenTelemetry dependency (for net8/net9 targets it lists no dependencies; only older TFMs list System.Memory and Unsafe). [1]
OpenTelemetry .NET packages, conversely, rely on the Activity/DiagnosticSource APIs (they may reference or recommend installing System.Diagnostics.DiagnosticSource), so OpenTelemetry depends on DiagnosticSource, not the other way around. [2][3]
Sources:
- System.Diagnostics.DiagnosticSource 9.0.0 (NuGet): package details / dependencies. [1]
- OpenTelemetry .NET docs / guidance stating OpenTelemetry uses the System.Diagnostics Activity APIs and that DiagnosticSource is provided/managed by package dependencies. [2][3]
Update OpenTelemetry packages to current versions (1.13.0).
The OpenTelemetry packages in this sample are outdated. Version 1.10.0 (released Nov 12, 2024) has been superseded by 1.13.0 (released Oct 22, 2025). Update all OpenTelemetry package references from 1.10.0 to 1.13.0 to match the latest stable release.
Regarding System.Diagnostics.DiagnosticSource: This dependency is present in the main SDK and will likely be included transitively through the OpenTelemetry packages, so explicit inclusion in the sample is not required.
🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/samples.csproj around lines 11 to 15, update
all OpenTelemetry PackageReference versions from 1.10.0 to 1.13.0; specifically
change the Version attributes for OpenTelemetry, OpenTelemetry.Exporter.Console,
OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, and
OpenTelemetry.Instrumentation.Http to "1.13.0". Do not add an explicit
System.Diagnostics.DiagnosticSource package reference since it will be provided
transitively by the updated OpenTelemetry packages.
…o 1.11.0 Fix build errors and security vulnerability: 1. Remove try-catch block from StreamAsync() method - C# doesn't allow yield return inside try-catch blocks - Moved telemetry setup outside of try block - Activity will still be disposed properly via using statement 2. Upgrade OpenTelemetry packages from 1.10.0 to 1.11.0 - Fixes security vulnerability GHSA-8785-wc3w-h8q6 - Updates all OpenTelemetry packages in both SDK and sample Resolves build errors: - CS1626: Cannot yield a value in the body of a try block with a catch clause
Switch the sample from console output to Phoenix OTLP exporter so traces
appear in the Phoenix UI at http://localhost:6006.
## Changes
- Add OpenTelemetry.Exporter.OpenTelemetryProtocol package
- Replace .AddConsoleExporter() with .AddOtlpExporter()
- Configure endpoint: http://localhost:4317 (Phoenix gRPC)
- Protocol: OtlpExportProtocol.Grpc
## Usage
1. Start Phoenix: docker run -p 6006:6006 -p 4317:4317 arizephoenix/phoenix:latest
2. Run sample: dotnet run
3. Make request: curl -X POST http://localhost:5000/api/stream -H "Content-Type: application/json" -d '{"message":"hi"}'
4. View traces at: http://localhost:6006
Traces will now appear in Phoenix UI instead of console output.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (6)
samples/StreamingWebApiSample/samples.csproj (1)
11-16: Update OpenTelemetry packages to latest stable version (1.13.0).While version 1.11.0 is an improvement over the original 1.10.0, the latest stable OpenTelemetry packages are at version 1.13.0 (released Oct 22, 2025). Update all six PackageReference entries to version 1.13.0 to stay current with the latest stable release and receive recent bug fixes and improvements.
Apply this diff:
- <PackageReference Include="OpenTelemetry" Version="1.11.0" /> - <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.11.0" /> - <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.0" /> - <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.0" /> - <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.0" /> - <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.0" /> + <PackageReference Include="OpenTelemetry" Version="1.13.0" /> + <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.13.0" /> + <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.0" /> + <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.13.0" /> + <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" /> + <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />samples/StreamingWebApiSample/Program.cs (5)
92-104: Raw capture without sanitization remains a privacy risk.Enabling
CapturePrompts,CaptureCompletions,CaptureToolDetails, andCaptureStreamChunkswithout sanitization callbacks can expose PII when running the sample. Developers copying this configuration may inadvertently leak sensitive data in production.Consider either defaulting to a safer preset like
OpenRouterTelemetryOptions.Production(which disables content capture) or demonstrating sanitization callbacks in at least one endpoint.Example with Production preset:
Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, // Log input prompts - CaptureCompletions = true, // Log outputs - CaptureToolDetails = true, // Log tool arguments/results - CaptureStreamChunks = true, // Log streaming chunks (high volume!) - MaxEventBodySize = 32_000 // 32KB limit + // Use Production preset for privacy-safe defaults + // Captures metrics but not sensitive content } + // Or copy Production preset settings explicitly for clarityOr demonstrate sanitization:
Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, CapturePrompts = true, CaptureCompletions = true, CaptureToolDetails = true, CaptureStreamChunks = true, - MaxEventBodySize = 32_000 + MaxEventBodySize = 32_000, + SanitizePrompt = prompt => Regex.Replace(prompt, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]"), + SanitizeCompletion = completion => Regex.Replace(completion, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]"), + SanitizeToolArguments = args => Regex.Replace(args, @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]") }
207-217: Same sanitization concern applies here.This endpoint also captures prompts, completions, and tool details without sanitization. Apply the same recommendations as the previous endpoint.
447-456: Sanitization concern applies to object generation endpoint.Structured outputs may contain sensitive data. Consider using
Productionpreset or adding sanitization callbacks.
495-504: Bug reports may contain particularly sensitive data.Bug descriptions often include system internals, stack traces, and potentially user data. Sanitization is especially important for this endpoint.
528-538: Sanitization concern applies to typed tools demo.Tool arguments and results may contain sensitive data. Apply the same sanitization recommendations.
🧹 Nitpick comments (1)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (1)
369-370: Consider capturing actual HTTP status code instead of hardcoding.The status code is hardcoded to 200, which works because errors throw earlier (line 214), but it's fragile. If error handling changes, this hardcoded value could become incorrect.
Consider capturing and passing the actual
response.StatusCodefrom line 213:using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream); + var httpStatusCode = (int)response.StatusCode; System.Diagnostics.Stopwatch? stopwatch = null; var chunkIndex = 0;Then use it at line 369:
- currentActivity.SetTag(GenAiSemanticConventions.AttributeHttpStatusCode, 200); + currentActivity.SetTag(GenAiSemanticConventions.AttributeHttpStatusCode, httpStatusCode);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
packages/dotnet-sdk/OpenRouter.NET.csproj(1 hunks)packages/dotnet-sdk/src/Internal/StreamingHandler.cs(6 hunks)samples/StreamingWebApiSample/Program.cs(7 hunks)samples/StreamingWebApiSample/samples.csproj(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/dotnet-sdk/OpenRouter.NET.csproj
🧰 Additional context used
🧬 Code graph analysis (2)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (6)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2)
HttpRequestHandler(11-108)HttpRequestHandler(21-37)packages/dotnet-sdk/src/Tools/ToolManager.cs (2)
ToolManager(11-174)ToolManager(19-23)packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
OpenRouterActivitySource(9-26)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (2)
TelemetryHelper(11-314)EnrichWithRequest(16-88)
samples/StreamingWebApiSample/Program.cs (3)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
OpenRouterClient(14-349)OpenRouterClient(34-36)OpenRouterClient(38-79)OpenRouterClient(81-90)packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
OpenRouterClientOptions(5-18)packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)
🔇 Additional comments (6)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (4)
1-1: LGTM: Telemetry scaffolding is properly integrated.The telemetry infrastructure is correctly added as an optional dependency with safe defaults (disabled by default).
Also applies to: 5-5, 20-20, 25-26, 31-31
43-65: LGTM: Span initialization follows OpenTelemetry patterns.The conditional activity creation ensures zero overhead when telemetry is disabled. The initial enrichment properly captures operation context and request metadata.
203-204: LGTM: Activity context properly captured.Capturing
Activity.Currentbefore streaming ensures consistent enrichment of the same span throughout the async enumeration, avoiding context loss.
224-231: LGTM: Metric tracking variables properly initialized.The variables capture key streaming metrics (TTFT, chunk count, tokens, finish reason) that are aggregated and added to the span at completion.
samples/StreamingWebApiSample/Program.cs (2)
2-2: LGTM: OpenTelemetry configuration follows best practices.The setup properly configures OTLP export to Phoenix with appropriate resource attributes and instrumentation. The localhost endpoint is appropriate for a sample application.
Also applies to: 10-12, 16-35
75-76: LGTM: Demonstrates telemetry opt-in pattern.Using a simple client without telemetry options shows that observability is opt-in, which is the correct default behavior.
| if (_telemetryOptions.CaptureStreamChunks && !string.IsNullOrEmpty(chunkToYield.TextDelta)) | ||
| { | ||
| currentActivity.AddEvent(new ActivityEvent(GenAiSemanticConventions.EventStreamChunk, | ||
| tags: new ActivityTagsCollection | ||
| { | ||
| { "chunk_index", totalChunks }, | ||
| { "text_delta", chunkToYield.TextDelta }, | ||
| { "elapsed_ms", (long)chunkToYield.ElapsedTime.TotalMilliseconds } | ||
| })); |
There was a problem hiding this comment.
Apply sanitization to stream chunk text deltas.
When CaptureStreamChunks is enabled, the text delta is logged verbatim without sanitization. This can leak PII or sensitive model outputs. Consider applying the SanitizeCompletion callback before logging.
Apply this diff:
// Optionally log stream chunks
if (_telemetryOptions.CaptureStreamChunks && !string.IsNullOrEmpty(chunkToYield.TextDelta))
{
+ var sanitizedDelta = _telemetryOptions.SanitizeCompletion?.Invoke(chunkToYield.TextDelta) ?? chunkToYield.TextDelta;
currentActivity.AddEvent(new ActivityEvent(GenAiSemanticConventions.EventStreamChunk,
tags: new ActivityTagsCollection
{
{ "chunk_index", totalChunks },
- { "text_delta", chunkToYield.TextDelta },
+ { "text_delta", sanitizedDelta },
{ "elapsed_ms", (long)chunkToYield.ElapsedTime.TotalMilliseconds }
}));
}🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Internal/StreamingHandler.cs around lines 314 to 322,
the code logs chunkToYield.TextDelta verbatim which can leak sensitive content;
call the configured sanitize callback before adding the "text_delta" tag: obtain
a sanitized string by invoking _telemetryOptions.SanitizeCompletion (if
non-null) with chunkToYield.TextDelta (handle nulls and exceptions safely,
falling back to an empty or original string), then use that sanitized value in
the ActivityEvent tag instead of the raw TextDelta; do not mutate the original
chunk object and ensure the call is null-safe.
Changes to StreamingWebApiSample/Program.cs: - Use HTTP endpoint: http://localhost:4318/v1/traces (instead of gRPC port 4317) - Switch protocol: HttpProtobuf (instead of Grpc) - Keep ConsoleExporter for debugging alongside OTLP Phoenix typically expects HTTP protocol on port 4318 for OTLP traces. This should resolve the issue where traces appeared in console but not in Phoenix UI.
- Switch back to gRPC protocol on port 4317 (Phoenix only exposes this port) - Add verbose logging for OpenTelemetry to debug OTLP export issues - Configure batch export processor with explicit settings - Keep ConsoleExporter for comparison This will help diagnose why traces appear in console but not Phoenix.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
samples/StreamingWebApiSample/Program.cs (1)
93-105: Privacy issue: Raw prompts/completions/tool payloads emitted without sanitization.This is the same issue flagged in the previous review. Enabling
CapturePrompts,CaptureCompletions,CaptureStreamChunks, andCaptureToolDetailswithout wiring up theSanitizePrompt,SanitizeCompletion, andSanitizeToolArgumentscallbacks causes user-entered content, model output, and tool arguments/results to be exported verbatim. This is a high-risk privacy/compliance issue.Either disable these capture flags by default or implement sanitization callbacks:
var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, // Log input prompts - CaptureCompletions = true, // Log outputs - CaptureToolDetails = true, // Log tool arguments/results - CaptureStreamChunks = true, // Log streaming chunks (high volume!) - MaxEventBodySize = 32_000 // 32KB limit + CapturePrompts = false, // Disable to avoid PII exposure + CaptureCompletions = false, // Disable to avoid PII exposure + CaptureToolDetails = false, // Disable to avoid PII exposure + CaptureStreamChunks = false, // Disable to avoid PII exposure + MaxEventBodySize = 32_000, // 32KB limit + // Or enable with sanitization: + // SanitizePrompt = (prompt) => "[REDACTED]", + // SanitizeCompletion = (completion) => "[REDACTED]", + // SanitizeToolArguments = (args) => "[REDACTED]" } });Also applies to: 208-218, 448-457, 496-505, 529-539
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
samples/StreamingWebApiSample/Program.cs(7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
samples/StreamingWebApiSample/Program.cs (3)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
OpenRouterClient(14-349)OpenRouterClient(34-36)OpenRouterClient(38-79)OpenRouterClient(81-90)packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
OpenRouterClientOptions(5-18)packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)
🔇 Additional comments (2)
samples/StreamingWebApiSample/Program.cs (2)
2-2: LGTM!The OpenTelemetry and observability imports are appropriate for the telemetry integration being added.
Also applies to: 10-12
16-36: LGTM!The OpenTelemetry configuration is appropriate for a sample application. The service name is descriptive, instrumentation covers the key layers (ASP.NET Core, HTTP client, OpenRouter), and the exporters (Console for debugging, OTLP for Phoenix) are correctly configured.
| // Simple client without telemetry for models endpoint | ||
| var client = new OpenRouterClient(apiKey); |
There was a problem hiding this comment.
Dispose the client after use.
The OpenRouterClient implements IDisposable and should be disposed to prevent resource leaks. When no custom HttpClient is provided via options, the client creates and owns an HttpClient internally that must be disposed.
Apply this diff to wrap the client in a using statement:
- // Simple client without telemetry for models endpoint
- var client = new OpenRouterClient(apiKey);
- var models = await client.GetModelsAsync();
+ // Simple client without telemetry for models endpoint
+ using var client = new OpenRouterClient(apiKey);
+ var models = await client.GetModelsAsync();Also applies to: 93-105, 208-218, 448-457, 496-505, 529-539
🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/Program.cs around lines 76-77 (and similarly at
93-105, 208-218, 448-457, 496-505, 529-539), the OpenRouterClient is created but
not disposed; wrap each client creation in a using/using-declaration or
explicitly call Dispose so the internal HttpClient is released when no custom
HttpClient is provided. Replace the direct instantiation with a using (or add
await using if the client is IAsyncDisposable) scope around the code that uses
the client so the client is disposed automatically at the end of the scope.
- Enable Trace level logging for OpenTelemetry components - Add console logger for debug output - Filter specifically for OTLP exporter logs - Simplify OTLP configuration to defaults This will reveal any gRPC connection errors or export failures.
Switch from gRPC (port 4317) to HTTP (port 6006/v1/traces) for OTLP export. Phoenix documentation shows HTTP endpoint: http://localhost:6006/v1/traces This is often more reliable than gRPC for local development.
Changes: - TelemetryHelper: Add OpenInference span attributes alongside events - Input messages: llm.input_messages.N.message.role/content - Output messages: llm.output_messages.N.message.role/content - Keep existing events for backward compatibility - StreamingHandler: Accumulate complete streamed text - Track full completion text across all chunks - Add OpenInference output message attributes at stream end - Enables Phoenix UI to display complete responses - Make TruncateIfNeeded internal for reuse in StreamingHandler Phoenix UI displays span attributes but not event attributes. By adding OpenInference conventions as span attributes, users can now view prompts, completions, and all LLM interaction data directly in Phoenix.
- Added OpenTelemetry configuration to the StreamingWebApiSample for improved observability. - Introduced telemetry options in OpenRouterClient initialization to enable detailed tracking of API interactions. - Updated OpenTelemetryExtensions to include a default sampling method for activity tracing. - Added necessary OpenTelemetry package references in the project file. This update allows for better monitoring and debugging capabilities in the sample application.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (7)
samples/StreamingWebApiSample/Program.cs (6)
83-95: Dispose the client after use.The
OpenRouterClientimplementsIDisposableand should be disposed to prevent resource leaks. When no customHttpClientis provided, the client creates and owns anHttpClientinternally that must be disposed.Apply this diff to wrap the client in a using statement:
- // Simple client without telemetry for models endpoint - var client = new OpenRouterClient(apiKey); + // Simple client without telemetry for models endpoint + using var client = new OpenRouterClient(apiKey);
99-112: Do not emit raw prompts/completions/tool payloads without sanitization.Enabling
CapturePrompts,CaptureCompletions,CaptureStreamChunks, andCaptureToolDetailsin this sample endpoint causes user-entered content, model output, and tool arguments/results (including search queries or other PII) to be exported verbatim via the configured Console exporter. This is a high-risk privacy/compliance issue. Additionally, the client is not disposed, leading to resource leaks.Please either:
- Disable these capture flags by default, or
- Wire up the
SanitizePrompt,SanitizeCompletion, andSanitizeToolArgumentscallbacks to redact sensitive data before export, or- Add prominent warnings in comments about the privacy implications of enabling raw capture in production
Also, wrap the client in a
usingstatement to ensure proper disposal.Apply this diff to address both issues:
- // ✨ Create client with observability enabled - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability enabled + // ⚠️ WARNING: This sample captures raw prompts/completions for demonstration. + // In production, disable these flags or implement sanitization callbacks. + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, // Log input prompts - CaptureCompletions = true, // Log outputs - CaptureToolDetails = true, // Log tool arguments/results - CaptureStreamChunks = true, // Log streaming chunks (high volume!) + CapturePrompts = false, // ⚠️ May log PII - enable only with sanitization + CaptureCompletions = false, // ⚠️ May log PII - enable only with sanitization + CaptureToolDetails = false, // ⚠️ May log sensitive data + CaptureStreamChunks = false, // ⚠️ High volume + PII risk MaxEventBodySize = 32_000 // 32KB limit } });
214-225: Do not emit raw prompts/completions/tool payloads without sanitization.This endpoint captures
CapturePrompts,CaptureCompletions, andCaptureToolDetailswithout sanitization callbacks, risking PII leakage via the Console exporter. Additionally, the client is not disposed properly.Apply this diff:
- // ✨ Create client with observability enabled - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability enabled + // ⚠️ WARNING: Raw capture disabled by default. Enable only with sanitization in production. + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true, - CaptureToolDetails = true + CapturePrompts = false, + CaptureCompletions = false, + CaptureToolDetails = false } });
454-464: Do not emit raw prompts/completions without sanitization.This endpoint captures
CapturePromptsandCaptureCompletionswithout sanitization, risking PII exposure. The client also lacks proper disposal.Apply this diff:
- // ✨ Create client with observability for object generation - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability for object generation + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true + CapturePrompts = false, + CaptureCompletions = false } });
502-512: Do not emit raw prompts/completions without sanitization.Bug reports may contain sensitive system information or user data. Capturing prompts and completions without sanitization poses privacy risks. The client also needs proper disposal.
Apply this diff:
- // ✨ Create client with observability for bug triage - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability for bug triage + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true + CapturePrompts = false, + CaptureCompletions = false } });
535-546: Do not emit raw prompts/completions/tool payloads without sanitization.This demo endpoint captures prompts, completions, and tool details without sanitization, creating PII leakage risk. The client also requires proper disposal.
Apply this diff:
- // ✨ Create client with observability for typed tools demo - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability for typed tools demo + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true, - CaptureToolDetails = true + CapturePrompts = false, + CaptureCompletions = false, + CaptureToolDetails = false } });packages/dotnet-sdk/src/Internal/StreamingHandler.cs (1)
315-324: Apply sanitization to stream chunk text deltas.The text delta is logged verbatim without applying the
SanitizeCompletioncallback, which can leak sensitive content.Apply this diff:
// Optionally log stream chunks if (_telemetryOptions.CaptureStreamChunks && !string.IsNullOrEmpty(chunkToYield.TextDelta)) { + var sanitizedDelta = _telemetryOptions.SanitizeCompletion?.Invoke(chunkToYield.TextDelta) ?? chunkToYield.TextDelta; currentActivity.AddEvent(new ActivityEvent(GenAiSemanticConventions.EventStreamChunk, tags: new ActivityTagsCollection { { "chunk_index", totalChunks }, - { "text_delta", chunkToYield.TextDelta }, + { "text_delta", sanitizedDelta }, { "elapsed_ms", (long)chunkToYield.ElapsedTime.TotalMilliseconds } })); }
🧹 Nitpick comments (1)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (1)
82-82: Optimize repeated GetMessageContent calls.The current code calls
GetMessageContenttwice when the sanitization callback is null—once for the sanitize path and once as the fallback. Consider storing the result in a variable first to avoid the redundant call.Example for line 82:
- var content = options.SanitizePrompt?.Invoke(GetMessageContent(message)) ?? GetMessageContent(message); + var rawContent = GetMessageContent(message); + var content = options.SanitizePrompt?.Invoke(rawContent) ?? rawContent;Apply similar changes to lines 174, 276, and 298.
Also applies to: 174-174, 276-276, 298-298
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs(6 hunks)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs(1 hunks)samples/StreamingWebApiSample/Program.cs(7 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
packages/dotnet-sdk/src/Internal/StreamingHandler.cs (5)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Internal/HttpRequestHandler.cs (2)
HttpRequestHandler(11-108)HttpRequestHandler(21-37)packages/dotnet-sdk/src/Observability/OpenRouterActivitySource.cs (1)
OpenRouterActivitySource(9-26)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (3)
TelemetryHelper(11-352)EnrichWithRequest(16-103)TruncateIfNeeded(338-351)
samples/StreamingWebApiSample/Program.cs (4)
packages/dotnet-sdk/src/OpenRouterClient.cs (4)
OpenRouterClient(14-349)OpenRouterClient(34-36)OpenRouterClient(38-79)OpenRouterClient(81-90)packages/dotnet-sdk/src/Tools/ToolRegistrationExtensions.cs (8)
OpenRouterClient(10-16)OpenRouterClient(18-24)OpenRouterClient(26-32)OpenRouterClient(34-40)OpenRouterClient(42-55)OpenRouterClient(57-66)OpenRouterClient(72-147)OpenRouterClient(149-213)packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
OpenRouterClientOptions(5-18)packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)
packages/dotnet-sdk/src/Observability/TelemetryHelper.cs (4)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/Observability/GenAiSemanticConventions.cs (1)
GenAiSemanticConventions(7-92)packages/dotnet-sdk/src/Models/ChatCompletionResponse.cs (1)
ChatCompletionResponse(5-27)packages/dotnet-sdk/src/Models/Message.cs (1)
ContentPart(48-52)
🔇 Additional comments (1)
samples/StreamingWebApiSample/Program.cs (1)
2-2: LGTM: OpenTelemetry setup is well-configured for development.The observability wiring correctly initializes Phoenix OTLP exporter with HTTP protocol, enables relevant instrumentations (ASP.NET Core, HttpClient, OpenRouter), and includes console output for debugging. The verbose logging levels are appropriate for a sample application.
Also applies to: 10-43
| if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0) | ||
| { | ||
| var completeText = completeTextBuilder.ToString(); | ||
| var truncated = TelemetryHelper.TruncateIfNeeded(completeText, _telemetryOptions.MaxEventBodySize); | ||
|
|
||
| currentActivity.SetTag("llm.output_messages.0.message.role", "assistant"); | ||
| currentActivity.SetTag("llm.output_messages.0.message.content", truncated); | ||
|
|
||
| if (!string.IsNullOrEmpty(finishReason)) | ||
| { | ||
| currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason); | ||
| } | ||
| } |
There was a problem hiding this comment.
Apply sanitization to complete output text before logging.
The accumulated complete text is added to span attributes without applying the SanitizeCompletion callback. This can leak PII or sensitive model outputs. The truncation on line 383 only limits size but does not sanitize content.
Apply this diff:
// OpenInference: Add complete output message as span attributes for Phoenix UI
if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0)
{
var completeText = completeTextBuilder.ToString();
+ var sanitizedText = _telemetryOptions.SanitizeCompletion?.Invoke(completeText) ?? completeText;
- var truncated = TelemetryHelper.TruncateIfNeeded(completeText, _telemetryOptions.MaxEventBodySize);
+ var truncated = TelemetryHelper.TruncateIfNeeded(sanitizedText, _telemetryOptions.MaxEventBodySize);
currentActivity.SetTag("llm.output_messages.0.message.role", "assistant");
currentActivity.SetTag("llm.output_messages.0.message.content", truncated);
if (!string.IsNullOrEmpty(finishReason))
{
currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason);
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0) | |
| { | |
| var completeText = completeTextBuilder.ToString(); | |
| var truncated = TelemetryHelper.TruncateIfNeeded(completeText, _telemetryOptions.MaxEventBodySize); | |
| currentActivity.SetTag("llm.output_messages.0.message.role", "assistant"); | |
| currentActivity.SetTag("llm.output_messages.0.message.content", truncated); | |
| if (!string.IsNullOrEmpty(finishReason)) | |
| { | |
| currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason); | |
| } | |
| } | |
| if (_telemetryOptions.CaptureCompletions && completeTextBuilder.Length > 0) | |
| { | |
| var completeText = completeTextBuilder.ToString(); | |
| var sanitizedText = _telemetryOptions.SanitizeCompletion?.Invoke(completeText) ?? completeText; | |
| var truncated = TelemetryHelper.TruncateIfNeeded(sanitizedText, _telemetryOptions.MaxEventBodySize); | |
| currentActivity.SetTag("llm.output_messages.0.message.role", "assistant"); | |
| currentActivity.SetTag("llm.output_messages.0.message.content", truncated); | |
| if (!string.IsNullOrEmpty(finishReason)) | |
| { | |
| currentActivity.SetTag("llm.output_messages.0.finish_reason", finishReason); | |
| } | |
| } |
🤖 Prompt for AI Agents
In packages/dotnet-sdk/src/Internal/StreamingHandler.cs around lines 380 to 392,
the code writes the accumulated completion text to activity tags without calling
the SanitizeCompletion callback, risking PII leakage; update the flow to first
run completeText through the configured SanitizeCompletion callback (or a no-op
if null), then apply TelemetryHelper.TruncateIfNeeded to the sanitized result,
and use that sanitized+truncated string when setting the
llm.output_messages.0.message.content and related tags (keep existing
finish_reason handling).
| internal static string TruncateIfNeeded(string content, int maxSize) | ||
| { | ||
| if (maxSize <= 0) return content; | ||
|
|
||
| var bytes = Encoding.UTF8.GetBytes(content); | ||
| if (bytes.Length <= maxSize) | ||
| { | ||
| return content; | ||
| } | ||
|
|
||
| // Truncate to maxSize bytes and add indicator | ||
| var truncated = Encoding.UTF8.GetString(bytes, 0, Math.Max(0, maxSize - 20)); | ||
| return truncated + "... [TRUNCATED]"; | ||
| } |
There was a problem hiding this comment.
UTF-8 truncation can corrupt multi-byte characters.
Line 349 truncates at an arbitrary byte boundary, which can split UTF-8 multi-byte characters and produce invalid output or replacement characters (�). This corrupts the truncated content.
Apply this diff to use character-safe truncation:
internal static string TruncateIfNeeded(string content, int maxSize)
{
if (maxSize <= 0) return content;
var bytes = Encoding.UTF8.GetBytes(content);
if (bytes.Length <= maxSize)
{
return content;
}
- // Truncate to maxSize bytes and add indicator
- var truncated = Encoding.UTF8.GetString(bytes, 0, Math.Max(0, maxSize - 20));
- return truncated + "... [TRUNCATED]";
+ // Truncate by character count to avoid splitting multi-byte UTF-8 sequences
+ const string suffix = "... [TRUNCATED]";
+ var suffixBytes = Encoding.UTF8.GetByteCount(suffix);
+ var targetSize = Math.Max(0, maxSize - suffixBytes);
+
+ // Walk back from targetSize to find a safe character boundary
+ var decoder = Encoding.UTF8.GetDecoder();
+ var charCount = decoder.GetCharCount(bytes, 0, targetSize, false);
+ var truncated = content.Substring(0, charCount);
+ return truncated + suffix;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| internal static string TruncateIfNeeded(string content, int maxSize) | |
| { | |
| if (maxSize <= 0) return content; | |
| var bytes = Encoding.UTF8.GetBytes(content); | |
| if (bytes.Length <= maxSize) | |
| { | |
| return content; | |
| } | |
| // Truncate to maxSize bytes and add indicator | |
| var truncated = Encoding.UTF8.GetString(bytes, 0, Math.Max(0, maxSize - 20)); | |
| return truncated + "... [TRUNCATED]"; | |
| } | |
| internal static string TruncateIfNeeded(string content, int maxSize) | |
| { | |
| if (maxSize <= 0) return content; | |
| var bytes = Encoding.UTF8.GetBytes(content); | |
| if (bytes.Length <= maxSize) | |
| { | |
| return content; | |
| } | |
| // Truncate by character count to avoid splitting multi-byte UTF-8 sequences | |
| const string suffix = "... [TRUNCATED]"; | |
| var suffixBytes = Encoding.UTF8.GetByteCount(suffix); | |
| var targetSize = Math.Max(0, maxSize - suffixBytes); | |
| // Walk back from targetSize to find a safe character boundary | |
| var decoder = Encoding.UTF8.GetDecoder(); | |
| var charCount = decoder.GetCharCount(bytes, 0, targetSize, false); | |
| var truncated = content.Substring(0, charCount); | |
| return truncated + suffix; | |
| } |
This change ensures proper formatting and adherence to coding standards by adding a newline at the end of the file.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (7)
samples/StreamingWebApiSample/Program.cs (7)
91-93: Dispose the client after use.The
OpenRouterClientimplementsIDisposableand should be disposed to prevent resource leaks. When no customHttpClientis provided, the client creates and owns anHttpClientinternally that must be disposed.Apply this diff to wrap the client in a using statement:
- // Simple client without telemetry for models endpoint - var client = new OpenRouterClient(apiKey); + // Simple client without telemetry for models endpoint + using var client = new OpenRouterClient(apiKey);
107-120: Do not emit raw prompts/completions/tool payloads without sanitization.Enabling
CapturePrompts,CaptureCompletions,CaptureStreamChunks, andCaptureToolDetailscauses user-entered content, model output, and tool arguments/results to be exported verbatim via the configured Console and OTLP exporters. This is a high-risk privacy/compliance issue. Wire up theSanitizePrompt,SanitizeCompletion, andSanitizeToolArgumentscallbacks to redact sensitive data before export.Apply this diff to add sanitization callbacks:
var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, CapturePrompts = true, CaptureCompletions = true, CaptureToolDetails = true, CaptureStreamChunks = true, - MaxEventBodySize = 32_000 + MaxEventBodySize = 32_000, + SanitizePrompt = prompt => "[REDACTED]", + SanitizeCompletion = completion => "[REDACTED]", + SanitizeToolArguments = args => "[REDACTED]" } });Or disable raw capture for production use:
Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true, - CaptureToolDetails = true, - CaptureStreamChunks = true, + CapturePrompts = false, + CaptureCompletions = false, + CaptureToolDetails = false, + CaptureStreamChunks = false, MaxEventBodySize = 32_000 }
108-120: Dispose the client after use.The client is not disposed, which will leak resources. Wrap it in a using statement.
Apply this diff:
- // ✨ Create client with observability enabled - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability enabled + using var client = new OpenRouterClient(new OpenRouterClientOptions
222-233: Apply sanitization and disposal fixes.This endpoint has the same PII/sanitization and disposal issues as the /api/stream endpoint.
Apply both fixes:
- // ✨ Create client with observability enabled - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability enabled + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true, - CaptureToolDetails = true + CapturePrompts = false, + CaptureCompletions = false, + CaptureToolDetails = false } });
462-472: Apply sanitization and disposal fixes.This endpoint captures prompts and completions without sanitization and does not dispose the client.
Apply both fixes:
- // ✨ Create client with observability for object generation - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability for object generation + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true + CapturePrompts = false, + CaptureCompletions = false } });
510-520: Apply sanitization and disposal fixes.This endpoint captures prompts and completions without sanitization and does not dispose the client.
Apply both fixes:
- // ✨ Create client with observability for bug triage - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability for bug triage + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true + CapturePrompts = false, + CaptureCompletions = false } });
543-554: Apply sanitization and disposal fixes.This endpoint has the same PII/sanitization and disposal issues as other endpoints.
Apply both fixes:
- // ✨ Create client with observability for typed tools demo - var client = new OpenRouterClient(new OpenRouterClientOptions + // ✨ Create client with observability for typed tools demo + using var client = new OpenRouterClient(new OpenRouterClientOptions { ApiKey = apiKey, Telemetry = new OpenRouterTelemetryOptions { EnableTelemetry = true, - CapturePrompts = true, - CaptureCompletions = true, - CaptureToolDetails = true + CapturePrompts = false, + CaptureCompletions = false, + CaptureToolDetails = false } });
🧹 Nitpick comments (1)
samples/StreamingWebApiSample/Program.cs (1)
16-20: Consider environment-specific logging configuration.The Trace-level logging for OpenTelemetry is useful for debugging but will generate excessive output in production environments. Consider wrapping this configuration in an environment check.
Apply this diff to enable verbose logging only in Development:
-// Enable verbose logging for OpenTelemetry debugging -builder.Logging.SetMinimumLevel(LogLevel.Debug); -builder.Logging.AddConsole(); -builder.Logging.AddFilter("OpenTelemetry", LogLevel.Trace); -builder.Logging.AddFilter("OpenTelemetry.Exporter.OpenTelemetryProtocol", LogLevel.Trace); +// Enable verbose logging for OpenTelemetry debugging (Development only) +if (builder.Environment.IsDevelopment()) +{ + builder.Logging.SetMinimumLevel(LogLevel.Debug); + builder.Logging.AddConsole(); + builder.Logging.AddFilter("OpenTelemetry", LogLevel.Trace); + builder.Logging.AddFilter("OpenTelemetry.Exporter.OpenTelemetryProtocol", LogLevel.Trace); +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs(1 hunks)samples/StreamingWebApiSample/Program.cs(7 hunks)tests/OpenRouter.NET.Tests/SchemaValidationTests.cs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/dotnet-sdk/src/Observability/OpenTelemetryExtensions.cs
🧰 Additional context used
🧬 Code graph analysis (1)
samples/StreamingWebApiSample/Program.cs (3)
packages/dotnet-sdk/src/Observability/OpenRouterTelemetryOptions.cs (1)
OpenRouterTelemetryOptions(7-122)packages/dotnet-sdk/src/OpenRouterClient.cs (5)
List(97-97)OpenRouterClient(14-349)OpenRouterClient(34-36)OpenRouterClient(38-79)OpenRouterClient(81-90)packages/dotnet-sdk/src/OpenRouterClientOptions.cs (1)
OpenRouterClientOptions(5-18)
🔇 Additional comments (1)
tests/OpenRouter.NET.Tests/SchemaValidationTests.cs (1)
410-410: Standard formatting: newline at end of file.Adding a newline at the end of the file is a standard practice and aligns with POSIX conventions.
| var telemetryOptions = new OpenRouterTelemetryOptions | ||
| { | ||
| EnableTelemetry = true, | ||
| CapturePrompts = true, | ||
| CaptureCompletions = true, | ||
| CaptureToolDetails = true | ||
| }; |
There was a problem hiding this comment.
Remove or use the unused telemetryOptions variable.
This variable is defined but never referenced. Each endpoint creates its own inline telemetry options instead.
Apply this diff to remove the unused variable:
-var telemetryOptions = new OpenRouterTelemetryOptions
-{
- EnableTelemetry = true,
- CapturePrompts = true,
- CaptureCompletions = true,
- CaptureToolDetails = true
-};
-
var conversationStore = new ConcurrentDictionary<string, List<Message>>();Alternatively, if you intended to reuse this configuration, reference it in the endpoints:
var client = new OpenRouterClient(new OpenRouterClientOptions
{
ApiKey = apiKey,
Telemetry = telemetryOptions // Reuse the shared config
});🤖 Prompt for AI Agents
In samples/StreamingWebApiSample/Program.cs around lines 78 to 84, the variable
telemetryOptions is defined but never used; either remove this unused variable
or reuse it by passing it into the OpenRouterClient configuration for each
endpoint (set Telemetry = telemetryOptions on new OpenRouterClientOptions).
Update all endpoint client constructions to reference the shared
telemetryOptions if you want reuse, otherwise delete the telemetryOptions
declaration to eliminate the unused variable.
Add full-featured observability to OpenRouter.NET using OpenTelemetry, enabling tracking of LLM API calls, token usage, performance metrics, and tool execution with platforms like Arize Phoenix, Jaeger, or any OTLP-compatible backend.
Key Features
Implementation Details
New Components
OpenRouterActivitySource: Singleton ActivitySource for tracingGenAiSemanticConventions: Constants for OTel GenAI attributesOpenRouterTelemetryOptions: Comprehensive opt-in configurationTelemetryHelper: Enricher methods for spans and eventsOpenTelemetryExtensions: Extension method for easy setupInstrumented Classes
OpenRouterClient.CreateChatCompletionAsync(): Chat completion spansStreamingHandler: Streaming metrics (TTFT, throughput)ToolManager.ExecuteTool(): Tool execution child spansObjectGenerator.GenerateObjectAsync(): Schema validation trackingHttpRequestHandler: HTTP-level attributesConfiguration Options
EnableTelemetry: Master switch (default: false)CaptureRawRequests/Responses: Log full HTTP payloadsCapturePrompts/Completions: Log structured messagesCaptureStreamChunks: Log individual chunks (high volume)CaptureToolDetails: Log tool arguments and resultsMaxEventBodySize: Size limit before truncation (32KB default)SanitizePrompt/Completion/ToolArguments: PII redaction callbacksPresets
OpenRouterTelemetryOptions.Default: All disabled (opt-in)OpenRouterTelemetryOptions.Production: Metrics only, no contentOpenRouterTelemetryOptions.Debug: Full capture for debuggingOpenRouterTelemetryOptions.FullTelemetry: Everything enabledDocumentation
docs/OBSERVABILITY.mdDependencies Added
Usage Example
Breaking Changes
None - all changes are additive and opt-in by default.
Resolves: #[issue-number] (if applicable)
Summary by CodeRabbit
New Features
Documentation
Samples