OpenTelemetry
Point any OTLP/HTTP exporter at EvalGuard. Traces, metrics, and logs ingest — no agent install.
Ingest endpoints (per signal): https://evalguard.ai/api/v1/ingest/otlp/{traces,metrics,logs}
Already on LangSmith, Langfuse, Arize, or Phoenix? One-line migration.
Every one of those products is OTel-shaped under the hood. Re-point your OTEL_EXPORTER_OTLP_ENDPOINT at EvalGuard and the same instrumentation flows here. We accept openinference.* span attributes natively (the convention authored by Arize, adopted by Phoenix and Arize AX) — no payload transformation required.
# From
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.smith.langchain.com/otel/
# To (EvalGuard exposes per-signal OTLP/HTTP endpoints)
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://evalguard.ai/api/v1/ingest/otlp/traces
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20${EVALGUARD_API_KEY}Coexist mode: add EvalGuard as a SECOND exporter via the OpenTelemetry Collector (config below) so LangSmith / Langfuse keep receiving the same spans during evaluation. Cut the other exporter when you're ready.
Ingest endpoints
OTLP/HTTP/JSON is supported for all three signal types. Protobuf is on the 2026 roadmap — JSON works with the standard OpenTelemetry Collector and every first-party SDK.
| Signal | Path | Max payload |
|---|---|---|
| Traces | POST /api/v1/ingest/otlp/traces | 2 MB |
| Metrics | POST /api/v1/ingest/otlp/metrics | 2 MB |
| Logs | POST /api/v1/ingest/otlp/logs | 2 MB |
Authentication
Use your EvalGuard API key (starts with eg_). Projects are inferred from the key, or override per-request via x-project-id.
Authorization: Bearer eg_live_your_key_here
Content-Type: application/json
x-project-id: proj_optional_overrideOpenTelemetry Collector
Add EvalGuard as an OTLP/HTTP exporter. Your collector routes the same spans to EvalGuard and any existing backend (Jaeger, Tempo, etc.) in parallel.
exporters:
otlphttp/evalguard:
traces_endpoint: https://evalguard.ai/api/v1/ingest/otlp/traces
metrics_endpoint: https://evalguard.ai/api/v1/ingest/otlp/metrics
logs_endpoint: https://evalguard.ai/api/v1/ingest/otlp/logs
headers:
Authorization: "Bearer eg_live_your_key"
encoding: json
compression: gzip
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlphttp/evalguard]
metrics:
receivers: [otlp]
exporters: [otlphttp/evalguard]
logs:
receivers: [otlp]
exporters: [otlphttp/evalguard]Per-signal *_endpoint values are sent as-is — the otlphttp exporter does NOT append a path to them. That is why each points directly at the /api/v1/ingest/otlp/<signal> routes shown in the Ingest endpoints table above.
Node.js SDK
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: "https://evalguard.ai/api/v1/ingest/otlp/traces",
headers: { Authorization: `Bearer ${process.env.EVALGUARD_API_KEY}` },
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();Python SDK
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://evalguard.ai/api/v1/ingest/otlp/traces",
headers={"Authorization": f"Bearer {EVALGUARD_API_KEY}"},
)
)
)
trace.set_tracer_provider(provider)First-party SDK (@evalguard/otel-sdk)
Prefer a batteries-included wrapper over hand-wiring the collector? The @evalguard/otel-sdk package configures the exporter and auto-instruments your LLM SDK calls (OpenAI, Anthropic, LiteLLM, and Google) in one call.
npm install @evalguard/otel-sdk @opentelemetry/apiimport { initEvalGuard } from "@evalguard/otel-sdk";
const { shutdown, instrumentationReady } = initEvalGuard({
apiKey: process.env.EVALGUARD_API_KEY,
projectId: "my-project-id",
serviceName: "my-llm-app",
});
// Auto-instrumentation completes asynchronously (it loads each SDK the same way
// your app does). Clients built later are covered regardless; await this if your
// first LLM call happens immediately.
await instrumentationReady;
// LLM SDK calls are now auto-traced and exported to EvalGuard.
// On process exit — flush and close all providers:
await shutdown();Auto-instrumentation is on by default — pass enableLLMInstrumentation: false to opt out and use the individual instrumentOpenAI() / llmSpan() helpers instead. See the package README for the full option set.
LLM semantic conventions
EvalGuard indexes spans on the standard OTel GenAI attributes. Your model, prompt, completion, and token counts flow through without custom mapping.
- gen_ai.request.model — indexed as
model - gen_ai.usage.prompt_tokens / completion_tokens — aggregated into cost
- gen_ai.response.finish_reason — surfaced in the trace waterfall
- llm.model (legacy) — also accepted
- service.name (resource attr) — becomes the trace service column
Response format
Ingest returns an OTLP partial-success response so your collector can retry only the failed spans.
{
"partialSuccess": {
"rejectedSpans": 0,
"errorMessage": ""
}
}{
"partialSuccess": {
"rejectedSpans": 3,
"errorMessage": "Some spans could not be persisted"
}
}The async-ingest path returns 202 Accepted; 200 OK is only the synchronous DB-fallback path. The rejected-count field name differs per signal: rejectedSpans (traces), rejectedDataPoints (metrics), rejectedLogRecords (logs).
Quotas
- Traces are counted against your plan's monthly trace quota.
- A 429 means you hit the cap — the body is the OTLP partial-success shape
{ "partialSuccess": { "rejectedSpans": <n>, "errorMessage": "Monthly trace quota exceeded..." } }; detect it via the HTTP 429 status, not a JSONerror_code. Upgrade or wait for reset. - Payload over 2 MB → 413. Batch your exporter to stay under that.