Go SDK
Install, initialize, and instrument Go LLM and agent apps with Neatlogs.
The Go SDK installs as github.com/neatlogs/neatlogs-go. It is built on OpenTelemetry: one Init call installs a global tracer provider and exports spans to Neatlogs over OTLP/HTTP. v1 focuses on Google Gemini — wrap a google.golang.org/genai client directly, or let spans from Google ADK flow through automatically. This page covers everything from install to the full Config reference; use the contents on the right to jump around.
New to tracing? A trace is the record of one run of your app; a span is one step inside it (an LLM call, a tool call). You instrument once, then read traces in the dashboard. See Introduction.
Install
go get github.com/neatlogs/neatlogs-goRequires Go ≥ 1.23. ADK helpers live in a separate module so the heavy ADK dependency tree stays out of the core SDK:
go get github.com/neatlogs/neatlogs-go/contrib/adkYour first trace
Call neatlogs.Init once at startup and defer the returned shutdown so buffered spans flush before the process exits. Then wrap your Gemini client with neatlogs.WrapGenAI — the wrapper has the same method signatures as client.Models, so wrapping is a one-line change.
package main
import (
"context"
"fmt"
"log"
"os"
"google.golang.org/genai"
neatlogs "github.com/neatlogs/neatlogs-go"
)
func main() {
ctx := context.Background()
shutdown, err := neatlogs.Init(ctx, neatlogs.Config{
APIKey: os.Getenv("NEATLOGS_API_KEY"),
WorkflowName: "my-first-app",
})
if err != nil {
log.Fatal(err)
}
defer shutdown(ctx)
client, err := genai.NewClient(ctx, &genai.ClientConfig{
APIKey: os.Getenv("GEMINI_API_KEY"),
Backend: genai.BackendGeminiAPI,
})
if err != nil {
log.Fatal(err)
}
gc := neatlogs.WrapGenAI(client) // the one added line
resp, err := gc.GenerateContent(ctx, "gemini-2.5-flash",
genai.Text("What is the capital of France?"), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Text())
}Run it and a trace appears under my-first-app within a few seconds. WrapGenAI opens a workflow root automatically (named after your WorkflowName) with the llm span nested inside — no extra wrapper needed.
Always defer shutdown(ctx) (or call neatlogs.Flush(ctx)) before a short-lived program exits — export runs on a background batch processor, so a process that ends immediately can drop its last spans. Long-running servers export continuously; call shutdown once on server shutdown.
Two ways spans reach Neatlogs
1. Active wrapping — WrapGenAI
WrapGenAI wraps a *genai.Client and traces each call with full request/response detail: input/output messages, tool definitions and calls, invocation parameters, token usage, and finish reason. It detects the Vertex AI backend from the client config and tags it distinctly from the Gemini API.
gc := neatlogs.WrapGenAI(client)
// Same signatures as genai's client.Models:
resp, err := gc.GenerateContent(ctx, "gemini-2.5-flash", contents, config)
for resp, err := range gc.GenerateContentStream(ctx, "gemini-2.5-flash", contents, config) {
// streaming chunks
}
emb, err := gc.EmbedContent(ctx, "text-embedding-004", contents, nil)
tok, err := gc.CountTokens(ctx, "gemini-2.5-flash", contents, nil)Any method the wrapper doesn't trace is reachable via gc.Raw(), which returns the underlying *genai.Models.
2. Passive passthrough — Google ADK
Init registers the global OpenTelemetry TracerProvider. Frameworks that emit OpenTelemetry GenAI semantic-convention spans — notably Google ADK — flow through Neatlogs automatically, with no per-call wrapping. The SDK normalizes their gen_ai.* and gcp.vertex.agent.* attributes into the neatlogs.* namespace, classifying span kinds (invoke_agent → agent, generate_content → llm, execute_tool → tool).
// The only Neatlogs-specific lines — no ADK call is wrapped.
shutdown, _ := neatlogs.Init(ctx, neatlogs.Config{
APIKey: os.Getenv("NEATLOGS_API_KEY"),
WorkflowName: "adk-agent",
})
defer shutdown(ctx)
// ... build and run your ADK agent exactly as usual; its spans are captured.ADK-Go records prompt/completion text on the OpenTelemetry logs signal, not on spans — so plain passthrough captures model, token usage, tool calls, and finish reasons, but not message text. To put request/response text on the trace, wrap the ADK model with WrapModel (below).
ADK input/output capture — WrapModel
contrib/adk closes the message-text gap: WrapModel wraps an ADK model.LLM so each GenerateContent call writes its request and response onto the generate_content span ADK already emits.
import (
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/model/gemini"
nladk "github.com/neatlogs/neatlogs-go/contrib/adk"
)
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
agent, err := llmagent.New(llmagent.Config{
Name: "weather_agent",
Model: nladk.WrapModel(model), // capture I/O on the trace
Instruction: "Answer the user's question.",
})A2A (agent-to-agent)
A2A calls cross an HTTP boundary, so trace context must be carried across it for client and remote agent to land in one trace. contrib/adk provides the pieces:
A2AHTTPClient()— an*http.Clientwhose transport injects the W3Ctraceparent. Pass it to the A2A client factory so outbound calls carry the trace context.A2ABeforeRequest/A2AAfterRequest— request/response callbacks that record the sent message and the reply on the client'sinvoke_agentspan (which has no local LLM to capture I/O from otherwise).A2AHandler(mux)— server middleware that extracts the incomingtraceparent, so a server you own nests its spans under the caller's trace.
import (
"github.com/a2aproject/a2a-go/v2/a2aclient"
"google.golang.org/adk/agent/remoteagent/v2"
nladk "github.com/neatlogs/neatlogs-go/contrib/adk"
)
factory := a2aclient.NewFactory(a2aclient.WithJSONRPCTransport(nladk.A2AHTTPClient()))
remote, err := remoteagent.NewA2A(remoteagent.A2AConfig{
Name: "remote_weather",
AgentCardProvider: remoteagent.NewAgentCardProvider(serverURL),
ClientProvider: remoteagent.NewA2AClientProvider(factory),
BeforeRequestCallbacks: []remoteagent.BeforeA2ARequestCallback{nladk.A2ABeforeRequest},
AfterRequestCallbacks: []remoteagent.AfterA2ARequestCallback{nladk.A2AAfterRequest},
})Span kinds
The SDK normalizes every span into the shared neatlogs.* namespace with one of these kinds. The genai wrapper produces llm and embedding spans; ADK passthrough adds agent and tool; the auto-root produces workflow. The full taxonomy (llm, tool, agent, chain, workflow, retriever, embedding, reranker, guardrail, mcp_tool, and more) is shared across all Neatlogs SDKs — see Span Kinds.
Transport
Standard OTLP/HTTP via go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp, targeting {endpoint}/v1/traces with an x-api-key header. Attribute normalization to the neatlogs.* namespace happens at the exporter boundary, so spans from any source — your WrapGenAI calls or passthrough ADK spans — are translated before they leave the process. Export is batched on a background goroutine and never blocks your agent code.
Config reference
shutdown, err := neatlogs.Init(ctx, neatlogs.Config{ /* … */ })| Field | Type | Default | Description |
|---|---|---|---|
APIKey | string | NEATLOGS_API_KEY env | Project API key. Export disabled (spans dropped) if unset. |
WorkflowName | string | caller source file | Label all traces appear under (e.g. main.go). |
SessionID | string | — | Group related traces (multi-turn). |
Tags | []string | — | Tags attached to every span. |
Lifecycle: Init returns a ShutdownFunc — defer shutdown(ctx) to flush and release resources. neatlogs.Flush(ctx) exports buffered spans immediately without shutting down (a no-op if not initialized).
Building in another language? See the Python SDK and TypeScript SDK.
