neatlogs

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

README

neatlogs-go

The Go SDK for Neatlogs — OpenTelemetry-based tracing for Go LLM agents. v1 focuses on Google Gemini support.

Spans are exported over OTLP/HTTP to the Neatlogs ingestion endpoint and keyed in the shared neatlogs.* attribute namespace used by the Python and TypeScript SDKs.

Install

go get github.com/neatlogs/neatlogs-go

Quick start

ctx := context.Background()

shutdown, err := neatlogs.Init(ctx, neatlogs.Config{
    APIKey:       os.Getenv("NEATLOGS_API_KEY"),
    WorkflowName: "my-agent",
})
if err != nil {
    log.Fatal(err)
}
defer shutdown(ctx)

client, _ := genai.NewClient(ctx, &genai.ClientConfig{APIKey: os.Getenv("GEMINI_API_KEY")})
gc := neatlogs.WrapGenAI(client) // the one added line

resp, _ := gc.GenerateContent(ctx, "gemini-2.5-flash", contents, config)

gc has the same method signatures as client.Models, so wrapping is a one-line change. See examples/genai.

Two ways spans reach Neatlogs

1. Active wrapping — WrapGenAI

Wrap a google.golang.org/genai client to trace each call with full detail: input/output messages, tool definitions and calls, invocation parameters, token usage, and finish reason. GenerateContent, GenerateContentStream, EmbedContent, and CountTokens are traced. Any untraced method is reachable via gc.Raw().

2. Passive passthrough — Google ADK and other OTel-native frameworks

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.

Attribute normalization is driven by the canonical attribute-mapping.json shared verbatim with the Python and TypeScript SDKs, so every span kind (llm, tool, agent, retriever, embedding, reranker, guardrail, mcp_tool, vector_store, and more) and every recognized source vocabulary (OpenTelemetry GenAI semconv, OpenInference, Traceloop) is translated into the neatlogs.* namespace by one shared contract. The mapper only renames keys; it performs no value rewriting or derived computation — the backend owns that.

ADK 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 the request and response on the trace, wrap the ADK model with contrib/adk's WrapModel:

import nladk "github.com/neatlogs/neatlogs-go/contrib/adk"

model, _ := gemini.NewModel(ctx, "gemini-2.5-flash", cfg)
agent, _ := llmagent.New(llmagent.Config{Model: nladk.WrapModel(model), ...})

WrapModel writes input/output messages onto the generate_content span ADK already emits. It lives in its own module so ADK's dependency tree stays out of the core SDK.

A2A (agent-to-agent)

A2A calls cross an HTTP boundary, so two extra pieces from contrib/adk keep the trace whole:

  • 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 (one linked trace end to end).
Examples
  • examples/genai — the WrapGenAI path.

  • examples/adk — every ADK path (single agent, streaming, tools, sequential/parallel/loop workflow agents, A2A, concurrent). Each runs one at a time to keep traces clean:

    go run . -scenario=tools      # or non-streaming, streaming, sequential,
                                  # parallel, loop, a2a, concurrent, or 'all'
    

    Both example modules are separate from the core SDK so heavy deps (ADK, a2a) stay out of it. The end-to-end test runs real ADK agents and asserts the spans arrive normalized to neatlogs.*.

Export runs on a background batch processor, so instrumentation never blocks or delays your agent code.

Configuration

Config field Env fallback Notes
APIKey NEATLOGS_API_KEY Your Neatlogs project key. Required to export; if empty, spans are dropped.
WorkflowName Service/run label grouping your traces. Defaults to the caller source file (e.g. main.go).
Tags Attached to every span (optional).

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 are translated before they leave the process.

Status

v1 — Google Gemini (google.golang.org/genai) active wrapping + OTel/ADK passthrough. More providers to follow.

Documentation

Overview

Package neatlogs is the Go SDK for Neatlogs — OpenTelemetry-based tracing for Go LLM agents.

It exports spans over OTLP/HTTP to the Neatlogs ingestion endpoint ({endpoint}/v1/traces) and normalizes their attributes into the neatlogs.* namespace shared with the Python and TypeScript SDKs.

There are two ways spans reach Neatlogs:

  • Active wrapping. Call WrapGenAI on a google.golang.org/genai client to trace each GenerateContent / GenerateContentStream / EmbedContent / CountTokens call with full request/response detail (including message text) on the span.

  • Passive passthrough. Init registers a *global* OpenTelemetry TracerProvider, so any OTel-native framework — notably Google ADK — emits spans that flow through Neatlogs automatically. ADK uses the OTel GenAI semantic conventions, which the SDK maps onto neatlogs.* keys. Note that ADK records prompt/completion text on the OTel logs signal rather than on spans, so the ADK passthrough captures model, token usage, tool calls and finish reasons, but not message text.

Typical usage:

ctx := context.Background()
shutdown, err := neatlogs.Init(ctx, neatlogs.Config{
	APIKey:       os.Getenv("NEATLOGS_API_KEY"),
	WorkflowName: "my-agent",
})
if err != nil { log.Fatal(err) }
defer shutdown(ctx)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Flush

func Flush(ctx context.Context) error

Flush forces a synchronous export of all buffered spans. Safe to call even when the SDK is not initialized (it is a no-op then).

func Identify

func Identify(ctx context.Context, opts IdentifyOptions) context.Context

Identify returns a copy of ctx carrying the given session + end-user identity. It is the single entry point for session/end-user on the Go SDK. Pass the returned context into Trace and/or your wrapped LLM calls; the (auto-)root span picks the identity up. Only set fields are bound, so calling Identify again overrides individual fields without clearing the others.

func Trace

func Trace(ctx context.Context, name string) (context.Context, trace.Span, func())

Trace opens a WORKFLOW root span named `name` and returns the child context, the span, and an end func the caller must invoke exactly once (usually via defer). It is the Go equivalent of Python's `with neatlogs.trace(...)` and TypeScript's `neatlogs.trace({...}, fn)`.

Session and end-user identity are NOT passed as arguments here — they ride on the context. Set them once at the request/turn boundary with Identify, then pass that context in:

ctx = neatlogs.Identify(ctx, neatlogs.IdentifyOptions{
    SessionID: "chat_123", EndUserID: "user_456",
    EndUserMetadata: map[string]any{"plan": "pro"},
})
ctx, span, end := neatlogs.Trace(ctx, "chat_turn") // reads identity from ctx
defer end()

Identity (session + end-user bound via Identify) is stamped on the root span by the identityProcessor, which reads it from the span's start context — so it applies to this WORKFLOW root, the WrapGenAI auto-root, and ADK passthrough spans uniformly. Trace itself only opens the span.

Types

type Config

type Config struct {
	// APIKey authenticates with Neatlogs. Falls back to NEATLOGS_API_KEY.
	// If empty after that fallback, export is disabled and spans are dropped.
	APIKey string

	// Endpoint is the Neatlogs ingestion base URL (without the /v1/traces
	// path). Falls back to NEATLOGS_ENDPOINT, then the staging default.
	Endpoint string

	// WorkflowName labels this service/run. Defaults to the executable name.
	WorkflowName string

	// Tags are attached to every span as a resource attribute.
	Tags []string

	// Debug enables verbose diagnostics on stderr.
	Debug bool

	// DisableExport drops all spans instead of sending them. Useful in tests.
	DisableExport bool
}

Config controls SDK initialization. All fields are optional except APIKey (which may also be supplied via the NEATLOGS_API_KEY environment variable).

type GenAIModels

type GenAIModels struct {
	// contains filtered or unexported fields
}

GenAIModels mirrors the call surface of (*genai.Client).Models, tracing each call. It is returned by WrapGenAI and is a drop-in replacement: the method signatures match google.golang.org/genai exactly, so existing call sites change by one line (the client they call) and nothing else.

func WrapGenAI

func WrapGenAI(client *genai.Client) *GenAIModels

WrapGenAI wraps a genai.Client so its model calls emit Neatlogs spans.

client, _ := genai.NewClient(ctx, &genai.ClientConfig{APIKey: key})
gc := neatlogs.WrapGenAI(client)
resp, _ := gc.GenerateContent(ctx, "gemini-2.5-flash", contents, cfg)

Spans carry full request/response detail — input/output messages, tool definitions and calls, invocation parameters, token usage and finish reason — keyed in the neatlogs.* namespace. The Vertex AI backend is detected from the client config and tagged distinctly from the Gemini API.

func (*GenAIModels) CountTokens

func (g *GenAIModels) CountTokens(ctx context.Context, model string, contents []*genai.Content, config *genai.CountTokensConfig) (*genai.CountTokensResponse, error)

CountTokens traces a token-counting call.

func (*GenAIModels) EmbedContent

func (g *GenAIModels) EmbedContent(ctx context.Context, model string, contents []*genai.Content, config *genai.EmbedContentConfig) (*genai.EmbedContentResponse, error)

EmbedContent traces an embedding call.

func (*GenAIModels) GenerateContent

func (g *GenAIModels) GenerateContent(ctx context.Context, model string, contents []*genai.Content, config *genai.GenerateContentConfig) (*genai.GenerateContentResponse, error)

GenerateContent traces a single content-generation call.

func (*GenAIModels) GenerateContentStream

func (g *GenAIModels) GenerateContentStream(ctx context.Context, model string, contents []*genai.Content, config *genai.GenerateContentConfig) iter.Seq2[*genai.GenerateContentResponse, error]

GenerateContentStream traces a streaming generation, accumulating chunks to reconstruct the response when the stream is fully consumed.

func (*GenAIModels) Raw

func (g *GenAIModels) Raw() *genai.Models

Raw returns the underlying genai.Models for any method this wrapper does not trace (e.g. cached-content operations), so callers are never blocked.

type IdentifyOptions

type IdentifyOptions struct {
	SessionID       string
	EndUserID       string
	EndUserMetadata map[string]any
}

IdentifyOptions carries the request-scoped identity for Identify. All fields are optional; only set ones are bound onto the context.

type Option

type Option func(*initOptions)

Option customizes Init. Options are for advanced/testing use; the common path needs only Config.

func WithExporter

func WithExporter(exp sdktrace.SpanExporter) Option

WithExporter overrides the OTLP/HTTP exporter with a custom SpanExporter. The SDK still wraps it so attributes are normalized to neatlogs.* before export. Useful for tests (in-memory exporter) or alternate sinks (stdout). When set, Config.Endpoint/APIKey are ignored for transport, but DisableExport still suppresses all export.

type ShutdownFunc

type ShutdownFunc func(context.Context) error

ShutdownFunc flushes pending spans and releases SDK resources. Call it (often via defer) before the process exits so buffered spans are not lost.

func Init

func Init(ctx context.Context, cfg Config, opts ...Option) (ShutdownFunc, error)

Init configures the global OpenTelemetry TracerProvider for Neatlogs and returns a ShutdownFunc. It is safe to call once; a second call without an intervening shutdown returns an error.

Directories

Path Synopsis
contrib
adk module
examples
genai command
Command genai demonstrates tracing a Google Gemini call with neatlogs-go via the active WrapGenAI wrapper (the path for apps that call the genai SDK directly, without a framework like ADK).
Command genai demonstrates tracing a Google Gemini call with neatlogs-go via the active WrapGenAI wrapper (the path for apps that call the genai SDK directly, without a framework like ADK).
internal
attributes
Package attributes defines the canonical neatlogs.* span attribute keys the SDK's own wrappers emit, plus a JSON-driven Mapper (see mapper.go) that normalizes attributes from any source — OpenTelemetry GenAI semconv (as emitted by Google ADK and others), OpenInference, Traceloop — into the same neatlogs.* namespace.
Package attributes defines the canonical neatlogs.* span attribute keys the SDK's own wrappers emit, plus a JSON-driven Mapper (see mapper.go) that normalizes attributes from any source — OpenTelemetry GenAI semconv (as emitted by Google ADK and others), OpenInference, Traceloop — into the same neatlogs.* namespace.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL