neatlogs

package module
v0.1.2 Latest Latest
Warning

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

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

README

neatlogs-go

The Go SDK for Neatlogs — OpenTelemetry-based tracing for Go LLM agents. The initial release line focuses on Google Gemini support and explicit instrumentation helpers.

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
go get github.com/neatlogs/neatlogs-go/contrib/genai

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 := nlgenai.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.

Instrumentation and isolation

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. Isolation from other tracing SDKs

Init creates a private OpenTelemetry TracerProvider. It never replaces the process-global provider or propagator, so Datadog and other instrumentation cannot export, parent, or be parented by Neatlogs spans.

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.

Frameworks that resolve tracers only from the global OTel API are not auto-instrumented. They require an explicit wrapper or an injected private tracer/provider integration. Google ADK support is therefore deferred until that integration can be isolated bidirectionally.

Cross-process propagation

Use InjectTraceContext and ExtractTraceContext at explicit HTTP/RPC boundaries. They use a private W3C trace-context propagator and do not touch the global OTel propagator.

For direct OpenAI/Anthropic calls and service boundaries, use StartLLMSpan, StartRetrieverSpan, StartToolSpanFromHeaders, or the lower-level Trace/StartSpan helpers. These all share the same private provider and automatic workflow-root behavior.

Examples

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 created through Neatlogs wrappers or an injected private tracer are translated before they leave the process.

Status

v0.1 — Google Gemini (google.golang.org/genai) active wrapping, direct LLM/retriever/tool helpers, and an isolated private provider. More providers and explicit framework wrappers will 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.

Spans reach Neatlogs through explicit SDK wrappers and tracing helpers:

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

Init never changes the process-global OpenTelemetry provider or propagator. This keeps Neatlogs isolated from Datadog and other co-tenant instrumentation. OTel-native frameworks that only use the global provider must be integrated through an explicit Neatlogs wrapper.

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 ExtractTraceContext added in v0.1.1

func ExtractTraceContext(
	ctx context.Context,
	carrier propagation.TextMapCarrier,
) context.Context

ExtractTraceContext returns a context containing the remote Neatlogs parent found in carrier. It does not activate or alter process-global OTel context.

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 InjectTraceContext added in v0.1.1

func InjectTraceContext(ctx context.Context, carrier propagation.TextMapCarrier)

InjectTraceContext writes the Neatlogs span carried by ctx into carrier.

func StartProviderSpan added in v0.1.1

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

StartProviderSpan starts a span of the given kind, opening an auto-root first when needed. The returned end func ends the provider span and the auto-root (if one was opened); callers must invoke it exactly once. Every provider-span kind passes through here, so all wrapper paths (sync, stream finalize, error) get auto-root coverage by calling end().

func StartSpan added in v0.1.1

func StartSpan(
	ctx context.Context,
	name string,
	kind string,
	attributes ...attribute.KeyValue,
) (context.Context, trace.Span, func())

StartSpan starts an explicitly typed Neatlogs span on the private provider. Use it at framework and service boundaries where Trace's WORKFLOW kind is not appropriate (for example a TOOL child extracted from an incoming trace).

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 spans created by integrations using the private provider. 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 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 LLMCallOptions added in v0.1.1

type LLMCallOptions struct {
	// Provider is the neatlogs provider id, e.g. "openai" or "anthropic".
	Provider string
	// System is the provider system label; defaults to Provider when empty.
	System string
	// Model is the model name, e.g. "gpt-5.5" or "claude-sonnet-4-6".
	Model     string
	Streaming bool

	MaxTokens   int
	Temperature *float64
	TopP        *float64

	// Messages are the input messages (including any system message).
	Messages []LLMMessage
	// Name overrides the span name; defaults to "{provider}.chat".
	Name string
}

LLMCallOptions describes a provider LLM call for StartLLMSpan. Only Provider and Model are typically required; the rest are recorded when set.

type LLMMessage added in v0.1.1

type LLMMessage struct {
	Role    string
	Content string
}

LLMMessage is one request or response message on an LLM span.

type LLMSpan added in v0.1.1

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

LLMSpan is a handle to an LLM span opened by StartLLMSpan. Record the result with SetOutputMessage/SetUsage/SetFinishReason (or SetError), then call End exactly once (via defer).

It is the Go analogue of the Python/TypeScript SDKs' LLM span: it captures provider/model, input and output messages, invocation parameters, token usage and finish reason under the neatlogs.* namespace. Use it to instrument direct provider SDK calls (OpenAI, Anthropic, ...) that WrapGenAI does not cover. The caller imports only this package — no OpenTelemetry types.

func StartLLMSpan added in v0.1.1

func StartLLMSpan(ctx context.Context, opts LLMCallOptions) (context.Context, *LLMSpan)

StartLLMSpan opens an LLM span for a provider call, auto-rooting under a WORKFLOW span when the context has no active parent (so a standalone call still yields a renderable trace) and nesting under an existing parent otherwise (e.g. a TOOL span continued from an upstream service). It uses the private Neatlogs provider only; the process-global OTel / Datadog context is never read or mutated. When Neatlogs is not initialized the span is a no-op.

Call End on the returned LLMSpan exactly once, usually via defer.

func (*LLMSpan) End added in v0.1.1

func (s *LLMSpan) End()

End closes the span (and its auto-root, if one was opened). Call exactly once.

func (*LLMSpan) SetError added in v0.1.1

func (s *LLMSpan) SetError(err error)

SetError marks the span failed and records err.

func (*LLMSpan) SetFinishReason added in v0.1.1

func (s *LLMSpan) SetFinishReason(reason string)

SetFinishReason records the provider finish/stop reason.

func (*LLMSpan) SetModel added in v0.1.1

func (s *LLMSpan) SetModel(model string)

SetModel overrides the model name recorded at start. Use it when the authoritative model is only known from the response (e.g. a provider that resolves an alias, or a fallback path that switches models). No-op for "".

func (*LLMSpan) SetOutputMessage added in v0.1.1

func (s *LLMSpan) SetOutputMessage(role, content string)

SetOutputMessage records the assistant's response message (index 0).

func (*LLMSpan) SetProvider added in v0.1.1

func (s *LLMSpan) SetProvider(provider string)

SetProvider overrides the provider/system recorded at start. Use it when the serving provider is only known after the call (e.g. a primary→fallback switch). No-op for "".

func (*LLMSpan) SetResponseID added in v0.1.1

func (s *LLMSpan) SetResponseID(id string)

SetResponseID records the provider response id.

func (*LLMSpan) SetUsage added in v0.1.1

func (s *LLMSpan) SetUsage(promptTokens, completionTokens, totalTokens int)

SetUsage records token usage. Pass 0 for any count that is unknown.

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 RetrieverSpan added in v0.1.1

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

RetrieverSpan is a handle to a retrieval / semantic-memory span opened by StartRetrieverSpan. Record results with SetDocuments/SetDocumentCount (or SetError), then call End exactly once (via defer).

Retrieval-shaped operations — vector search, RAG document lookup, and agent memory recall — all use span kind "retriever"; there is no separate "memory" kind in the canonical mapping.

func StartRetrieverSpan added in v0.1.1

func StartRetrieverSpan(ctx context.Context, name, query string, topK int) (context.Context, *RetrieverSpan)

StartRetrieverSpan opens a RETRIEVER span for a retrieval / memory operation named name, recording the query and (optional) top-k. It auto-roots when the context has no active parent and nests under an existing parent otherwise (e.g. a TOOL span continued from an upstream service). Uses the private Neatlogs provider only; when Neatlogs is not initialized the span is a no-op.

Call End on the returned RetrieverSpan exactly once, usually via defer.

func (*RetrieverSpan) End added in v0.1.1

func (r *RetrieverSpan) End()

End closes the span (and its auto-root, if one was opened). Call exactly once.

func (*RetrieverSpan) SetDocumentCount added in v0.1.1

func (r *RetrieverSpan) SetDocumentCount(count int)

SetDocumentCount records how many documents the retrieval returned.

func (*RetrieverSpan) SetDocuments added in v0.1.1

func (r *RetrieverSpan) SetDocuments(documents any, count int)

SetDocuments records the retrieved documents (JSON-encoded) and their count. It also writes the same JSON to the generic output field so every completed retrieval, including an empty result set, has an explicit I/O output.

func (*RetrieverSpan) SetError added in v0.1.1

func (r *RetrieverSpan) SetError(err error)

SetError marks the span failed and records err.

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 a private OpenTelemetry TracerProvider for Neatlogs and returns a ShutdownFunc. It never changes process-global OpenTelemetry state. It is safe to call once; a second call without an intervening shutdown returns an error.

type ToolSpan added in v0.1.1

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

ToolSpan is a handle to a TOOL span opened by StartToolSpanFromHeaders. Record the result with SetOutput or SetError, then call End exactly once (via defer).

func StartToolSpanFromHeaders added in v0.1.1

func StartToolSpanFromHeaders(
	ctx context.Context,
	headers http.Header,
	toolName string,
	input string,
	opts IdentifyOptions,
) (context.Context, *ToolSpan)

StartToolSpanFromHeaders continues the Neatlogs trace carried on an incoming request's headers (injected by an upstream Neatlogs SDK) and opens a TOOL child span named toolName, binding the given identity and recording input.

It exists so a service boundary can be instrumented with Neatlogs alone: header extraction, the private W3C propagator, span-kind and tool.* attribute keys, and error status are all handled here, so the caller imports only this package — no OpenTelemetry types. It uses the private Neatlogs propagator and provider only; the process-global OTel / Datadog context is never read or mutated. When Neatlogs is not initialized the span is a no-op and this just returns a wrapped context.

Call End on the returned ToolSpan exactly once, usually via defer.

func (*ToolSpan) End added in v0.1.1

func (t *ToolSpan) End()

End closes the span. Call exactly once, usually via defer.

func (*ToolSpan) SetError added in v0.1.1

func (t *ToolSpan) SetError(err error)

SetError marks the span failed and records err as the tool output.

func (*ToolSpan) SetOutput added in v0.1.1

func (t *ToolSpan) SetOutput(output string)

SetOutput records the tool's output payload on the span.

Directories

Path Synopsis
contrib
adk module
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