Koog tracing with Future AGI

Send Koog (Kotlin) agent traces to a self-hosted Future AGI instance with Koog's OpenTelemetry feature and an OTLP/HTTP exporter.

Send Koog agent, model and tool spans to a self-hosted Future AGI instance using Koog’s OpenTelemetry feature. This JVM guide uses ai.koog:koog-agents 1.3.0 and an OpenAI-compatible model endpoint.

Content is off by default. Set FI_KOOG_CAPTURE_CONTENT=true to include prompts and tool payloads; see Privacy.

How the recipe works

PieceWhat it does
ExporterOpenTelemetry Java OtlpHttpSpanExporter to {FI_BASE_URL}/tracer/v1/traces, with both X-Api-Key and X-Secret-Key, and compression none (works on every Future AGI release; Future AGI cloud and self-hosted v1.56.0 and later also accept gzip).
Resourceproject_name and project_type=observe on the resource. service.name is not read as a project name.
SDKThe recipe builds the Kotlin OpenTelemetry SDK itself and hands it to Koog with setSdk, so it can flush before exit. See Why not setShutdownOnAgentClose.
FutureAgiSpanExporterA small wrapper around the OTLP exporter: keeps content out unless you opt in, never exports stack traces, adds fi.span.kind where Future AGI would show unknown, copies the run id to session.id, and waits for each HTTP export to finish.
Koog’s own setup helperssetSdk is the escape hatch Koog documents for full control of the SDK. With it, Koog ignores addSpanExporter, addResourceAttributes and addSpanProcessor.

Prerequisites

  • JDK 17 or newer, and Gradle (the example uses the Kotlin DSL)
  • A Future AGI API key and secret key
  • A model and Chat Completions endpoint supported by Koog’s OpenAI client, with function calling enabled for the weather tool.

1. Add dependencies

koog-agents already contains the OpenTelemetry feature and the OpenTelemetry Java SDK. Add only the OTLP exporter, on the same OpenTelemetry version Koog 1.3.0 uses (1.61.0).

// build.gradle.kts
dependencies {
    implementation("ai.koog:koog-agents:1.3.0")
    implementation(platform("io.opentelemetry:opentelemetry-bom:1.61.0"))
    implementation("io.opentelemetry:opentelemetry-exporter-otlp")
}

2. Set environment variables

export FI_BASE_URL="http://localhost:4318"       # replace with your self-hosted Future AGI origin
export FI_API_KEY="YOUR_API_KEY"
export FI_SECRET_KEY="YOUR_SECRET_KEY"
export FI_PROJECT_NAME="my-koog-agent"
# optional
export OTEL_SERVICE_NAME="weather-service"         # default koog-agent
export FI_KOOG_CAPTURE_CONTENT=false               # default false, see Privacy

# the model the agent in step 4 calls
export OPENAI_API_KEY="YOUR_OPENAI_KEY"
export OPENAI_BASE_URL="https://api.openai.com"    # or any OpenAI-compatible endpoint, without /v1

3. Copy the two setup files

Copy FutureAgiTelemetry.kt and FutureAgiSpanExporter.kt from examples/frameworks/koog into your project. The core of FutureAgiTelemetry is this:

import ai.koog.agents.features.opentelemetry.feature.OpenTelemetryConfig
import io.opentelemetry.api.metrics.MeterProvider
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
import io.opentelemetry.kotlin.ExperimentalApi
import io.opentelemetry.kotlin.createOpenTelemetry
import io.opentelemetry.kotlin.export.TelemetryCloseable
import io.opentelemetry.kotlin.tracing.export.batchSpanProcessor
import io.opentelemetry.kotlin.tracing.export.toOtelKotlinSpanExporter
import java.time.Duration

@OptIn(ExperimentalApi::class)
class FutureAgiTelemetry(val config: Config) {
    val spanExporter = FutureAgiSpanExporter(
        OtlpHttpSpanExporter.builder()
            .setEndpoint(config.baseUrl.trimEnd('/') + "/tracer/v1/traces")
            .addHeader("X-Api-Key", config.apiKey)
            .addHeader("X-Secret-Key", config.secretKey)
            .setCompression("none")
            .setTimeout(Duration.ofSeconds(10))
            .setMeterProvider(MeterProvider.noop())
            .build(),
        captureContent = config.captureContent,
    )

    val sdk = createOpenTelemetry {
        tracerProvider {
            resource(mapOf(
                "service.name" to config.serviceName,
                "project_name" to config.projectName,
                "project_type" to "observe",
            ))
            export { batchSpanProcessor(spanExporter.toOtelKotlinSpanExporter()) }
        }
    }

    fun configure(otel: OpenTelemetryConfig) {
        otel.setServiceInfo(config.serviceName, config.serviceVersion)
        otel.setVerbose(config.captureContent) // Koog masks content unless verbose is on
        otel.setSdk(sdk)
    }

    suspend fun shutdown() {
        (sdk as TelemetryCloseable).forceFlush()
        (sdk as TelemetryCloseable).shutdown()
    }
    // Config (with fromEnv) is in the example file.
}

4. Install it on your agent

The example selects OpenAIModels.Chat.GPT5_6Luna from Koog 1.3.0 with reasoningEffort = ReasoningEffort.NONE. Choose a function-calling model available to your endpoint and adjust these settings to its requirements.

import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.agent.config.AIAgentConfig
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.core.tools.annotations.LLMDescription
import ai.koog.agents.core.tools.annotations.Tool
import ai.koog.agents.core.tools.reflect.ToolSet
import ai.koog.agents.features.opentelemetry.feature.OpenTelemetry
import ai.koog.prompt.executor.clients.openai.OpenAIChatParams
import ai.koog.prompt.executor.clients.openai.OpenAIClientSettings
import ai.koog.prompt.executor.clients.openai.OpenAILLMClient
import ai.koog.prompt.executor.clients.openai.OpenAIModels
import ai.koog.prompt.executor.clients.openai.base.models.ReasoningEffort
import ai.koog.prompt.executor.llms.MultiLLMPromptExecutor
import ai.koog.prompt.dsl.prompt
import ai.koog.prompt.llm.LLMProvider
import ai.koog.utils.io.use

@LLMDescription("Weather tools")
class WeatherTools : ToolSet {
    @Tool
    @LLMDescription("Returns the current weather for a city")
    fun getWeather(@LLMDescription("City name") city: String): String = "Sunny, 21 C in $city"
}

suspend fun main() {
    val telemetry = FutureAgiTelemetry(FutureAgiTelemetry.Config.fromEnv())
    val openAI = OpenAILLMClient(
        apiKey = System.getenv("OPENAI_API_KEY"),
        settings = OpenAIClientSettings(baseUrl = System.getenv("OPENAI_BASE_URL") ?: "https://api.openai.com"),
    )
    try {
        AIAgent(
            id = "weather-agent", // readable span names; Koog uses a random UUID otherwise
            promptExecutor = MultiLLMPromptExecutor(LLMProvider.OpenAI to openAI),
            agentConfig = AIAgentConfig(
                model = OpenAIModels.Chat.GPT5_6Luna,
                prompt = prompt("weather-agent", params = OpenAIChatParams(reasoningEffort = ReasoningEffort.NONE)) {
                    system("You are a weather assistant. Always call getWeather before answering.")
                },
                maxAgentIterations = 10,
            ),
            toolRegistry = ToolRegistry { tools(WeatherTools()) },
        ) {
            install(OpenTelemetry) { telemetry.configure(this) }
        }.use { agent ->
            // The second argument becomes gen_ai.conversation.id and the Future AGI session id.
            println(agent.run("What is the weather in Paris?", "user-42-chat-7"))
        }
    } finally {
        telemetry.shutdown() // flush, or the last spans of the run are lost
    }
}

Create one FutureAgiTelemetry per process and pass it to every agent. Call shutdown once, after the last agent is closed. A process killed with kill -9 loses the spans still in the batch.

What you see in Future AGI

Open your project in Observe to inspect the trace for an agent.run. A run with one tool call can have this structure:

create_agent weather-agent          agent   model
  invoke_agent weather-agent        agent   model
    strategy single_run             chain
      node __start__                chain
      node nodeCallLLM              chain
        chat gpt-5.6-luna           llm     model, input/output tokens
      node nodeExecuteTool          chain
        execute_tool getWeather     tool    tool name and description
      node nodeSendToolResult       chain
        chat gpt-5.6-luna           llm     model, input/output tokens
      node __finish__               chain
CapturedSource
Span names, timing, Koog’s parent linksKoog
gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokensKoog, on chat spans
Tool name, description and call idKoog, on execute_tool spans
Status ERROR and error.type (for example java.lang.IllegalStateException) on failed tool calls, model calls and runsKoog
fi.span.kind on agent, strategy and node spans; session.id from gen_ai.conversation.idThe recipe
Prompts, responses, system instructions, tool definitions, arguments and results, node inputs and outputs, raw error textOnly with the opt-in below

The recipe does not add user IDs. Koog reports 0 tokens on invoke_agent; look for per-call counts on the chat spans.

Privacy

Content is off by default. With it off, the exported spans carry no prompt, no response, no system prompt, no tool arguments or results, no node inputs or outputs, no raw error text and no span events. Koog writes HIDDEN:non-empty in place of each content attribute, and the recipe blanks the error message in the span status (Koog does not mask that one). Span names, ids, timing, model, token counts, tool names, status code and error.type are kept.

The recipe enforces this even if setVerbose(true) is called somewhere else in your code: with the opt-in off, the wrapper masks the content attributes again before export.

To turn content on:

export FI_KOOG_CAPTURE_CONTENT=true

This sets Koog’s verbose mode and keeps raw error text. Stack traces stay out even then: Koog serializes a failed tool’s stack trace into the node input and output, and the recipe empties that stackTrace field and drops exception.stacktrace from span events. Content then appears as span attributes (gen_ai.input.messages, gen_ai.output.messages, system_instructions, gen_ai.tool.call.arguments, gen_ai.tool.call.result, koog.node.input, koog.node.output). Future AGI shows them in the span’s attribute list. The Input and Output columns stay empty, because Future AGI does not read Koog’s message attributes into them yet.

No resource attribute names your machine: the resource holds project_name, project_type, service.name, service.version and the OpenTelemetry SDK name. API keys stay in HTTP headers and are never span attributes.

Why not setShutdownOnAgentClose

In Koog 1.3.0, setShutdownOnAgentClose(true) can shut down the exporter before the batch queue drains. The recipe builds the SDK, passes it with setSdk, and flushes it in shutdown(); the wrapper waits for each HTTP export. With setSdk, Koog ignores addSpanExporter, addResourceAttributes and addSpanProcessor, so the resource is set in createOpenTelemetry instead.

Troubleshooting

SymptomCause and fix
Spans print to the console, nothing in Future AGINo exporter is installed, so Koog uses its console exporter. Call telemetry.configure(this) inside install(OpenTelemetry).
Only some spans of a run arrive, invoke_agent missingThe JVM exited before the flush. Call telemetry.shutdown() in a finally block, after the agent is closed. Do not rely on setShutdownOnAgentClose(true).
HTTP 401 missing credentialsOne header is missing. Both X-Api-Key and X-Secret-Key are required.
No project appearsproject_name is missing from the resource. setServiceInfo does not set it.
Export fails with a gzip or 415 errorCompression was enabled. Releases before v1.56.0 answered HTTP 400; current releases accept gzip. On an older self-hosted release keep setCompression("none").
Spans show type unknownFutureAgiSpanExporter is not in the export path. Pass the OTLP exporter through it.
A failed run shows OK in the trace listKoog leaves the root create_agent span OK when a run fails. Open the trace: invoke_agent and the failed chat or execute_tool span are ERROR.
addResourceAttributes has no effectExpected with setSdk. Put attributes in the resource(...) map in FutureAgiTelemetry.

Limits

  • This guide is for the JVM; Android, iOS, JS and Wasm are outside its scope.
  • Through the Java exporter bridge every attribute value arrives as a string (opentelemetry-kotlin 0.3.0). Future AGI still reads model and token counts.
  • Koog 1.3.0 writes system instructions under system_instructions, not gen_ai.system_instructions.
  • The execute_tool and create_agent spans carry no conversation id, so they are not linked to the session. The rest of the run is.
  • The recipe depends on span names and attribute keys Koog chooses (invoke_agent, koog.node.id, …). Re-test when you upgrade Koog.
  • Do not add a metric exporter pointed at Future AGI: this endpoint accepts traces only.

How this differs from other pages

  • The Java pages cover traceai-java-* wrappers around specific clients. This guide exports Koog’s own spans.
  • Spring Boot covers Spring AI. If you run Koog inside Spring Boot, use this page for the Koog spans.
  • Koog’s built-in Langfuse, Weave and Datadog exporters are not used. They send to those vendors’ endpoints with their keys.

Next Steps

Was this page helpful?

Questions & Discussion