OpenTelemetry (OTLP)

Point any OpenTelemetry gRPC trace exporter at Neatlogs and your spans are ingested — no Neatlogs SDK required.

Neatlogs speaks OTLP (the OpenTelemetry Protocol) over gRPC, so an application that already emits OpenTelemetry spans can send traces without installing a Neatlogs SDK. Point any OpenTelemetry trace exporter from an OpenTelemetry Collector, an instrumentation library like OpenLLMetry, or a framework that emits the OpenTelemetry GenAI semantic conventions.

Note

Prefer a Neatlogs SDK when one exists for your language (Python, TypeScript, Go). For an unsupported language, default to the dependency-free HTTP ingest endpoint POST /v1/trace. Use OTLP/gRPC when the application already has OpenTelemetry instrumentation or a Collector.


Endpoint

Note

gRPC endpoint: ingest.neatlogs.com:443


Authentication

Authenticate with your project key — the same key you pass to init() — sent as x-api-key in the gRPC metadata:

Warning

Use the x-api-key metadata key — not an Authorization: Bearer token.


Getting started

Configure a standard OpenTelemetry gRPC trace exporter with the Neatlogs endpoint and your x-api-key, then register it on your tracer provider.

import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

exporter = OTLPSpanExporter(
    endpoint="ingest.neatlogs.com:443",
    headers=(("x-api-key", os.environ["NEATLOGS_API_KEY"]),),
)

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)

# Spans are batched — flush before your program exits, or they're lost.
provider.shutdown()
import { Metadata } from '@grpc/grpc-js';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';

const metadata = new Metadata();
metadata.set('x-api-key', process.env.NEATLOGS_API_KEY!);

const exporter = new OTLPTraceExporter({
  url: 'https://ingest.neatlogs.com',
  metadata,
});

const provider = new NodeTracerProvider({
  spanProcessors: [new BatchSpanProcessor(exporter)],
});
provider.register();

// Spans are batched — flush before your process exits, or they're lost.
await provider.shutdown();
import (
	"context"
	"crypto/tls"
	"log"
	"os"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	"google.golang.org/grpc/credentials"
)

exporter, err := otlptracegrpc.New(context.Background(),
	otlptracegrpc.WithEndpoint("ingest.neatlogs.com:443"),
	otlptracegrpc.WithHeaders(map[string]string{
		"x-api-key": os.Getenv("NEATLOGS_API_KEY"),
	}),
	otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{})),
)
if err != nil {
	log.Fatal(err)
}

provider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
otel.SetTracerProvider(provider)

// Spans are batched — flush before your program exits, or they're lost.
defer provider.Shutdown(context.Background())

Already running an OpenTelemetry app? Point it at Neatlogs without touching code via the standard environment variables:

export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://ingest.neatlogs.com
export OTEL_EXPORTER_OTLP_TRACES_HEADERS=x-api-key=<your-project-key>

Retrieval semantic conventions

Both OTLP transports normalize the OpenTelemetry GenAI retrieval attributes to the canonical Neatlogs retriever namespace:

OpenTelemetry attributeNeatlogs attribute
gen_ai.retrieval.query.textneatlogs.retriever.query
gen_ai.retrieval.top_kneatlogs.retriever.top_k
gen_ai.retrieval.documentsneatlogs.retriever.documents.0, .1, …

Use neatlogs.retriever.* when setting Neatlogs attributes directly. The older neatlogs.retrieval.* spelling is accepted only as an ingestion compatibility alias and is normalized before storage.


Isolation & co-existing with other OpenTelemetry tracing

The Neatlogs SDKs keep their spans on a private tracer provider rather than the process-global one, so they can run alongside another OpenTelemetry setup (Datadog, Braintrust, a Collector, your own provider) without either side capturing, parenting, or shutting down the other's spans.

init() decides isolation automatically, and there are two triggers. A co-tenant LLM-observability tool — OpenLLMetry/Traceloop, Langfuse, OpenLIT, Arize Phoenix, Arize, Logfire — either (1) already owns or shares the global provider, or (2) is merely installed and importable, even if it hasn't loaded yet. The second case covers the common FastAPI pattern where an import-time neatlogs.init() runs before a startup handler loads the other tool; staying private leaves the global slot free for it.

Practical consequence: having one of those packages in your requirements is enough to flip Neatlogs onto a private provider. If you actually want Neatlogs to own the global provider — say, to capture your own raw-OTel spans — pass isolate=False. Override with two kwargs:

import neatlogs
from opentelemetry.sdk.trace import TracerProvider

# Force isolation on/off (default: auto-detect).
neatlogs.init(api_key="...", isolate=True)

# Full isolation into a provider YOU own but did NOT set as the OTel global.
# Neatlogs emits all its spans there, never shuts it down, and skips HTTP
# auto-instrumentation. Passing this implies isolation.
neatlogs.init(api_key="...", tracer_provider=TracerProvider())

Neatlogs always uses a private provider and never registers globally or adopts a foreign global provider. To route Neatlogs spans into a provider you own, pass it — the SDK adds its processors and flushes it, but never registers it globally or shuts it down:

import { init } from 'neatlogs';
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';

await init({
  apiKey: process.env.NEATLOGS_API_KEY,
  tracerProvider: new BasicTracerProvider(),
  // beforeExit/SIGTERM/SIGINT flush+shutdown — defaults true when Neatlogs owns
  // the provider, false when you pass tracerProvider (you own its lifecycle).
  registerShutdownHandlers: false,
});

neatlogs.Init always configures a private provider and never calls otel.SetTracerProvider / otel.SetTextMapPropagator, so isolation is automatic and needs no flag. A co-tenant tracer can neither export nor parent Neatlogs spans, and vice-versa. The trade-off: frameworks that resolve their tracer only from the global OTel API (e.g. Google ADK) are not auto-captured — instrument them explicitly. See the Go SDK.


Troubleshooting

Warning

Flush before your process exits. The exporter batches spans, so a short-lived script or a serverless handler can exit before the batch is sent — and those spans are silently dropped. Call provider.shutdown() (Python / TypeScript) or provider.Shutdown(ctx) (Go) before the process ends. Long-running services flush on their own interval; they only need this on graceful shutdown.

Warning

Export hangs or fails with DEADLINE_EXCEEDED? If the connection stalls before any span arrives, it's almost always client-side gRPC DNS resolution failing in a restricted network — a minimal container, a CI runner or behind a corporate proxy. Force the system resolver:

  • Python and other gRPC C-core exporters: set GRPC_DNS_RESOLVER=native.
  • Go: use the endpoint passthrough:///ingest.neatlogs.com:443 so the host is passed to the dialer verbatim.

See also

  • HTTP Trace Injection — a dependency-free JSON receiver, no OpenTelemetry required.
  • Go SDK — a native OTLP-based SDK that wraps the exporter setup above.
  • Span Kinds — the span taxonomy Neatlogs maps OTLP spans into.

On this page

Ask Neatlogs AI

Answers from the docs

How can I help?

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