Skip to content

Commit ea4a4c5

Browse files
committed
Clean up OTel instrumentation
1 parent 2ecdf7d commit ea4a4c5

File tree

15 files changed

+230
-273
lines changed

15 files changed

+230
-273
lines changed

Directory.Packages.props

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<PackageVersion Include="AWSSDK.SQS" Version="4.0.2" />
2626
<PackageVersion Include="AWSSDK.S3" Version="4.0.7.14" />
2727
<PackageVersion Include="Elastic.OpenTelemetry" Version="1.1.0" />
28-
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
28+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
2929
<PackageVersion Include="KubernetesClient" Version="17.0.14" />
3030
<PackageVersion Include="Elastic.Aspire.Hosting.Elasticsearch" Version="9.3.0" />
3131
<PackageVersion Include="Elastic.Clients.Elasticsearch" Version="9.1.4" />
@@ -76,10 +76,10 @@
7676
<ItemGroup>
7777
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0" />
7878
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.4.0" />
79-
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
80-
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
81-
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
82-
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
79+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.13.1" />
80+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.13.1" />
81+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.13.0" />
82+
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.13.0" />
8383
</ItemGroup>
8484
<!-- Test packages -->
8585
<ItemGroup>
@@ -99,4 +99,4 @@
9999
</PackageVersion>
100100
<PackageVersion Include="xunit.v3" Version="2.0.2" />
101101
</ItemGroup>
102-
</Project>
102+
</Project>

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/AskAiEvent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import * as z from 'zod'
44
// Event type constants for type-safe referencing
55
export const EventTypes = {
66
CONVERSATION_START: 'conversation_start',
7-
CHUNK: 'chunk',
8-
CHUNK_COMPLETE: 'chunk_complete',
7+
MESSAGE_CHUNK: 'message_chunk',
8+
MESSAGE_COMPLETE: 'message_complete',
99
SEARCH_TOOL_CALL: 'search_tool_call',
1010
TOOL_CALL: 'tool_call',
1111
TOOL_RESULT: 'tool_result',
@@ -23,14 +23,14 @@ export const ConversationStartEventSchema = z.object({
2323
})
2424

2525
export const ChunkEventSchema = z.object({
26-
type: z.literal(EventTypes.CHUNK),
26+
type: z.literal(EventTypes.MESSAGE_CHUNK),
2727
id: z.string(),
2828
timestamp: z.number(),
2929
content: z.string(),
3030
})
3131

3232
export const ChunkCompleteEventSchema = z.object({
33-
type: z.literal(EventTypes.CHUNK_COMPLETE),
33+
type: z.literal(EventTypes.MESSAGE_COMPLETE),
3434
id: z.string(),
3535
timestamp: z.number(),
3636
fullContent: z.string(),

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/ChatMessage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const computeAiStatus = (
144144
m.type === EventTypes.SEARCH_TOOL_CALL ||
145145
m.type === EventTypes.TOOL_CALL ||
146146
m.type === EventTypes.TOOL_RESULT ||
147-
m.type === EventTypes.CHUNK
147+
m.type === EventTypes.MESSAGE_CHUNK
148148
)
149149
.sort((a, b) => a.timestamp - b.timestamp)
150150

@@ -166,9 +166,9 @@ const computeAiStatus = (
166166
case EventTypes.TOOL_RESULT:
167167
return STATUS_MESSAGES.ANALYZING
168168

169-
case EventTypes.CHUNK: {
169+
case EventTypes.MESSAGE_CHUNK: {
170170
const allContent = events
171-
.filter((m) => m.type === EventTypes.CHUNK)
171+
.filter((m) => m.type === EventTypes.MESSAGE_CHUNK)
172172
.map((m) => m.content)
173173
.join('')
174174

src/Elastic.Documentation.Site/Assets/web-components/SearchOrAskAi/AskAi/StreamingAiMessage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const StreamingAiMessage = ({
3535
if (event.conversationId && !threadId) {
3636
setThreadId(event.conversationId)
3737
}
38-
} else if (event.type === EventTypes.CHUNK) {
38+
} else if (event.type === EventTypes.MESSAGE_CHUNK) {
3939
contentRef.current += event.content
4040
} else if (event.type === EventTypes.ERROR) {
4141
// Handle error events from the stream

src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ namespace Elastic.Documentation.Api.Core.AskAi;
1111
/// </summary>
1212
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
1313
[JsonDerivedType(typeof(ConversationStart), typeDiscriminator: "conversation_start")]
14-
[JsonDerivedType(typeof(Chunk), typeDiscriminator: "chunk")]
15-
[JsonDerivedType(typeof(ChunkComplete), typeDiscriminator: "chunk_complete")]
14+
[JsonDerivedType(typeof(MessageChunk), typeDiscriminator: "message_chunk")]
15+
[JsonDerivedType(typeof(MessageComplete), typeDiscriminator: "message_complete")]
1616
[JsonDerivedType(typeof(SearchToolCall), typeDiscriminator: "search_tool_call")]
1717
[JsonDerivedType(typeof(ToolCall), typeDiscriminator: "tool_call")]
1818
[JsonDerivedType(typeof(ToolResult), typeDiscriminator: "tool_result")]
@@ -33,7 +33,7 @@ string ConversationId
3333
/// <summary>
3434
/// Streaming text chunk from AI
3535
/// </summary>
36-
public sealed record Chunk(
36+
public sealed record MessageChunk(
3737
string Id,
3838
long Timestamp,
3939
string Content
@@ -42,7 +42,7 @@ string Content
4242
/// <summary>
4343
/// Complete message when streaming is done
4444
/// </summary>
45-
public sealed record ChunkComplete(
45+
public sealed record MessageComplete(
4646
string Id,
4747
long Timestamp,
4848
string FullContent
@@ -111,8 +111,8 @@ string Message
111111
/// </summary>
112112
[JsonSerializable(typeof(AskAiEvent))]
113113
[JsonSerializable(typeof(AskAiEvent.ConversationStart))]
114-
[JsonSerializable(typeof(AskAiEvent.Chunk))]
115-
[JsonSerializable(typeof(AskAiEvent.ChunkComplete))]
114+
[JsonSerializable(typeof(AskAiEvent.MessageChunk))]
115+
[JsonSerializable(typeof(AskAiEvent.MessageComplete))]
116116
[JsonSerializable(typeof(AskAiEvent.SearchToolCall))]
117117
[JsonSerializable(typeof(AskAiEvent.ToolCall))]
118118
[JsonSerializable(typeof(AskAiEvent.ToolResult))]

src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information
44

55
using System.Diagnostics;
6+
using System.Text.Json;
7+
using Elastic.Documentation.Api.Core;
68
using Microsoft.Extensions.Logging;
79

810
namespace Elastic.Documentation.Api.Core.AskAi;
@@ -16,41 +18,22 @@ public class AskAiUsecase(
1618

1719
public async Task<Stream> AskAi(AskAiRequest askAiRequest, Cancel ctx)
1820
{
19-
// Start activity for the chat request - DO NOT use 'using' because the stream is consumed later
20-
// The activity will be passed to the transformer which will dispose it when the stream completes
21-
var activity = AskAiActivitySource.StartActivity("chat", ActivityKind.Client);
22-
23-
// Generate a correlation ID for tracking if this is a new conversation
24-
// For first messages (no ThreadId), generate a temporary ID that will be updated when the provider responds
25-
var correlationId = askAiRequest.ThreadId ?? $"temp-{Guid.NewGuid()}";
26-
27-
// Set GenAI semantic convention attributes
28-
_ = (activity?.SetTag("gen_ai.operation.name", "chat"));
29-
_ = (activity?.SetTag("gen_ai.conversation.id", correlationId)); // Will be updated when we receive ConversationStart with actual ID
30-
_ = (activity?.SetTag("gen_ai.usage.input_tokens", askAiRequest.Message.Length)); // Approximate token count
31-
32-
// Custom attributes for tracking our abstraction layer
33-
// We use custom attributes because we don't know the actual GenAI provider (OpenAI, Anthropic, etc.)
34-
// or model (gpt-4, claude, etc.) - those are abstracted by AgentBuilder/LlmGateway
35-
_ = (activity?.SetTag("docs.ai.gateway", streamTransformer.AgentProvider)); // agent-builder or llm-gateway
36-
_ = (activity?.SetTag("docs.ai.agent_name", streamTransformer.AgentId)); // docs-agent or docs_assistant
37-
38-
// Add GenAI prompt event
39-
_ = (activity?.AddEvent(new ActivityEvent("gen_ai.content.prompt",
40-
timestamp: DateTimeOffset.UtcNow,
41-
tags:
42-
[
43-
new KeyValuePair<string, object?>("gen_ai.prompt", askAiRequest.Message),
44-
new KeyValuePair<string, object?>("gen_ai.system_instructions", AskAiRequest.SystemPrompt)
45-
])));
46-
47-
logger.LogDebug("Processing AskAiRequest: {Request}", askAiRequest);
48-
21+
logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId);
22+
var activity = AskAiActivitySource.StartActivity($"chat", ActivityKind.Client);
23+
_ = activity?.SetTag("gen_ai.operation.name", "chat");
24+
_ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); // agent-builder or llm-gateway
25+
_ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); // docs-agent or docs_assistant
26+
var inputMessages = new[]
27+
{
28+
new InputMessage("user", [new MessagePart("text", askAiRequest.Message)])
29+
};
30+
var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray);
31+
_ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson);
32+
logger.LogInformation("AskAI input message: {InputMessage}", askAiRequest.Message);
33+
logger.LogInformation("Streaming AskAI response");
4934
var rawStream = await askAiGateway.AskAi(askAiRequest, ctx);
50-
5135
// The stream transformer will handle disposing the activity when streaming completes
5236
var transformedStream = await streamTransformer.TransformAsync(rawStream, activity, ctx);
53-
5437
return transformedStream;
5538
}
5639
}

src/api/Elastic.Documentation.Api.Core/SerializationContext.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,21 @@
88

99
namespace Elastic.Documentation.Api.Core;
1010

11+
/// <summary>
12+
/// Types for OpenTelemetry telemetry serialization (AOT-compatible)
13+
/// </summary>
14+
public record MessagePart(string Type, string Content);
15+
16+
public record InputMessage(string Role, MessagePart[] Parts);
17+
18+
public record OutputMessage(string Role, MessagePart[] Parts, string FinishReason);
1119

1220
[JsonSerializable(typeof(AskAiRequest))]
1321
[JsonSerializable(typeof(SearchRequest))]
1422
[JsonSerializable(typeof(SearchResponse))]
23+
[JsonSerializable(typeof(InputMessage))]
24+
[JsonSerializable(typeof(OutputMessage))]
25+
[JsonSerializable(typeof(MessagePart))]
26+
[JsonSerializable(typeof(InputMessage[]))]
1527
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
1628
public partial class ApiJsonContext : JsonSerializerContext;

src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ public class AgentBuilderStreamTransformer(ILogger<AgentBuilderStreamTransformer
2626

2727
// Special handling for error events - they may have a different structure
2828
if (type == "error")
29-
{
3029
return ParseErrorEventFromRoot(id, timestamp, json);
31-
}
3230

3331
// Most Agent Builder events have data nested in a "data" property
3432
if (!json.TryGetProperty("data", out var innerData))
@@ -43,10 +41,10 @@ public class AgentBuilderStreamTransformer(ILogger<AgentBuilderStreamTransformer
4341
new AskAiEvent.ConversationStart(id, timestamp, convId.GetString()!),
4442

4543
"message_chunk" when innerData.TryGetProperty("text_chunk", out var textChunk) =>
46-
new AskAiEvent.Chunk(id, timestamp, textChunk.GetString()!),
44+
new AskAiEvent.MessageChunk(id, timestamp, textChunk.GetString()!),
4745

4846
"message_complete" when innerData.TryGetProperty("message_content", out var fullContent) =>
49-
new AskAiEvent.ChunkComplete(id, timestamp, fullContent.GetString()!),
47+
new AskAiEvent.MessageComplete(id, timestamp, fullContent.GetString()!),
5048

5149
"reasoning" =>
5250
// Parse reasoning message if available
@@ -76,7 +74,7 @@ public class AgentBuilderStreamTransformer(ILogger<AgentBuilderStreamTransformer
7674
return null;
7775
}
7876

79-
private AskAiEvent.Reasoning ParseReasoningEvent(string id, long timestamp, JsonElement innerData)
77+
private static AskAiEvent.Reasoning ParseReasoningEvent(string id, long timestamp, JsonElement innerData)
8078
{
8179
// Agent Builder sends: {"data":{"reasoning":"..."}}
8280
var message = innerData.TryGetProperty("reasoning", out var reasoningProp)
@@ -86,7 +84,7 @@ private AskAiEvent.Reasoning ParseReasoningEvent(string id, long timestamp, Json
8684
return new AskAiEvent.Reasoning(id, timestamp, message ?? "Thinking...");
8785
}
8886

89-
private AskAiEvent.ToolResult ParseToolResultEvent(string id, long timestamp, JsonElement innerData)
87+
private static AskAiEvent.ToolResult ParseToolResultEvent(string id, long timestamp, JsonElement innerData)
9088
{
9189
// Extract tool_call_id and results
9290
var toolCallId = innerData.TryGetProperty("tool_call_id", out var tcId) ? tcId.GetString() : id;
@@ -99,7 +97,7 @@ private AskAiEvent.ToolResult ParseToolResultEvent(string id, long timestamp, Js
9997
return new AskAiEvent.ToolResult(id, timestamp, toolCallId ?? id, result);
10098
}
10199

102-
private AskAiEvent ParseToolCallEvent(string id, long timestamp, JsonElement innerData)
100+
private static AskAiEvent ParseToolCallEvent(string id, long timestamp, JsonElement innerData)
103101
{
104102
// Extract fields from Agent Builder's tool_call structure
105103
var toolCallId = innerData.TryGetProperty("tool_call_id", out var tcId) ? tcId.GetString() : id;
@@ -128,16 +126,13 @@ private AskAiEvent ParseToolCallEvent(string id, long timestamp, JsonElement inn
128126
return new AskAiEvent.ToolCall(id, timestamp, toolCallId ?? id, toolId ?? "unknown", args);
129127
}
130128

131-
private AskAiEvent.ErrorEvent ParseErrorEventFromRoot(string id, long timestamp, JsonElement root)
129+
private static AskAiEvent.ErrorEvent ParseErrorEventFromRoot(string id, long timestamp, JsonElement root)
132130
{
133131
// Agent Builder sends: {"error":{"code":"...","message":"...","meta":{...}}}
134132
var errorMessage = root.TryGetProperty("error", out var errorProp) &&
135133
errorProp.TryGetProperty("message", out var msgProp)
136134
? msgProp.GetString()
137135
: null;
138-
139-
Logger.LogError("Error event received from Agent Builder: {ErrorMessage}", errorMessage ?? "Unknown error");
140-
141136
return new AskAiEvent.ErrorEvent(id, timestamp, errorMessage ?? "Unknown error occurred");
142137
}
143138
}

src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ public class LlmGatewayStreamTransformer(ILogger<LlmGatewayStreamTransformer> lo
4141
new AskAiEvent.ConversationStart(id, timestamp, Guid.NewGuid().ToString()),
4242

4343
"ai_message_chunk" when messageData.TryGetProperty("content", out var content) =>
44-
new AskAiEvent.Chunk(id, timestamp, content.GetString()!),
44+
new AskAiEvent.MessageChunk(id, timestamp, content.GetString()!),
4545

4646
"ai_message" when messageData.TryGetProperty("content", out var fullContent) =>
47-
new AskAiEvent.ChunkComplete(id, timestamp, fullContent.GetString()!),
47+
new AskAiEvent.MessageComplete(id, timestamp, fullContent.GetString()!),
4848

4949
"tool_call" when messageData.TryGetProperty("toolCalls", out var toolCalls) =>
5050
TransformToolCall(id, timestamp, toolCalls),
@@ -56,6 +56,8 @@ public class LlmGatewayStreamTransformer(ILogger<LlmGatewayStreamTransformer> lo
5656
"agent_end" =>
5757
new AskAiEvent.ConversationEnd(id, timestamp),
5858

59+
"error" => ParseErrorEvent(id, timestamp, messageData),
60+
5961
"chat_model_start" or "chat_model_end" =>
6062
null, // Skip model lifecycle events
6163

@@ -110,4 +112,18 @@ public class LlmGatewayStreamTransformer(ILogger<LlmGatewayStreamTransformer> lo
110112
Logger.LogWarning("Unknown LLM Gateway event type: {Type}", type);
111113
return null;
112114
}
115+
116+
private AskAiEvent.ErrorEvent ParseErrorEvent(string id, long timestamp, JsonElement messageData)
117+
{
118+
// LLM Gateway error format: {error: "...", message: "..."}
119+
var errorMessage = messageData.TryGetProperty("message", out var msgProp)
120+
? msgProp.GetString()
121+
: messageData.TryGetProperty("error", out var errProp)
122+
? errProp.GetString()
123+
: null;
124+
125+
Logger.LogError("Error event received from LLM Gateway: {ErrorMessage}", errorMessage ?? "Unknown error");
126+
127+
return new AskAiEvent.ErrorEvent(id, timestamp, errorMessage ?? "Unknown error occurred");
128+
}
113129
}

0 commit comments

Comments
 (0)