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.
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
EU projects (OTLP/gRPC): use eu.ingest.neatlogs.com:443 with TLS and send your project key as x-api-key metadata.
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:
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>For an EU project, configure the gRPC exporter with:
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://eu.ingest.neatlogs.com
export OTEL_EXPORTER_OTLP_TRACES_HEADERS=x-api-key=<your-eu-project-key>Retrieval semantic conventions
Both OTLP transports normalize the OpenTelemetry GenAI retrieval attributes to the canonical Neatlogs retriever namespace:
| OpenTelemetry attribute | Neatlogs attribute |
|---|---|
gen_ai.retrieval.query.text | neatlogs.retriever.query |
gen_ai.retrieval.top_k | neatlogs.retriever.top_k |
gen_ai.retrieval.documents | neatlogs.retriever.documents |
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.
Neatlogs always uses a private provider and never registers, adopts, or shuts down the process-global provider. The deprecated isolate argument cannot disable this behavior. If you need Neatlogs to use a provider you own, pass a provider that you have not installed globally; Neatlogs adds its processors and flushes it, while provider shutdown remains your responsibility:
import neatlogs
from opentelemetry.sdk.trace import TracerProvider
# Use a caller-owned private provider. Do not install this provider globally.
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. Frameworks that resolve their tracer only from the global OTel API are not passively captured. Google ADK uses the explicit contrib/adk integration, which injects Neatlogs-owned model/tool callbacks and a run wrapper without changing global OTel state. See the Go SDK.
Troubleshooting
End spans, then drain the batch. Scripts and CLI tools should shut the
provider down before exit. Long-running services, workers, and stream
consumers should initialize once, stop accepting work during graceful
shutdown, then shut down the provider once. Serverless handlers should force
flush before returning and reserve shutdown for actual runtime teardown.
Tests should force flush before export assertions and shut down in suite
teardown. For streams, end spans on completion, error, or cancellation;
flushing cannot export a span that remains open. Use your language's standard
provider forceFlush/ForceFlush and shutdown/Shutdown methods.
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:443so 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.
