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.

Note

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-go

Requires 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/adk

Your 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.

Warning

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_agentagent, generate_contentllm, execute_tooltool).

// 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.
Note

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.Client whose transport injects the W3C traceparent. 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's invoke_agent span (which has no local LLM to capture I/O from otherwise).
  • A2AHandler(mux) — server middleware that extracts the incoming traceparent, 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{ /* … */ })
FieldTypeDefaultDescription
APIKeystringNEATLOGS_API_KEY envProject API key. Export disabled (spans dropped) if unset.
WorkflowNamestringcaller source fileLabel all traces appear under (e.g. main.go).
SessionIDstringGroup related traces (multi-turn).
Tags[]stringTags attached to every span.

Lifecycle: Init returns a ShutdownFuncdefer 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.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

Ask anything about instrumenting, tracing, or the Neatlogs dashboard.