Skip to content

Commit fc63890

Browse files
patnikoCopilot
andauthored
docs: fix OpenTelemetry guide to use correct SDK APIs (#597)
* docs: fix OpenTelemetry guide to use correct SDK APIs - CopilotClient() takes CopilotClientOptions, not SessionConfig; model is set on create_session() instead - session.send() returns a message ID, not an async iterator; events are received via session.on(handler) callbacks - session.send() takes a dict {"prompt": ...}, not a bare string - Add required on_permission_request to all create_session() calls - Fix imports: SessionConfig → PermissionHandler - Rewrite complete example with correct event subscription pattern Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: skip validation for contextual code snippets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 942a253 commit fc63890

File tree

1 file changed

+119
-73
lines changed

1 file changed

+119
-73
lines changed

docs/opentelemetry-instrumentation.md

Lines changed: 119 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@ tracer = trace.get_tracer(__name__)
4949
### 2. Create Spans Around Agent Operations
5050

5151
```python
52-
from copilot import CopilotClient, SessionConfig
52+
from copilot import CopilotClient, PermissionHandler
53+
from copilot.generated.session_events import SessionEventType
5354
from opentelemetry import trace, context
5455
from opentelemetry.trace import SpanKind
5556

56-
# Initialize client
57-
client = CopilotClient(SessionConfig(model="gpt-5"))
57+
# Initialize client and start the CLI server
58+
client = CopilotClient()
59+
await client.start()
60+
5861
tracer = trace.get_tracer(__name__)
5962

6063
# Create a span for the agent invocation
@@ -73,18 +76,34 @@ span = tracer.start_span(
7376
token = context.attach(trace.set_span_in_context(span))
7477

7578
try:
76-
# Your agent code here
77-
async for event in session.send("Hello, world!"):
78-
# Process events and add attributes
79-
pass
79+
# Create a session (model is set here, not on the client)
80+
session = await client.create_session({
81+
"model": "gpt-5",
82+
"on_permission_request": PermissionHandler.approve_all,
83+
})
84+
85+
# Subscribe to events via callback
86+
def handle_event(event):
87+
if event.type == SessionEventType.ASSISTANT_USAGE:
88+
if event.data.model:
89+
span.set_attribute("gen_ai.response.model", event.data.model)
90+
91+
unsubscribe = session.on(handle_event)
92+
93+
# Send a message (returns a message ID)
94+
await session.send({"prompt": "Hello, world!"})
95+
96+
# Or send and wait for the session to become idle
97+
response = await session.send_and_wait({"prompt": "Hello, world!"})
8098
finally:
8199
context.detach(token)
82100
span.end()
101+
await client.stop()
83102
```
84103

85104
## Copilot SDK Event to GenAI Attribute Mapping
86105

87-
The Copilot SDK emits `SessionEventType` events during agent execution. Here's how to map these events to GenAI semantic convention attributes:
106+
The Copilot SDK emits `SessionEventType` events during agent execution. Subscribe to these events using `session.on(handler)`, which returns an unsubscribe function. Here's how to map these events to GenAI semantic convention attributes:
88107

89108
### Core Session Events
90109

@@ -131,7 +150,7 @@ When you receive an `ASSISTANT_USAGE` event, extract token usage:
131150
```python
132151
from copilot.generated.session_events import SessionEventType
133152

134-
async for event in session.send("Hello"):
153+
def handle_usage(event):
135154
if event.type == SessionEventType.ASSISTANT_USAGE:
136155
data = event.data
137156
if data.model:
@@ -140,9 +159,13 @@ async for event in session.send("Hello"):
140159
span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens))
141160
if data.output_tokens is not None:
142161
span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens))
162+
163+
unsubscribe = session.on(handle_usage)
164+
await session.send({"prompt": "Hello"})
143165
```
144166

145167
**Event Data Structure:**
168+
<!-- docs-validate: skip -->
146169
```python
147170
@dataclass
148171
class Usage:
@@ -168,43 +191,43 @@ import json
168191
# Dictionary to track active tool spans
169192
tool_spans = {}
170193

171-
async for event in session.send("What's the weather?"):
194+
def handle_tool_events(event):
172195
data = event.data
173-
196+
174197
if event.type == SessionEventType.TOOL_EXECUTION_START and data:
175198
call_id = data.tool_call_id or str(uuid.uuid4())
176199
tool_name = data.tool_name or "unknown"
177-
200+
178201
tool_attrs = {
179202
"gen_ai.tool.name": tool_name,
180203
"gen_ai.operation.name": "execute_tool",
181204
}
182-
205+
183206
if call_id:
184207
tool_attrs["gen_ai.tool.call.id"] = call_id
185-
208+
186209
# Optional: include tool arguments (may contain sensitive data)
187210
if data.arguments is not None:
188211
try:
189212
tool_attrs["gen_ai.tool.call.arguments"] = json.dumps(data.arguments)
190213
except Exception:
191214
tool_attrs["gen_ai.tool.call.arguments"] = str(data.arguments)
192-
215+
193216
tool_span = tracer.start_span(
194217
name=f"execute_tool {tool_name}",
195218
kind=SpanKind.CLIENT,
196219
attributes=tool_attrs
197220
)
198221
tool_token = context.attach(trace.set_span_in_context(tool_span))
199222
tool_spans[call_id] = (tool_span, tool_token)
200-
223+
201224
elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
202225
call_id = data.tool_call_id
203226
entry = tool_spans.pop(call_id, None) if call_id else None
204-
227+
205228
if entry:
206229
tool_span, tool_token = entry
207-
230+
208231
# Optional: include tool result (may contain sensitive data)
209232
if data.result is not None:
210233
try:
@@ -213,13 +236,16 @@ async for event in session.send("What's the weather?"):
213236
result_str = str(data.result)
214237
# Truncate to 512 chars to avoid huge spans
215238
tool_span.set_attribute("gen_ai.tool.call.result", result_str[:512])
216-
239+
217240
# Mark as error if tool failed
218241
if hasattr(data, "success") and data.success is False:
219242
tool_span.set_attribute("error.type", "tool_error")
220-
243+
221244
context.detach(tool_token)
222245
tool_span.end()
246+
247+
unsubscribe = session.on(handle_tool_events)
248+
await session.send({"prompt": "What's the weather?"})
223249
```
224250

225251
**Tool Event Data:**
@@ -233,7 +259,7 @@ async for event in session.send("What's the weather?"):
233259
Capture the final message as a span event:
234260

235261
```python
236-
async for event in session.send("Tell me a joke"):
262+
def handle_message(event):
237263
if event.type == SessionEventType.ASSISTANT_MESSAGE and event.data:
238264
if event.data.content:
239265
# Add as a span event (opt-in for content recording)
@@ -246,6 +272,9 @@ async for event in session.send("Tell me a joke"):
246272
})
247273
}
248274
)
275+
276+
unsubscribe = session.on(handle_message)
277+
await session.send({"prompt": "Tell me a joke"})
249278
```
250279

251280
## Complete Example
@@ -254,7 +283,7 @@ async for event in session.send("Tell me a joke"):
254283
import asyncio
255284
import json
256285
import uuid
257-
from copilot import CopilotClient, SessionConfig
286+
from copilot import CopilotClient, PermissionHandler
258287
from copilot.generated.session_events import SessionEventType
259288
from opentelemetry import trace, context
260289
from opentelemetry.trace import SpanKind
@@ -269,72 +298,85 @@ tracer = trace.get_tracer(__name__)
269298

270299
async def invoke_agent(prompt: str):
271300
"""Invoke agent with full OpenTelemetry instrumentation."""
272-
301+
273302
# Create main span
274303
span_attrs = {
275304
"gen_ai.operation.name": "invoke_agent",
276305
"gen_ai.provider.name": "github.copilot",
277306
"gen_ai.agent.name": "example-agent",
278307
"gen_ai.request.model": "gpt-5",
279308
}
280-
309+
281310
span = tracer.start_span(
282311
name="invoke_agent example-agent",
283312
kind=SpanKind.CLIENT,
284313
attributes=span_attrs
285314
)
286315
token = context.attach(trace.set_span_in_context(span))
287316
tool_spans = {}
288-
317+
289318
try:
290-
client = CopilotClient(SessionConfig(model="gpt-5"))
291-
async with client.create_session() as session:
292-
async for event in session.send(prompt):
293-
data = event.data
294-
295-
# Handle usage events
296-
if event.type == SessionEventType.ASSISTANT_USAGE and data:
297-
if data.model:
298-
span.set_attribute("gen_ai.response.model", data.model)
299-
if data.input_tokens is not None:
300-
span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens))
301-
if data.output_tokens is not None:
302-
span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens))
303-
304-
# Handle tool execution
305-
elif event.type == SessionEventType.TOOL_EXECUTION_START and data:
306-
call_id = data.tool_call_id or str(uuid.uuid4())
307-
tool_name = data.tool_name or "unknown"
308-
309-
tool_attrs = {
310-
"gen_ai.tool.name": tool_name,
311-
"gen_ai.operation.name": "execute_tool",
312-
"gen_ai.tool.call.id": call_id,
313-
}
314-
315-
tool_span = tracer.start_span(
316-
name=f"execute_tool {tool_name}",
317-
kind=SpanKind.CLIENT,
318-
attributes=tool_attrs
319-
)
320-
tool_token = context.attach(trace.set_span_in_context(tool_span))
321-
tool_spans[call_id] = (tool_span, tool_token)
322-
323-
elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
324-
call_id = data.tool_call_id
325-
entry = tool_spans.pop(call_id, None) if call_id else None
326-
if entry:
327-
tool_span, tool_token = entry
328-
context.detach(tool_token)
329-
tool_span.end()
330-
331-
# Capture final message
332-
elif event.type == SessionEventType.ASSISTANT_MESSAGE and data:
333-
if data.content:
334-
print(f"Assistant: {data.content}")
335-
319+
client = CopilotClient()
320+
await client.start()
321+
322+
session = await client.create_session({
323+
"model": "gpt-5",
324+
"on_permission_request": PermissionHandler.approve_all,
325+
})
326+
327+
# Subscribe to events via callback
328+
def handle_event(event):
329+
data = event.data
330+
331+
# Handle usage events
332+
if event.type == SessionEventType.ASSISTANT_USAGE and data:
333+
if data.model:
334+
span.set_attribute("gen_ai.response.model", data.model)
335+
if data.input_tokens is not None:
336+
span.set_attribute("gen_ai.usage.input_tokens", int(data.input_tokens))
337+
if data.output_tokens is not None:
338+
span.set_attribute("gen_ai.usage.output_tokens", int(data.output_tokens))
339+
340+
# Handle tool execution
341+
elif event.type == SessionEventType.TOOL_EXECUTION_START and data:
342+
call_id = data.tool_call_id or str(uuid.uuid4())
343+
tool_name = data.tool_name or "unknown"
344+
345+
tool_attrs = {
346+
"gen_ai.tool.name": tool_name,
347+
"gen_ai.operation.name": "execute_tool",
348+
"gen_ai.tool.call.id": call_id,
349+
}
350+
351+
tool_span = tracer.start_span(
352+
name=f"execute_tool {tool_name}",
353+
kind=SpanKind.CLIENT,
354+
attributes=tool_attrs
355+
)
356+
tool_token = context.attach(trace.set_span_in_context(tool_span))
357+
tool_spans[call_id] = (tool_span, tool_token)
358+
359+
elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
360+
call_id = data.tool_call_id
361+
entry = tool_spans.pop(call_id, None) if call_id else None
362+
if entry:
363+
tool_span, tool_token = entry
364+
context.detach(tool_token)
365+
tool_span.end()
366+
367+
# Capture final message
368+
elif event.type == SessionEventType.ASSISTANT_MESSAGE and data:
369+
if data.content:
370+
print(f"Assistant: {data.content}")
371+
372+
unsubscribe = session.on(handle_event)
373+
374+
# Send message and wait for completion
375+
response = await session.send_and_wait({"prompt": prompt})
376+
336377
span.set_attribute("gen_ai.response.finish_reasons", ["stop"])
337-
378+
unsubscribe()
379+
338380
except Exception as e:
339381
span.set_attribute("error.type", type(e).__name__)
340382
raise
@@ -344,9 +386,10 @@ async def invoke_agent(prompt: str):
344386
tool_span.set_attribute("error.type", "stream_aborted")
345387
context.detach(tool_token)
346388
tool_span.end()
347-
389+
348390
context.detach(token)
349391
span.end()
392+
await client.stop()
350393

351394
# Run
352395
asyncio.run(invoke_agent("What's 2+2?"))
@@ -388,6 +431,7 @@ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
388431

389432
### Checking at Runtime
390433

434+
<!-- docs-validate: skip -->
391435
```python
392436
import os
393437

@@ -403,6 +447,7 @@ if should_record_content() and event.data.content:
403447

404448
For MCP-based tools, add these additional attributes following the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):
405449

450+
<!-- docs-validate: skip -->
406451
```python
407452
tool_attrs = {
408453
# Required
@@ -507,6 +552,7 @@ View traces in the Azure Portal under your Application Insights resource → Tra
507552
### Tool spans not showing as children
508553

509554
Make sure to attach the tool span to the parent context:
555+
<!-- docs-validate: skip -->
510556
```python
511557
tool_token = context.attach(trace.set_span_in_context(tool_span))
512558
```

0 commit comments

Comments
 (0)