DevOps
OpenTelemetry GenAI Semantic Conventions: Span Agent di Tempo
Oktober 202613 menit baca

Ini adalah aturan penamaan OpenTelemetry untuk span, metric dan event dari aplikasi generative AI, mencakup model call, agent, tool dan MCP. Konvensinya kini berada di repository open-telemetry/semantic-conventions-genai. Semua dokumen di sana masih berstatus Development, jadi nama atribut bisa berubah antar-release.
invoke_agent mewakili satu pemanggilan agent, dengan kind INTERNAL untuk agent di dalam proses dan CLIENT untuk agent hosted yang remote. invoke_workflow mewakili run yang dihadapi pengguna dan mengoordinasikan beberapa agent atau GenAI call, misalnya run OpenAI Agents dengan handoff. Spec menyatakan pemanggilan agent tunggal sebaiknya tidak dilaporkan sebagai workflow.
Tidak. Model call selesai saat mengembalikan permintaan tool call, dan tool baru berjalan sesudahnya. Konvensi menggambarkan span tool sebagai sibling di bawah span invoke_agent yang sama. Menaruhnya di bawah span chat membuat durasi span chat membengkak dan menyembunyikan latency tool di dalam latency model.
Daftarkan TracingProcessor yang mengubah span SDK menjadi span OpenTelemetry dan mengekspornya lewat OTLP ke collector seperti Alloy yang meneruskan ke Tempo. add_trace_processor tetap mempertahankan upload default ke dashboard trace OpenAI, sedangkan set_trace_processors menggantinya. Paket resmi opentelemetry-instrumentation-genai-openai-agents bisa dipakai sebagai alternatif menulis processor sendiri.
Tidak. Atribut seperti gen_ai.input.messages, gen_ai.output.messages dan gen_ai.tool.call.arguments berstatus Opt-In, dan konvensi menyatakan instrumentasi sebaiknya tidak merekamnya secara default. Untuk production, spec menyarankan mengunggah konten ke storage eksternal dan mencatat referensinya di span. Bila Anda merekam konten di span pada Tempo, max_attribute_bytes default 2048 akan memotong nilai yang lebih panjang.

Ringkasan Utama
OpenTelemetry GenAI semantic conventions memodelkan satu run agent sebagai span invoke_workflow atau invoke_agent, dengan span chat dan execute_tool sebagai sibling di bawahnya. TracingProcessor kustom di OpenAI Agents SDK bisa mengirim bentuk itu ke Grafana Tempo, tetapi isi pesan bersifat opt-in, dan secara default Tempo memotong nilai atribut di atas 2048 byte.
Bayangkan helpdesk ERP tempat agent sengketa invoice menerima tiket dari agent triase, lalu menyerahkannya ke agent pencocokan accounts payable yang memanggil tiga tool ke modul purchasing. Seorang pengguna melapor bahwa agent butuh empat puluh detik dan memberi jawaban yang salah. Trace layanan di Tempo hanya menampilkan satu HTTP request panjang dan sederet query database: tidak ada agent, tidak ada model call, tidak ada tool, sehingga tidak bisa diketahui apakah waktunya habis di model, di tool yang berulang, atau di handoff yang seharusnya tidak terjadi.
OpenTelemetry GenAI semantic conventions dibuat untuk mengisi celah itu. Tulisan ini membahas apa yang didefinisikan konvensi span agent saat ini, bentuk pohon span yang seharusnya dihasilkan satu run agent, trace processor Python lengkap yang memetakan span OpenAI Agents SDK ke konvensi tersebut dan mengekspornya lewat OTLP, cara menangani capture pesan tanpa membocorkan data ERP, serta query TraceQL yang membuat hasilnya berguna. Setiap nama atribut dan requirement level di bawah diambil langsung dari repository konvensinya, yang masih menandai semuanya berstatus Development.
Konvensi GenAI kini berada di repository tersendiri, open-telemetry/semantic-conventions-genai, dan halaman lama di opentelemetry.io mengarah ke sana dengan catatan bahwa halaman itu tidak lagi dipelihara. Semua dokumen di dalamnya berstatus Development, artinya nama atribut masih bisa berubah antar-release. Di dalamnya, dokumen agent mendefinisikan enam operasi, masing-masing diidentifikasi oleh gen_ai.operation.name dan nama span yang dibentuk darinya.
| gen_ai.operation.name | Nama span | Kind | Kapan dipakai |
|---|---|---|---|
invoke_workflow | invoke_workflow {gen_ai.workflow.name} | INTERNAL | Entry point yang dihadapi pengguna dan mengoordinasikan beberapa agent atau GenAI call, misalnya run dengan handoff. Bukan untuk agent tunggal. |
invoke_agent | invoke_agent {gen_ai.agent.name} | INTERNAL / CLIENT | Satu pemanggilan agent. INTERNAL bila agent berjalan di proses Anda, CLIENT bila agent itu hosted dan remote. |
create_agent | create_agent {gen_ai.agent.name} | CLIENT | Membuat resource hosted agent di provider, misalnya agent Bedrock. |
plan | plan {gen_ai.agent.name} | INTERNAL | Hanya bila instrumentasi bisa membedakan tahap planning dari inference biasa secara andal. |
chat | chat {gen_ai.request.model} | CLIENT | Setiap model call. Didefinisikan di dokumen model spans, yang diperluas oleh dokumen agent. |
execute_tool | execute_tool {gen_ai.tool.name} | INTERNAL | Setiap eksekusi tool. Load dan read Agent Skills adalah refinement dari span ini, bukan span terpisah. |
Requirement level lebih penting daripada namanya. Di semua span ini hanya gen_ai.operation.name yang Required, ditambah gen_ai.provider.name pada span yang memanggil provider. Nama agent, deskripsi, versi dan conversation id berstatus Conditionally Required, artinya diisi bila datanya ada. Jumlah token dan parameter request berstatus Recommended. Semua yang membawa konten, dari gen_ai.input.messages sampai gen_ai.tool.call.arguments, berstatus Opt-In, dan dokumen model spans menyatakan instrumentasi sebaiknya tidak merekamnya secara default.
Untuk contoh sengketa invoice, konvensi mengarah ke pohon seperti di bawah. Root-nya invoke_workflow karena run ini melibatkan handoff, dan spec menyebut Runner.run di OpenAI Agents dengan handoff, sub-agent atau agents-as-tools sebagai contoh workflow. Agent tunggal tanpa delegasi memakai invoke_agent sebagai root.
invoke_workflow invoice_dispute INTERNAL gen_ai.conversation.id=TCK-2026-10-0193
├─ invoke_agent Triage INTERNAL gen_ai.agent.name=Triage
│ ├─ chat <model> CLIENT gen_ai.usage.input_tokens=1840
│ └─ agent.handoff INTERNAL (no GenAI operation exists for this)
└─ invoke_agent AP Matcher INTERNAL gen_ai.agent.name=AP Matcher
├─ chat <model> CLIENT finish: tool call
├─ execute_tool get_purchase_order INTERNAL sibling of the chat span, not its child
├─ execute_tool get_goods_receipt INTERNAL
└─ chat <model> CLIENT final answer
# Wrong: execute_tool nested under the chat span that requested it.
# The model call has already returned when the tool runs; nesting it there
# inflates the chat span's duration and hides tool latency inside model latency.Aturan struktur yang paling sering salah adalah letak span tool. Model call mengembalikan permintaan tool call lalu selesai; framework kemudian menjalankan tool, dan model call berikutnya dimulai setelahnya. Deskripsi plan span di konvensi menegaskan bentuk yang dimaksud: span tool atau task biasanya menjadi operasi sibling di bawah span invoke_agent yang sama. Menaruh execute_tool di bawah span chat yang memintanya membuat span chat terlihat selama tool berjalan, dan itu merusak semua dashboard latency yang dibangun dari span chat.
Handoff sama sekali tidak punya operasi GenAI. Nilai well-known mencakup chat, embeddings, retrieval, operasi memory, create_agent, invoke_agent, invoke_workflow, plan dan execute_tool, tetapi tidak ada untuk pemindahan kendali antar-agent. Span invoke_agent kedua sudah menunjukkan bahwa perpindahan terjadi; span handoff dipertahankan sebagai span INTERNAL biasa agar momen perpindahannya tetap terlihat, tanpa berpura-pura termasuk dalam konvensi.
OpenAI Agents SDK men-trace setiap run secara default dan meneruskan setiap trace dan span ke processor yang terdaftar lewat empat callback: on_trace_start, on_trace_end, on_span_start dan on_span_end. Setiap span membawa span_id, parent_id, timestamp, error opsional, dan objek span_data bertipe. Pemetaannya sebagian besar satu-satu, dengan dua pengecualian yang perlu diputuskan sejak awal.
| Objek SDK | Operasi GenAI | Catatan |
|---|---|---|
Trace | invoke_workflow | Nama trace adalah RunConfig.workflow_name, yang default-nya string literal Agent workflow, jadi isilah. group_id menjadi gen_ai.conversation.id. |
AgentSpanData | invoke_agent | AgentSpanData berisi name, handoffs, tools dan output_type. Hanya name yang punya padanan atribut konvensi. |
ResponseSpanData / GenerationSpanData | chat | Model Responses API default menghasilkan ResponseSpanData berisi objek Response dan usage; model Chat Completions menghasilkan GenerationSpanData berisi model dan usage. |
FunctionSpanData | execute_tool | FunctionSpanData menyimpan nama tool beserta input dan output, yang kosong bila capture data sensitif dimatikan. |
HandoffSpanData / GuardrailSpanData | Tidak didefinisikan | Dikirim sebagai span biasa dengan atribut ber-prefix vendor, agar backend yang paham GenAI tidak salah membacanya. |
TaskSpanData / TurnSpanData | Dilewati | Versi SDK terbaru menambahkan span task dan turn di sekitar setiap run dan setiap giliran model. Keduanya tidak punya konvensi, jadi child-nya dipindahkan ke ancestor terdekat yang dipetakan. |
Melewati span task dan turn adalah keputusan desain, bukan kewajiban. Mempertahankannya menambah dua level antara agent dan model call-nya, dan query struktural TraceQL seperti span agent yang langsung diikuti span tool akan berhenti cocok. Bila pengelompokan per giliran penting bagi Anda, kirim keduanya sebagai span INTERNAL biasa dan tulis query dengan operator descendant, bukan operator child.
Processor di bawah adalah subclass TracingProcessor, menyimpan dictionary dari id SDK ke span OpenTelemetry, dan memulai setiap span OpenTelemetry dengan parent context eksplisit yang diambil dari dictionary itu. Bagian terakhir itulah intinya: SDK melacak span aktifnya sendiri di contextvar Python, sehingga mengandalkan current context OpenTelemetry akan menempelkan span ke HTTP request span yang kebetulan aktif, atau tidak ke mana pun.
# genai_processor.py
# pip install openai-agents opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
import threading
from typing import Any
from agents.tracing import (
AgentSpanData, FunctionSpanData, GenerationSpanData, GuardrailSpanData,
HandoffSpanData, ResponseSpanData, Span, Trace, TracingProcessor,
)
from opentelemetry import trace as otel_trace
from opentelemetry.trace import SpanKind, Status, StatusCode
class GenAISemconvProcessor(TracingProcessor):
"""Re-emit Agents SDK traces as OpenTelemetry GenAI spans (semconv status: Development)."""
def __init__(self, tracer_provider, capture_content: bool = False):
self._provider = tracer_provider
self._tracer = otel_trace.get_tracer("erp.agents.genai", tracer_provider=tracer_provider)
self._capture = capture_content
self._otel: dict[str, otel_trace.Span] = {} # SDK trace_id / span_id -> OTel span
self._alias: dict[str, str] = {} # skipped SDK span -> nearest mapped ancestor
self._agent: dict[str, str] = {} # mapped key -> agent name, for tool spans
self._lock = threading.Lock() # callbacks arrive from concurrent runs
# -- trace -> invoke_workflow root ---------------------------------------------
def on_trace_start(self, trace: Trace) -> None:
attrs = {"gen_ai.operation.name": "invoke_workflow", "gen_ai.workflow.name": trace.name}
group_id = getattr(trace, "group_id", None)
if group_id: # a real ticket/session id only -- the spec forbids inventing one
attrs["gen_ai.conversation.id"] = group_id
root = self._tracer.start_span(
f"invoke_workflow {trace.name}", kind=SpanKind.INTERNAL, attributes=attrs)
with self._lock:
self._otel[trace.trace_id] = root
def on_trace_end(self, trace: Trace) -> None:
with self._lock:
root = self._otel.pop(trace.trace_id, None)
if root is not None:
root.end()
# -- spans ---------------------------------------------------------------------
def on_span_start(self, span: Span[Any]) -> None:
data = span.span_data
with self._lock:
parent_key = span.parent_id or span.trace_id
parent_key = self._alias.get(parent_key, parent_key)
described = self._describe(data, self._agent.get(parent_key))
if described is None:
# task/turn spans: re-parent their children onto the nearest mapped
# ancestor, so Tempo shows agent > chat, not agent > turn > chat.
self._alias[span.span_id] = parent_key
return
name, kind, attrs = described
parent = self._otel.get(parent_key)
ctx = otel_trace.set_span_in_context(parent) if parent is not None else None
self._otel[span.span_id] = self._tracer.start_span(
name, context=ctx, kind=kind, attributes=attrs) # sampling sees these
self._agent[span.span_id] = (
data.name if isinstance(data, AgentSpanData) else self._agent.get(parent_key, ""))
def _describe(self, data, agent_name):
if isinstance(data, AgentSpanData):
return (f"invoke_agent {data.name}", SpanKind.INTERNAL,
{"gen_ai.operation.name": "invoke_agent", "gen_ai.agent.name": data.name})
if isinstance(data, (ResponseSpanData, GenerationSpanData)):
api = "responses" if isinstance(data, ResponseSpanData) else "chat_completions"
return ("chat", SpanKind.CLIENT, # renamed in on_span_end once the model is known
{"gen_ai.operation.name": "chat", "gen_ai.provider.name": "openai",
"openai.api.type": api})
if isinstance(data, FunctionSpanData):
attrs = {"gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": data.name,
"gen_ai.tool.type": "function"}
if agent_name:
attrs["gen_ai.agent.name"] = agent_name
return (f"execute_tool {data.name}", SpanKind.INTERNAL, attrs)
if isinstance(data, HandoffSpanData):
return ("agent.handoff", SpanKind.INTERNAL, {}) # no GenAI operation: no gen_ai.*
if isinstance(data, GuardrailSpanData):
return (f"guardrail {data.name}", SpanKind.INTERNAL, {})
return None
def on_span_end(self, span: Span[Any]) -> None:
with self._lock:
self._alias.pop(span.span_id, None)
self._agent.pop(span.span_id, None)
out = self._otel.pop(span.span_id, None)
if out is None:
return
data = span.span_data
if isinstance(data, ResponseSpanData):
if data.response is not None:
# The response echoes the model that answered, so this is response.model;
# the request-side name never reaches this span type.
out.update_name(f"chat {data.response.model}")
out.set_attribute("gen_ai.response.model", data.response.model)
out.set_attribute("gen_ai.response.id", data.response.id)
self._usage(out, data.usage)
elif isinstance(data, GenerationSpanData):
if data.model:
out.update_name(f"chat {data.model}")
out.set_attribute("gen_ai.request.model", data.model)
self._usage(out, data.usage)
elif isinstance(data, FunctionSpanData) and self._capture:
# Opt-In attributes. Both are None when trace_include_sensitive_data=False.
if data.input:
out.set_attribute("gen_ai.tool.call.arguments", data.input)
if data.output is not None:
out.set_attribute("gen_ai.tool.call.result", str(data.output))
elif isinstance(data, HandoffSpanData):
out.set_attribute("openai_agents.handoff.from", data.from_agent or "")
out.set_attribute("openai_agents.handoff.to", data.to_agent or "")
elif isinstance(data, GuardrailSpanData):
out.set_attribute("openai_agents.guardrail.triggered", data.triggered)
if span.error:
# SpanError is a message plus a dict, not an exception class, so there is
# no low-cardinality error name to report: fall back to the spec's _OTHER.
out.set_attribute("error.type", "_OTHER")
out.set_status(Status(StatusCode.ERROR, span.error.get("message", "")))
out.end()
@staticmethod
def _usage(out, usage):
if not usage:
return
out.set_attribute("gen_ai.usage.input_tokens", usage.get("input_tokens", 0))
out.set_attribute("gen_ai.usage.output_tokens", usage.get("output_tokens", 0))
cached = (usage.get("input_tokens_details") or {}).get("cached_tokens", 0)
if cached: # already included in input_tokens, per the spec
out.set_attribute("gen_ai.usage.cache_read.input_tokens", cached)
def shutdown(self) -> None:
self._provider.shutdown()
def force_flush(self) -> None:
self._provider.force_flush()Dua detail di dalamnya mengikuti konvensi dengan cermat. Atribut yang menurut spec sebaiknya diberikan saat span dibuat, seperti nama operasi, nama tool dan nama agent, dikirim ke start_span agar sampler bisa memakainya. Span chat diganti namanya setelah response tiba, karena nama model belum diketahui saat span dimulai. Pemasangannya cukup dengan tracer provider, OTLP exporter yang diarahkan ke collector yang sudah meneruskan ke Tempo, dan satu pemanggilan registrasi.
# telemetry.py -- call configure_tracing() once, at process start
from agents import add_trace_processor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from genai_processor import GenAISemconvProcessor
def configure_tracing() -> None:
provider = TracerProvider(resource=Resource.create({
"service.name": "erp-invoice-agent",
"deployment.environment.name": "staging",
}))
# Alloy or the OTel Collector on the same Docker network, forwarding to Tempo.
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint="http://alloy:4318/v1/traces")))
# add_trace_processor KEEPS the default exporter to OpenAI's trace dashboard.
# set_trace_processors([...]) REPLACES it -- use that when traces must stay on your VPS.
add_trace_processor(GenAISemconvProcessor(provider, capture_content=False))
# agent_run.py
from agents import RunConfig, Runner
result = await Runner.run(
triage_agent,
"Why is the invoice for PO-2026-0412 blocked?",
run_config=RunConfig(
workflow_name="invoice_dispute", # becomes gen_ai.workflow.name
group_id=ticket.number, # becomes gen_ai.conversation.id
trace_include_sensitive_data=False, # tool inputs/outputs never leave the SDK
),
)Worker yang berjalan lama butuh satu baris lagi. SDK mengekspor dalam batch di background, dan dokumentasinya menyarankan memanggil flush_traces setelah satu unit kerja di Celery atau background task FastAPI agar trace langsung diekspor. Dengan processor di atas, force_flush juga mem-flush batch processor OpenTelemetry, sehingga span sampai di Tempo sebelum worker didaur ulang.
Pilih antara add_trace_processor dan set_trace_processors dengan sengaja. add_trace_processor menambahkan processor Anda di samping exporter default, sehingga setiap trace tetap terkirim ke dashboard trace hosted milik OpenAI selain ke Tempo. set_trace_processors mengganti daftar default, dan itulah yang Anda butuhkan bila trace harus tetap di server sendiri. Selain itu, dokumentasi SDK menyatakan tracing tidak tersedia bagi organisasi yang memakai API OpenAI dengan kebijakan Zero Data Retention, jadi periksa hal itu sebelum merancang di atasnya.
Sebagian besar manfaat konvensi datang dari segelintir atribut yang konsisten di semua layanan yang mengirimnya. Berikut atribut yang tuntutan spesifiknya mudah terlewat.
Resource attribute menyelesaikan sisanya. service.name menunjukkan dari layanan agent mana sebuah trace berasal, dan atribut environment menjaga trace staging agar tidak masuk dashboard production. Keduanya tidak khusus GenAI, dan justru karena itu trace agent bisa berada di instance Tempo yang sama dengan bagian platform lainnya serta digabungkan dengan span HTTP dan database di sekitarnya.
Prompt, output model, dan hasil tool di agent ERP berisi nama supplier, nilai invoice, dan kadang data pribadi. Dokumen model spans dengan tegas menyebut konten ini sensitif, sering berukuran besar, dan sebaiknya tidak direkam secara default. Dokumen itu menjabarkan tiga pola pemakaian, berurutan dari yang paling sederhana hingga yang paling matang secara operasional.
Di sisi SDK, RunConfig.trace_include_sensitive_data default-nya true, dan environment variable OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA bisa mengubah default itu. Menyetelnya ke false mengosongkan field input dan output sebelum processor mana pun melihatnya, dan itu lebih aman daripada mengandalkan processor Anda sendiri untuk membuangnya. Processor di atas menambah gerbang kedua, capture_content, sehingga menyalakan capture menjadi perubahan yang disengaja di dua tempat. Bila Anda menyalakannya di stack self-hosted, Tempo punya batasnya sendiri yang perlu diantisipasi.
# tempo.yaml -- the default silently cuts long attribute values
distributor:
# Default 2048 bytes. A tool result holding a 40-line purchase order, or any
# gen_ai.input.messages value, is truncated before it is stored -- the span
# still arrives, so nothing looks broken until someone reads it.
max_attribute_bytes: 16384
# Watch for it instead of guessing:
# sum by (scope) (rate(tempo_distributor_attributes_truncated_total[5m]))Menaikkan batas itu adalah kompromi, bukan perbaikan. Panduan troubleshooting Tempo mendokumentasikan max_attribute_bytes justru karena atribut besar menjadi salah satu penyebab querier kehabisan memori, dan default 2048 byte ada untuk melindungi cluster. Naikkan hanya sebesar konten yang memang Anda pilih untuk direkam, dan utamakan pola storage eksternal begitu capture pesan lebih dari sekadar alat debugging.
Metric tempo_distributor_attributes_truncated_total membawa label scope, dan distributor juga mencatat log contoh setiap atribut yang terpotong dengan rate limit. Pasang alert pada metric itu untuk scope span setelah capture dinyalakan, sehingga Anda tahu soal pemotongan dari grafik, bukan dari engineer yang membaca setengah hasil tool.
Begitu span mengikuti konvensi, query TraceQL bisa ditulis berdasarkan makna, bukan nama span khusus tiap layanan. Atribut span memakai prefix span. , intrinsic seperti status dan duration memakai span: dengan titik dua, dan operator struktural memilih child, descendant dan sibling. Berikut query yang menjawab pertanyaan dari contoh di awal.
// Every failed tool call, with the agent that made it
{ span.gen_ai.operation.name = "execute_tool" && span:status = error }
| select(span.gen_ai.tool.name, span.gen_ai.agent.name)
// Runs where one agent called the same tool more than five times -- a looping agent
{ span.gen_ai.operation.name = "invoke_agent" }
> { span.gen_ai.tool.name = "get_purchase_order" } | count() > 5
// Slow model calls inside the dispute workflow only
{ span.gen_ai.workflow.name = "invoice_dispute" }
>> { span.gen_ai.operation.name = "chat" && span:duration > 8s }
// Every trace for one helpdesk ticket, across retries and follow-up messages
{ span.gen_ai.conversation.id = "TCK-2026-10-0193" }
// Failed spans that sit beside a handoff under the same agent
{ span:name = "agent.handoff" } ~ { span:status = error }Query kedua adalah yang membuat seluruh upaya ini sepadan. Agent yang berulang tidak terlihat di metric layanan, karena setiap tool call adalah request yang cepat dan sukses, padahal itulah cara paling umum agent menghabiskan token tanpa menghasilkan jawaban. Operator child hanya berfungsi karena processor membuang span turn; bila span itu tetap ada, query yang sama perlu operator descendant.
Processor kustom bukan satu-satunya jalan. OpenTelemetry kini menerbitkan opentelemetry-instrumentation-genai-openai-agents, yang mendaftarkan tracing processor ke SDK dan menghasilkan span workflow, agent dan tool. Paket ini melanjutkan paket sebelumnya, opentelemetry-instrumentation-openai-agents-v2, yang kini hanya menerima security patch. Kedua pendekatan berbeda dalam hal-hal yang menentukan pilihan.
| Aspek | Processor kustom | Paket resmi |
|---|---|---|
| Span model call | Dikirim dari ResponseSpanData dan GenerationSpanData di processor yang sama. | Tidak dikirim; memasang opentelemetry-instrumentation-genai-openai di sampingnya yang menyediakannya. |
| Konten pesan | Hanya argumen dan hasil tool; konversi input item SDK ke schema messages diserahkan ke Anda. | Diatur lewat environment variable, dengan completion hook upload untuk storage eksternal. |
| Mengikuti perubahan spec | Manual. Setiap rename di spec berstatus Development harus Anda edit sendiri. | Mengikuti konvensi lewat release paket, saat ini masih beta. |
| Handoff, guardrail, penamaan | Sepenuhnya di tangan Anda, termasuk atribut vendor dan span yang dilewati. | Ditentukan paket; nama span workflow diambil dari workflow_name. |
# pip install opentelemetry-instrumentation-genai-openai-agents \
# opentelemetry-instrumentation-genai-openai # the chat spans come from this one
from opentelemetry.instrumentation.genai.openai_agents import OpenAIAgentsInstrumentor
# Default keeps uploading to OpenAI's hosted tracing as well; this routes only to OTel.
OpenAIAgentsInstrumentor().instrument(disable_openai_trace_export=True)
# Message capture is an environment switch, off unless you set it:
# OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=...
# or ship content to storage instead of span attributes:
# OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload
# OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=...Saat tulisan ini dibuat, paket opentelemetry-instrumentation-genai-openai-agents di PyPI berada di versi 1.2b0. Secara default paket ini tetap mengaktifkan upload SDK ke tracing hosted milik OpenAI di samping OpenTelemetry, dan disable_openai_trace_export=True membuat trace hanya dikirim lewat OpenTelemetry.
Pembagian yang masuk akal adalah memulai dengan paket resmi untuk bentuk standar, lalu beralih ke processor kustom hanya bila Anda butuh sesuatu yang tidak disediakannya, misalnya aturan redaksi per tool atau atribut khusus ERP di setiap span agent. Processor kustom di tulisan ini cukup pendek untuk dipelihara, tetapi konsekuensinya Anda harus membaca changelog konvensi, karena spec berstatus Development boleh mengganti nama atribut yang menjadi dasar dashboard Anda.
Trace agent menjadi berguna ketika mengikuti bentuk bersama: root berupa workflow atau agent, model call dan tool call sebagai sibling di bawahnya, nama atribut yang sama di setiap layanan, dan konten yang tidak direkam kecuali ada yang sengaja memutuskannya. Kunci versi konvensi yang Anda implementasikan, isi conversation id yang nyata, jangan taruh tool di bawah span chat, dan periksa apa yang dipotong backend Anda sebelum memercayai apa yang ditampilkannya.