Code examples
Copy-pasteable scripts that send OpenTelemetry GenAI spans to Coralogix AI Center using the new semantic conventions. Each example uses the OTEL standard OTLP exporter — no Coralogix-specific SDK required.
Use the domain selector at the top of this page to set your Coralogix region. The collector config below updates automatically to use the matching domain.
Prerequisites: deploy an OpenTelemetry Collector
All examples below export traces to a local OpenTelemetry Collector over OTLP/gRPC. The collector handles authentication, batching, and retry — keeping credentials out of your application code.
Save the following as otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
exporters:
coralogix:
domain: ""
private_key: "${CORALOGIX_PRIVATE_KEY}"
application_name: "my-genai-app"
subsystem_name: "my-service"
application_name_attributes:
- "cx.application.name"
subsystem_name_attributes:
- "cx.subsystem.name"
timeout: 30s
service:
pipelines:
traces:
receivers: [otlp]
exporters: [coralogix]
Run the collector:
export CORALOGIX_PRIVATE_KEY="<YOUR_CX_API_KEY>"
docker run --rm -p 4317:4317 \
-v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \
-e CORALOGIX_PRIVATE_KEY \
otel/opentelemetry-collector-contrib:latest
Tip
See OpenTelemetry integration for AI Center for more collector deployment options.
Example 1: OpenAI Agents SDK
Install
pip install openai-agents opentelemetry-instrumentation-openai-agents-v2 opentelemetry-instrumentation-openai-v2 opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
Environment variables
export OPENAI_API_KEY="<YOUR_OPENAI_API_KEY>"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://<COLLECTOR_HOST>:4317"
export OTEL_EXPORTER_OTLP_INSECURE="true"
export OTEL_RESOURCE_ATTRIBUTES="cx.application.name=my-genai-app,cx.subsystem.name=my-subsystem"
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental"
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="true"
Script
import asyncio
from agents import Agent, Runner, function_tool
# --- OTel imports ---
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai_agents import OpenAIAgentsInstrumentor
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# --- OTel setup: configure tracer provider and OTLP exporter ---
def configure_otel() -> TracerProvider:
resource = Resource.create()
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter() # reads OTEL_EXPORTER_OTLP_ENDPOINT from env
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return provider
# --- Your app logic (replace with your own) ---
@function_tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is 22C and sunny."
async def main():
# OTel: initialize tracing and auto-instrument the OpenAI Agents SDK
provider = configure_otel()
OpenAIAgentsInstrumentor().instrument(tracer_provider=provider)
OpenAIInstrumentor().instrument(tracer_provider=provider)
# Your app logic
agent = Agent(
name="Travel Assistant",
instructions="You are a concise travel assistant. Answer in one sentence.",
tools=[get_weather],
)
result = await Runner.run(agent, input="What's the weather in Paris?")
print(f"Agent response: {result.final_output}")
# OTel: flush and shut down the tracer provider
provider.force_flush()
provider.shutdown()
if __name__ == "__main__":
asyncio.run(main())
Expected span attributes
gen_ai.operation.name="chat"gen_ai.provider.name="openai"gen_ai.request.model,gen_ai.response.modelgen_ai.usage.input_tokens,gen_ai.usage.output_tokensgen_ai.input.messages,gen_ai.output.messages(as log events with content capture)- Agent spans:
gen_ai.agent.name,gen_ai.tool.name
Tip
If pip install fails for pre-release packages, try pip install --pre opentelemetry-instrumentation-openai-agents-v2.
Example 2: Anthropic / Claude SDK
Install
pip install anthropic opentelemetry-instrumentation-anthropic opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
Environment variables
export ANTHROPIC_API_KEY="<YOUR_ANTHROPIC_API_KEY>"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://<COLLECTOR_HOST>:4317"
export OTEL_EXPORTER_OTLP_INSECURE="true"
export OTEL_RESOURCE_ATTRIBUTES="cx.application.name=my-genai-app,cx.subsystem.name=my-service"
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental"
export TRACELOOP_TRACE_CONTENT="true"
Script
import anthropic
# --- OTel imports ---
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# --- OTel setup: configure tracer provider and OTLP exporter ---
def configure_otel() -> TracerProvider:
resource = Resource.create()
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter() # reads OTEL_EXPORTER_OTLP_ENDPOINT from env
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return provider
def main():
# OTel: initialize tracing and auto-instrument the Anthropic SDK
provider = configure_otel()
AnthropicInstrumentor().instrument(tracer_provider=provider)
# Your app logic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=256,
messages=[
{"role": "user", "content": "What is OpenTelemetry in one sentence?"}
],
)
print(f"Claude response: {message.content[0].text}")
# OTel: flush and shut down the tracer provider
provider.force_flush()
provider.shutdown()
if __name__ == "__main__":
main()
Expected span attributes
gen_ai.system="anthropic"gen_ai.request.model="claude-sonnet-4-20250514"gen_ai.usage.input_tokens,gen_ai.usage.output_tokensgen_ai.response.finish_reasons=["end_turn"]
Warning
opentelemetry-instrumentation-anthropic on PyPI is from OpenLLMetry (Traceloop), not official OTel contrib. Semconv migration from legacy to new is in progress. Content capture uses TRACELOOP_TRACE_CONTENT=true (not the standard OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT).
Example 3: AWS Bedrock
Install
pip install boto3 opentelemetry-instrumentation-botocore opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
Environment variables
export AWS_ACCESS_KEY_ID="<YOUR_AWS_ACCESS_KEY>"
export AWS_SECRET_ACCESS_KEY="<YOUR_AWS_SECRET_KEY>"
export AWS_DEFAULT_REGION="us-east-1"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://<COLLECTOR_HOST>:4317"
export OTEL_EXPORTER_OTLP_INSECURE="true"
export OTEL_RESOURCE_ATTRIBUTES="cx.application.name=my-genai-app,cx.subsystem.name=my-service"
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental"
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="true"
Script
import boto3
# --- OTel imports ---
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# --- OTel setup: configure tracer provider and OTLP exporter ---
def configure_otel() -> TracerProvider:
resource = Resource.create()
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter() # reads OTEL_EXPORTER_OTLP_ENDPOINT from env
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return provider
def main():
# OTel: initialize tracing and auto-instrument botocore (Bedrock)
provider = configure_otel()
BotocoreInstrumentor().instrument(tracer_provider=provider)
# Your app logic
client = boto3.client("bedrock-runtime", region_name="us-east-1")
response = client.converse(
modelId="anthropic.claude-3-haiku-20240307-v1:0",
messages=[
{
"role": "user",
"content": [{"text": "What is OpenTelemetry in one sentence?"}],
}
],
inferenceConfig={"maxTokens": 256, "temperature": 0.7},
)
text = response["output"]["message"]["content"][0]["text"]
usage = response["usage"]
print(f"Bedrock response: {text}")
print(f"Tokens - input: {usage['inputTokens']}, output: {usage['outputTokens']}")
# OTel: flush and shut down the tracer provider
provider.force_flush()
provider.shutdown()
if __name__ == "__main__":
main()
Expected span attributes
gen_ai.provider.name="aws.bedrock"gen_ai.request.model="anthropic.claude-3-haiku-20240307-v1:0"gen_ai.operation.name="chat"gen_ai.usage.input_tokens,gen_ai.usage.output_tokensgen_ai.request.max_tokens=256,gen_ai.request.temperature=0.7- AWS-specific:
rpc.system="aws-api",rpc.service="BedrockRuntime"
Tip
Use the Converse API (not InvokeModel) — it has full tracing support in the botocore instrumentation. The model must be enabled in your AWS account for the chosen region.
Example 4: OpenLLMetry (Traceloop SDK) — Python
The most popular community instrumentation. Auto-instruments OpenAI calls with zero code changes.
Warning
OpenLLMetry currently uses legacy semconv (gen_ai.prompt / gen_ai.completion indexed attributes). Migration to new is tracked in issue #3515. Coralogix AI Center supports both.
Install
Environment variables
export OPENAI_API_KEY="<YOUR_OPENAI_API_KEY>"
export TRACELOOP_BASE_URL="http://<COLLECTOR_HOST>:4317/v1/traces"
export TRACELOOP_TRACE_CONTENT="true"
export OTEL_RESOURCE_ATTRIBUTES="cx.application.name=my-genai-app,cx.subsystem.name=my-service"
Script
from openai import OpenAI
from traceloop.sdk import Traceloop # OTel: Traceloop auto-instruments OpenAI
Traceloop.init() # OTel: reads TRACELOOP_BASE_URL, TRACELOOP_HEADERS from env
# Your app logic
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What is OpenTelemetry in one sentence?"},
],
max_tokens=100,
)
print(response.choices[0].message.content)
Expected span attributes (legacy semconv)
gen_ai.system="openai"gen_ai.request.model="gpt-4o-mini"gen_ai.usage.prompt_tokens,gen_ai.usage.completion_tokensgen_ai.prompt.0.role,gen_ai.prompt.0.content,gen_ai.prompt.1.role,gen_ai.prompt.1.contentgen_ai.completion.0.role,gen_ai.completion.0.content
Example 5: Java — manual instrumentation
No auto-instrumentation exists for OpenAI in Java. Manual spans with new semconv.
Dependencies (pom.xml)
<dependencies>
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>4.35.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.59.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Environment variables
Script
package com.example;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.*;
// --- OTel imports ---
import io.opentelemetry.api.common.*;
import io.opentelemetry.api.trace.*;
import io.opentelemetry.context.Scope;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
public class GenAiManualInstrumentation {
public static void main(String[] args) {
// --- OTel setup: configure OTLP exporter, resource, and tracer ---
var cxEndpoint = System.getenv().getOrDefault("CX_OTEL_ENDPOINT",
"http://<COLLECTOR_HOST>:4317");
var exporter = OtlpGrpcSpanExporter.builder()
.setEndpoint(cxEndpoint).build();
var resource = Resource.getDefault().merge(Resource.create(
Attributes.builder()
.put("service.name", "java-genai-demo")
.put("cx.application.name", "my-genai-app")
.put("cx.subsystem.name", "my-service").build()));
var tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
.setResource(resource).build();
var otel = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
var tracer = otel.getTracer("genai-manual");
// --- Your app logic ---
var openAi = OpenAIOkHttpClient.fromEnv();
var model = "gpt-4o-mini";
var systemMsg = "You are a concise assistant.";
var userMsg = "Explain distributed tracing in one sentence.";
// --- OTel: build gen_ai.input.messages in new semconv format ---
var inputJson = "[" +
"{\"role\":\"system\",\"parts\":[{\"type\":\"text\",\"content\":\"" + systemMsg + "\"}]}," +
"{\"role\":\"user\",\"parts\":[{\"type\":\"text\",\"content\":\"" + userMsg + "\"}]}]";
// OTel: create a GenAI span with required attributes
Span span = tracer.spanBuilder("chat " + model)
.setSpanKind(SpanKind.CLIENT)
.setAttribute("gen_ai.operation.name", "chat")
.setAttribute("gen_ai.provider.name", "openai")
.setAttribute("gen_ai.request.model", model)
.setAttribute("gen_ai.input.messages", inputJson)
.startSpan();
try (Scope ignored = span.makeCurrent()) {
var params = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.maxCompletionTokens(100)
.addSystemMessage(systemMsg)
.addUserMessage(userMsg).build();
var completion = openAi.chat().completions().create(params);
var text = completion.choices().get(0).message().content().orElse("");
span.setAttribute("gen_ai.response.model", completion.model());
span.setAttribute("gen_ai.response.id", completion.id());
span.setAttribute("gen_ai.usage.input_tokens",
completion.usage().map(u -> u.promptTokens()).orElse(0L));
span.setAttribute("gen_ai.usage.output_tokens",
completion.usage().map(u -> u.completionTokens()).orElse(0L));
span.setAttribute("gen_ai.output.messages",
"[{\"role\":\"assistant\",\"parts\":[{\"type\":\"text\",\"content\":\"" +
text.replace("\"", "\\\"") + "\"}]}]");
System.out.println("Response: " + text);
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
} finally {
span.end();
}
tracerProvider.forceFlush();
tracerProvider.shutdown();
}
}
Example 5: .NET — Microsoft.Extensions.AI
Microsoft.Extensions.AI natively implements new semconv v1.37. No opt-in needed.
Install
dotnet new console -n OtelGenAiDotnet && cd OtelGenAiDotnet
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.5.1
dotnet add package Microsoft.Extensions.AI --version 10.5.2
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.15.3
dotnet add package OpenTelemetry.Extensions.Hosting --version 1.15.3
Environment variables
Script (Program.cs)
using Microsoft.Extensions.AI;
using OpenAI;
// --- OTel imports ---
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var cxEndpoint = Environment.GetEnvironmentVariable("CX_OTEL_ENDPOINT")
?? "http://<COLLECTOR_HOST>:4317";
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
?? throw new InvalidOperationException("OPENAI_API_KEY required");
// --- OTel setup: configure tracer provider with OTLP exporter ---
const string sourceName = "GenAI.Demo";
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("dotnet-genai-demo")
.AddAttributes(new Dictionary<string, object> {
["cx.application.name"] = "my-genai-app",
["cx.subsystem.name"] = "my-service",
}))
.AddSource(sourceName)
.AddOtlpExporter(opts => {
opts.Endpoint = new Uri(cxEndpoint);
opts.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
})
.Build();
// --- Your app logic: create chat client with OTel auto-instrumentation ---
IChatClient chatClient = new ChatClientBuilder(
new OpenAIClient(openAiKey).GetChatClient("gpt-4o-mini").AsIChatClient())
.UseOpenTelemetry(sourceName: sourceName, // OTel: enables GenAI span emission
configure: c => c.EnableSensitiveData = true) // OTel: captures message content
.Build();
var response = await chatClient.GetResponseAsync(
new List<ChatMessage> {
new(ChatRole.System, "You are a concise assistant."),
new(ChatRole.User, "What is OpenTelemetry in one sentence?"),
},
new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f });
Console.WriteLine($"Response: {response.Text}");
Console.WriteLine($"Tokens: {response.Usage?.InputTokenCount} in, {response.Usage?.OutputTokenCount} out");
Tip
EnableSensitiveData = true is required for message content. The IChatClient abstraction works with any provider (Azure OpenAI, Ollama, etc.).
Example 6: Manual instrumentation (no third-party library)
Pure manual spans with no instrumentation library — just OTel SDK + raw HTTP. Universal template for any provider/language.
Install
Environment variables
Script
import json, os
import httpx
# --- OTel imports ---
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import SpanKind, StatusCode
# --- OTel setup: configure tracer provider and OTLP exporter ---
CX_ENDPOINT = os.environ.get("CX_OTEL_ENDPOINT", "http://<COLLECTOR_HOST>:4317")
OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "")
resource = Resource.create({"service.name": "manual-genai-demo",
"cx.application.name": "my-genai-app", "cx.subsystem.name": "my-service"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint=CX_ENDPOINT, insecure=True)))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("genai-manual", "1.0.0")
def to_semconv(messages):
"""Convert OpenAI messages to new semconv JSON parts format."""
result = []
for m in messages:
parts = []
if m.get("content"): parts.append({"type": "text", "content": m["content"]})
if m.get("tool_calls"):
for tc in m["tool_calls"]:
parts.append({"type": "tool_call", "id": tc["id"],
"name": tc["function"]["name"], "arguments": tc["function"]["arguments"]})
entry = {"role": m["role"], "parts": parts}
if m.get("tool_call_id"): entry["tool_call_id"] = m["tool_call_id"]
result.append(entry)
return json.dumps(result)
# --- Your app logic with manual OTel GenAI spans ---
def chat(messages, model="gpt-4o-mini", max_tokens=200, user=None):
# OTel: create a GenAI span with required attributes
with tracer.start_as_current_span(f"chat {model}", kind=SpanKind.CLIENT,
attributes={"gen_ai.operation.name": "chat", "gen_ai.provider.name": "openai",
"gen_ai.request.model": model, "gen_ai.request.max_tokens": max_tokens,
"gen_ai.input.messages": to_semconv(messages),
"server.address": "api.openai.com", "server.port": 443}) as span:
if user: span.set_attribute("gen_ai.request.user", user)
body = {"model": model, "messages": messages, "max_tokens": max_tokens}
if user: body["user"] = user
resp = httpx.post("https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {OPENAI_KEY}",
"Content-Type": "application/json"}, json=body, timeout=60.0)
resp.raise_for_status()
data = resp.json()
usage = data.get("usage", {})
choices = data.get("choices", [])
span.set_attribute("gen_ai.response.model", data.get("model", model))
span.set_attribute("gen_ai.response.id", data.get("id", ""))
span.set_attribute("gen_ai.response.finish_reasons",
json.dumps([c.get("finish_reason") for c in choices]))
span.set_attribute("gen_ai.usage.input_tokens", usage.get("prompt_tokens", 0))
span.set_attribute("gen_ai.usage.output_tokens", usage.get("completion_tokens", 0))
out = []
for c in choices:
m = c.get("message", {})
parts = []
if m.get("content"): parts.append({"type": "text", "content": m["content"]})
if m.get("tool_calls"):
for tc in m["tool_calls"]:
parts.append({"type": "tool_call", "id": tc["id"],
"name": tc["function"]["name"], "arguments": tc["function"]["arguments"]})
out.append({"role": m.get("role", "assistant"), "parts": parts})
span.set_attribute("gen_ai.output.messages", json.dumps(out))
return data
result = chat(
messages=[{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "What is OpenTelemetry in one sentence?"}],
user="user-42")
print(f"Response: {result['choices'][0]['message']['content']}")
provider.force_flush()
provider.shutdown()
Universal template
To adapt for Anthropic, Gemini, or any other provider, change the HTTP endpoint, request body format, and response parsing. The OTel span attributes remain identical.
Example 7: Go — manual instrumentation
No auto-instrumentation for GenAI in Go. Manual spans with new semconv + raw net/http to OpenAI.
go.mod
module go-genai-manual-demo
go 1.22
require (
go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0
go.opentelemetry.io/otel/sdk v1.34.0
go.opentelemetry.io/otel/trace v1.34.0
)
Environment variables
main.go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
)
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
User string `json:"user,omitempty"`
}
type chatChoice struct {
Message chatMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}
type chatUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
}
type chatResponse struct {
ID string `json:"id"`
Model string `json:"model"`
Choices []chatChoice `json:"choices"`
Usage chatUsage `json:"usage"`
}
type msgPart struct {
Type string `json:"type"`
Content string `json:"content"`
}
type semconvMsg struct {
Role string `json:"role"`
Parts []msgPart `json:"parts"`
}
// --- OTel setup: configure OTLP exporter and tracer provider ---
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
endpoint := os.Getenv("CX_OTEL_ENDPOINT")
if endpoint == "" {
endpoint = "<COLLECTOR_HOST>:4317"
}
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(endpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
res, _ := resource.Merge(resource.Default(),
resource.NewWithAttributes(semconv.SchemaURL,
semconv.ServiceName("go-genai-manual-demo")))
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter), sdktrace.WithResource(res))
otel.SetTracerProvider(tp)
return tp, nil
}
// --- Your app logic with manual OTel GenAI spans ---
func chatCompletion(ctx context.Context, tracer trace.Tracer) (*chatResponse, error) {
model := "gpt-4o-mini"
user := "user-42"
msgs := []chatMessage{
{Role: "system", Content: "You are a concise assistant."},
{Role: "user", Content: "What is OpenTelemetry?"},
}
inputMsgs := make([]semconvMsg, len(msgs))
for i, m := range msgs {
inputMsgs[i] = semconvMsg{Role: m.Role,
Parts: []msgPart{{Type: "text", Content: m.Content}}}
}
inputJSON, _ := json.Marshal(inputMsgs)
ctx, span := tracer.Start(ctx, "chat "+model,
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
attribute.String("gen_ai.operation.name", "chat"),
attribute.String("gen_ai.provider.name", "openai"),
attribute.String("gen_ai.request.model", model),
attribute.String("gen_ai.request.user", user),
attribute.String("gen_ai.input.messages", string(inputJSON)),
attribute.String("server.address", "api.openai.com"),
attribute.Int("server.port", 443),
))
defer span.End()
body, _ := json.Marshal(chatRequest{Model: model, Messages: msgs, User: user})
req, _ := http.NewRequestWithContext(ctx, "POST",
"https://api.openai.com/v1/chat/completions", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("OPENAI_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
span.RecordError(err)
return nil, err
}
defer resp.Body.Close()
respBytes, _ := io.ReadAll(resp.Body)
var chatResp chatResponse
json.Unmarshal(respBytes, &chatResp)
finishReasons := make([]string, len(chatResp.Choices))
outMsgs := make([]semconvMsg, len(chatResp.Choices))
for i, c := range chatResp.Choices {
finishReasons[i] = c.FinishReason
outMsgs[i] = semconvMsg{Role: c.Message.Role,
Parts: []msgPart{{Type: "text", Content: c.Message.Content}}}
}
outJSON, _ := json.Marshal(outMsgs)
span.SetAttributes(
attribute.String("gen_ai.response.id", chatResp.ID),
attribute.String("gen_ai.response.model", chatResp.Model),
attribute.StringSlice("gen_ai.response.finish_reasons", finishReasons),
attribute.Int("gen_ai.usage.input_tokens", chatResp.Usage.PromptTokens),
attribute.Int("gen_ai.usage.output_tokens", chatResp.Usage.CompletionTokens),
attribute.String("gen_ai.output.messages", string(outJSON)))
return &chatResp, nil
}
func main() {
ctx := context.Background()
tp, err := initTracer(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tp.Shutdown(ctx)
}()
resp, err := chatCompletion(ctx, otel.Tracer("genai-manual-demo"))
if err != nil {
log.Fatal(err)
}
for _, c := range resp.Choices {
fmt.Printf("%s: %s\n", c.FinishReason, c.Message.Content)
}
fmt.Printf("Tokens: %d in, %d out\n", resp.Usage.PromptTokens, resp.Usage.CompletionTokens)
}
Build and run
Tip
The Go OTel SDK does not export gen_ai.* constants yet — all keys are raw strings. Use gen_ai.provider.name (new semconv); the deprecated gen_ai.system is not needed.
Example 9: Google GenAI with instrumentor + local collector
Uses the GoogleGenAiSdkInstrumentor from opentelemetry-instrumentation-google-genai to auto-instrument the Google GenAI SDK. Traces are exported to a local OpenTelemetry Collector over OTLP/gRPC; the collector forwards to Coralogix (see your otel-collector-config.yaml).
Install
pip install google-genai opentelemetry-instrumentation-google-genai opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc
Environment variables
export GOOGLE_API_KEY="<YOUR_GOOGLE_API_KEY>"
# Point to your local OTel Collector (which forwards to Coralogix)
export OTEL_EXPORTER_OTLP_ENDPOINT="http://<COLLECTOR_HOST>:4317"
export OTEL_EXPORTER_OTLP_INSECURE="true"
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental"
export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT="SPAN_AND_EVENT"
Script
import os
from google import genai
# --- OTel imports ---
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# --- OTel setup: configure tracer provider and OTLP exporter ---
def configure_otel() -> TracerProvider:
resource = Resource.create({
SERVICE_NAME: "my-genai-service",
"cx.application.name": "my-genai-app",
"cx.subsystem.name": "my-service",
})
provider = TracerProvider(resource=resource)
endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://<COLLECTOR_HOST>:4317")
insecure = os.getenv("OTEL_EXPORTER_OTLP_INSECURE", "true").lower() == "true"
provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, insecure=insecure))
)
trace.set_tracer_provider(provider)
return provider
def main():
# OTel: initialize tracing, then auto-instrument the Google GenAI SDK
provider = configure_otel()
GoogleGenAiSdkInstrumentor().instrument()
# Your app logic
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="What is OpenTelemetry in one sentence?",
)
print(f"Gemini response: {response.text}")
# OTel: flush and shut down the tracer provider
provider.force_flush()
provider.shutdown()
if __name__ == "__main__":
main()
Expected span attributes
gen_ai.provider.name="google_genai"gen_ai.request.model="gemini-2.0-flash"gen_ai.operation.name="chat"gen_ai.usage.input_tokens,gen_ai.usage.output_tokensgen_ai.input.messages,gen_ai.output.messages(with content capture)
Tip
Put your Coralogix Send-Your-Data API key on the collector (e.g. CORALOGIX_PRIVATE_KEY in otel-collector-config.yaml), not in the Python exporter. The exporter only needs to reach the local collector.
Comparison
| # | Example | Language | Instrumentation | Semconv |
|---|---|---|---|---|
| 1 | OpenAI Agents SDK | Python | OTel contrib (auto) | New (opt-in) |
| 2 | Anthropic Claude | Python | OpenLLMetry (auto) | Legacy (migrating) |
| 3 | AWS Bedrock | Python | OTel contrib (auto) | New (opt-in) |
| 4 | OpenLLMetry + OpenAI | Python | Traceloop SDK (auto) | Legacy |
| 5 | Java manual | Java | Manual spans | New |
| 6 | .NET Microsoft.Extensions.AI | C# | Microsoft.Extensions.AI (auto) | New (native) |
| 7 | Manual (no third-party) | Python | Manual spans + raw HTTP | New |
| 8 | Go manual | Go | Manual spans + net/http | New |
| 9 | Google GenAI + collector | Python | OTel contrib (auto) | New (opt-in) |
Next steps
Look up which open-source library to use for your provider in Compatibility matrix.