OpenAI Go tracing with Future AGI
Trace the official OpenAI Go SDK (openai-go v3) with the traceAI Go middleware: one LLM span per chat completion, streaming and tool calls included, exported to Future AGI over OTLP/HTTP.
traceaiopenai (module github.com/future-agi/traceAI/go/openai) traces Go services that call OpenAI with the official SDK, github.com/openai/openai-go/v3. Add its middleware when you build the client to create an LLM span for each Chat Completions request, including streamed requests. The SDK keeps its own HTTP client and transport policy.
Use Go 1.26 or later with openai-go/v3 v3.71.1 or later. The commands below install traceAI module v0.1.0, openai-go/v3 v3.78.0 and OpenTelemetry Go v1.47.0.
- OpenAI (Python and JS/TS) and OpenAI Java use separate integrations. The Go module writes the same attribute names as the Python package.
- Arize’s
openinference-instrumentation-openai-gotargetsgithub.com/openai/openai-gov1, a different import path from v3, and writes OpenInference attribute names. It is not required for this setup.
Source: traceAI go/openai.
1. Install
Add the released packages to your Go module:
go get github.com/future-agi/traceAI/go/openai@v0.1.0 \
github.com/openai/openai-go/v3@v3.78.0 \
go.opentelemetry.io/otel/sdk@v1.47.0 \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@v1.47.0
2. Set environment variables
export FI_API_KEY="your-futureagi-api-key"
export FI_SECRET_KEY="your-futureagi-secret-key"
export FI_PROJECT_NAME="my-service"
export OPENAI_API_KEY="your-openai-api-key"
FI_BASE_URL is optional (default https://api.futureagi.com; set it to your own collector when you self-host). The OpenAI SDK reads OPENAI_API_KEY and OPENAI_BASE_URL itself.
3. Create the tracer provider
The module does not build an exporter. Use the stock OpenTelemetry OTLP/HTTP exporter with the three things Future AGI needs: both auth headers, the project_name resource attribute, and an uncompressed body (the collector rejects gzip).
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
traceaiopenai "github.com/future-agi/traceAI/go/openai"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/option"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
func newTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
apiKey, secretKey := os.Getenv("FI_API_KEY"), os.Getenv("FI_SECRET_KEY")
project := os.Getenv("FI_PROJECT_NAME")
if apiKey == "" || secretKey == "" || project == "" {
return nil, errors.New("set FI_API_KEY, FI_SECRET_KEY and FI_PROJECT_NAME")
}
base := strings.TrimRight(envOr("FI_BASE_URL", "https://api.futureagi.com"), "/")
exp, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpointURL(base+"/tracer/v1/traces"),
otlptracehttp.WithHeaders(map[string]string{
"X-Api-Key": apiKey,
"X-Secret-Key": secretKey,
}),
otlptracehttp.WithCompression(otlptracehttp.NoCompression),
)
if err != nil {
return nil, err
}
res, err := resource.Merge(resource.Default(), resource.NewSchemaless(
attribute.String("project_name", project),
attribute.String("project_type", "observe"),
attribute.String("service.name", project),
))
if err != nil {
return nil, err
}
return sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithBatcher(exp),
), nil
}
func shutdown(tp *sdktrace.TracerProvider) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := tp.Shutdown(ctx); err != nil {
fmt.Fprintln(os.Stderr, "trace export:", err)
}
}
resource.Default() adds only the SDK name and version and OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES if you set them. No host, process or OS detector is added, so no host name, user name or local path is exported.
4. Add the middleware and flush on exit
tp, err := newTracerProvider(ctx)
if err != nil {
return err
}
defer shutdown(tp) // flush before exit
// OPENAI_API_KEY and OPENAI_BASE_URL are read by the SDK as usual.
client := openai.NewClient(
option.WithMiddleware(traceaiopenai.Middleware(traceaiopenai.WithTracerProvider(tp))),
)
Call the SDK as usual. defer shutdown(tp) matters: the batch exporter sends spans in the background, and spans still queued when the process exits are lost. Without WithTracerProvider the middleware uses the OpenTelemetry global provider (otel.SetTracerProvider).
For a stream, ask for the usage chunk if you want token counts on the span:
params.StreamOptions = openai.ChatCompletionStreamOptionsParam{IncludeUsage: openai.Bool(true)}
s := client.Chat.Completions.NewStreaming(ctx, params)
defer s.Close()
The span ends when the stream reaches [DONE], fails, is cancelled, or is closed.
5. Sessions and your own spans
Put a session id on the context; every chat span started from it carries session.id, and Future AGI groups those traces into one session. ContextWithUserID does the same for user.id (use an opaque id, not an email address).
sessionCtx := traceaiopenai.ContextWithSessionID(ctx, *session)
ctx, agent := tracer.Start(sessionCtx, "weather-agent", trace.WithAttributes(
attribute.String(traceaiopenai.GenAISpanKind, traceaiopenai.SpanKindAgent),
attribute.String(traceaiopenai.GenAIOperationName, "invoke_agent"),
attribute.String("gen_ai.agent.name", "weather-agent"),
), trace.WithAttributes(traceaiopenai.ContextAttributes(sessionCtx)...))
The middleware spans the model calls. Your agent loop and your tool code are yours to span, as above; ContextAttributes copies the session and user ids onto them, and TraceConfig.SetAttributes applies the same privacy switches to the values you add:
tc.SetAttributes(span,
attribute.String(traceaiopenai.InputValue, args),
attribute.String(traceaiopenai.InputMimeType, traceaiopenai.MimeTypeJSON))
6. Run the example
The complete program—including an agent span, a get_weather tool call, a streamed answer and an example error—is main.go.
Download the program into a new directory, initialize its module, and install the released packages:
mkdir openai-go-example
cd openai-go-example
curl -fLO https://raw.githubusercontent.com/future-agi/docs/main/examples/traceai/openai-go/main.go
go mod init example.com/openai-go-example
go get github.com/future-agi/traceAI/go/openai@v0.1.0 \
github.com/openai/openai-go/v3@v3.78.0 \
go.opentelemetry.io/otel/sdk@v1.47.0 \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@v1.47.0
go mod tidy
With the environment variables from step 2 set, run the agent and inspect its traces:
go run . -stream -fail
Open Observe → your project → Tracing. The agent run appears as one trace:
weather-agent agent span
├── chat gpt-4.1-mini model requests get_weather
├── execute_tool get_weather tool span
└── chat gpt-4.1-mini streamed final answer
The LLM spans show the model, provider, token counts and conversation. The -fail call appears as a separate trace with an ERROR span.
What is captured
| Attribute | When |
|---|---|
gen_ai.span.kind = LLM, gen_ai.operation.name = chat, gen_ai.provider.name | always (azure for Azure OpenAI hosts, otherwise openai) |
gen_ai.request.model, gen_ai.request.temperature, top_p, max_tokens, seed, frequency_penalty, presence_penalty, stop_sequences | when set on the request |
gen_ai.request.parameters | allow-listed request options as JSON |
gen_ai.response.id, gen_ai.response.model, gen_ai.response.finish_reasons | when the response has them |
gen_ai.usage.input_tokens, output_tokens, total_tokens, cache_read_tokens, output_tokens.reasoning | only when the API returns usage; missing usage is not synthesized as zero |
gen_ai.input.messages.*, gen_ai.output.messages.* (roles, text, image URLs, tool calls with id, name, arguments), gen_ai.tool.definitions.*, input.value, output.value | content; on by default |
session.id, user.id | when set on the context |
http.response.status_code, http.request.resend_count, error.type, error.message | on errors and retries |
Never captured, in any mode: request headers (the OpenAI key, Authorization, the Future AGI keys), transport error text (it carries URLs and host names; only a class such as transport, canceled or timeout is recorded), audio bytes, and the free-form request fields user, metadata, safety_identifier and prompt_cache_key.
Privacy
Content is captured by default. Configure privacy through environment variables or pass traceaiopenai.WithTraceConfig(traceaiopenai.TraceConfig{...}) when constructing the middleware.
| Variable | Effect |
|---|---|
FI_HIDE_INPUTS=true | input.value becomes __REDACTED__; gen_ai.input.messages.*, system instructions, the standalone gen_ai.tool.call.arguments attribute and the API error message are dropped |
FI_HIDE_OUTPUTS=true | output.value becomes __REDACTED__; gen_ai.output.messages.*, the standalone gen_ai.tool.call.result attribute and the API error message are dropped |
FI_HIDE_INPUT_MESSAGES=true / FI_HIDE_OUTPUT_MESSAGES=true | drop only gen_ai.input.messages.* / gen_ai.output.messages.* |
FI_HIDE_INPUT_TEXT=true | redact text only in gen_ai.input.messages.*; keep roles and tool calls |
FI_HIDE_OUTPUT_TEXT=true | redact text only in gen_ai.output.messages.*; keep roles and tool calls |
FI_HIDE_INPUT_IMAGES=true | drop image URLs only from gen_ai.input.messages.* |
FI_HIDE_LLM_INVOCATION_PARAMETERS=true | drop gen_ai.request.parameters |
FI_BASE64_IMAGE_MAX_LENGTH | redact data: images longer than this (default 32000) |
Tool data follows the conversation channel. Model-generated tool-call arguments are outputs (output.value and gen_ai.output.messages.*), so FI_HIDE_INPUTS=true alone does not hide them. Tool results returned to the model are inputs (input.value and gen_ai.input.messages.*), so FI_HIDE_OUTPUTS=true alone does not hide them. The standalone tool-attribute rules do not remove every copy of tool data. To hide tool data across a conversation, enable both FI_HIDE_INPUTS=true and FI_HIDE_OUTPUTS=true.
The message, text and image switches alone do not prevent content export. With these granular switches, input.value retains original prompts and image URLs (including base64 images up to FI_BASE64_IMAGE_MAX_LENGTH), and output.value retains original answers. To withhold this content from telemetry, set FI_HIDE_INPUTS=true and FI_HIDE_OUTPUTS=true before constructing the middleware.
A boolean switch counts only when it is true (any case). With both broad switches enabled, the model, token counts, tool names, session id and error status are still exported; prompts, answers, images, tool arguments and results are not.
Errors, cancellation and retries
- An HTTP 4xx or 5xx response, or an
errorevent inside a stream, ends the span with status ERROR,http.response.status_codeanderror.type. The API’s error message is added unless inputs or outputs are hidden. The SDK still returns its error to you unchanged. - A context cancelled or timed out before the response completes ends the span with ERROR and
error.typecanceledortimeout. A stream you close early yourself ends without an error. - openai-go runs middleware once per HTTP attempt, so a retried call is one span per attempt; retries carry
http.request.resend_count. Start a span around the call, as the example’s agent span does, so the attempts share one trace, or useoption.WithMaxRetries(0). - One SDK call is one span. Calling again after an error is a new attempt and a new span.
Not supported
- The Responses API, Embeddings, Images, Audio, Realtime and the Assistants API pass through without a span. Only Chat Completions are traced.
github.com/openai/openai-gov1 (the old import path).- Cost is not computed in Go; Future AGI prices the model from the token counts.
Troubleshooting
- No traces. Check that the process calls
shutdown(tp)(ortp.Shutdown) before it exits, and thatFI_API_KEY,FI_SECRET_KEYandFI_PROJECT_NAMEare set. The exporter URL must be the full traces URL,{FI_BASE_URL}/tracer/v1/traces. - Export errors about the body. Keep
otlptracehttp.WithCompression(otlptracehttp.NoCompression); the collector rejects gzip. - Spans in the wrong project or none at all.
project_namemust be a resource attribute on the tracer provider, not a span attribute. - No token counts on a streamed span. Set
StreamOptions.IncludeUsage; without it the API sends no usage chunk. - Every retry is a separate trace. Start a parent span around the call (step 5).
- No span at all for a call. Only Chat Completions are traced; check that the middleware is on the client (or on that request with
option.WithMiddleware).
Questions & Discussion