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

EU projects (OTLP/gRPC): use eu.ingest.neatlogs.com:443 with TLS and send your project key as x-api-key metadata.

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>

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 attributeNeatlogs attribute
gen_ai.retrieval.query.textneatlogs.retriever.query
gen_ai.retrieval.top_kneatlogs.retriever.top_k
gen_ai.retrieval.documentsneatlogs.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

Warning

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.

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.