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