Skip to content

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.model
  • gen_ai.usage.input_tokens, gen_ai.usage.output_tokens
  • gen_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_tokens
  • gen_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_tokens
  • gen_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

pip install traceloop-sdk openai

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_tokens
  • gen_ai.prompt.0.role, gen_ai.prompt.0.content, gen_ai.prompt.1.role, gen_ai.prompt.1.content
  • gen_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

export OPENAI_API_KEY="sk-..."
export CX_OTEL_ENDPOINT="http://<COLLECTOR_HOST>:4317"

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

export OPENAI_API_KEY="sk-..."
export CX_OTEL_ENDPOINT="http://<COLLECTOR_HOST>:4317"

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

pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc httpx

Environment variables

export OPENAI_API_KEY="sk-..."
export CX_OTEL_ENDPOINT="http://<COLLECTOR_HOST>:4317"

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

export OPENAI_API_KEY="sk-..."
export CX_OTEL_ENDPOINT="<COLLECTOR_HOST>:4317"

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

go mod tidy && go run main.go

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_tokens
  • gen_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

#ExampleLanguageInstrumentationSemconv
1OpenAI Agents SDKPythonOTel contrib (auto)New (opt-in)
2Anthropic ClaudePythonOpenLLMetry (auto)Legacy (migrating)
3AWS BedrockPythonOTel contrib (auto)New (opt-in)
4OpenLLMetry + OpenAIPythonTraceloop SDK (auto)Legacy
5Java manualJavaManual spansNew
6.NET Microsoft.Extensions.AIC#Microsoft.Extensions.AI (auto)New (native)
7Manual (no third-party)PythonManual spans + raw HTTPNew
8Go manualGoManual spans + net/httpNew
9Google GenAI + collectorPythonOTel contrib (auto)New (opt-in)

Next steps

Look up which open-source library to use for your provider in Compatibility matrix.