Vanilla OpenTelemetry SDKs
Any language with an OpenTelemetry SDK can send traces to Rius today: the ingest endpoint is standard OTLP/HTTP. This is the path for languages without a native SDK (everything except Python and TypeScript).
Configuration by environment variables
Every OTel SDK reads the standard exporter variables when it builds the exporter from its defaults, so this configuration is the same in Go, Node.js, Java, Rust, .NET, and Python:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.eu.console.rius-glassflow.com"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer <your API key>"
export OTEL_SERVICE_NAME="my-agent"
# Keep long LLM spans whole; the default of 128 attributes is too low
export OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=4096
# Rius ingests traces only. Unless this process also exports metrics or logs
# to another backend, turn those exporters off
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=noneThe variables take effect through the SDK, not on their own. Set up a tracer
provider with an OTLP exporter that has no endpoint or headers set in code (in
Java, through SDK autoconfiguration with AutoConfiguredOpenTelemetrySdk), or,
in Python, Node.js, Java or .NET, run the zero-code agent. The protocol
variable is read by the zero-code agents, Java autoconfiguration and .NET
(which otherwise defaults to gRPC). Elsewhere the exporter package you import
decides the protocol. In Python, for example, install the SDK and the
http/protobuf exporter:
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-httpThen register the provider. Importing the exporter from
opentelemetry.exporter.otlp.proto.http is what selects http/protobuf:
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
# With no arguments, the exporter reads the OTEL_EXPORTER_OTLP_* variables
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)In Node.js, NodeSDK with the protobuf exporter does the same. Turn off its
metrics and logs exporters, which it otherwise starts against the same
endpoint, and shut it down before the process exits, because queued spans are
not sent otherwise:
npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-proto @opentelemetry/apiimport { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
const sdk = new NodeSDK({
// No url or headers in code, so the exporter reads OTEL_EXPORTER_OTLP_*
traceExporter: new OTLPTraceExporter(),
// Rius ingests traces only
metricReaders: [],
logRecordProcessors: [],
})
sdk.start()
// ... your agent code ...
// Flushes queued spans before the process exits (top-level await needs an ES module)
await sdk.shutdown()In Go, register a provider with the HTTP trace exporter and shut it down
before main returns:
go get go.opentelemetry.io/otel go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttppackage main
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func main() {
ctx := context.Background()
// No endpoint or headers in code, so the exporter reads OTEL_EXPORTER_OTLP_*
exporter, err := otlptracehttp.New(ctx)
if err != nil {
log.Fatal(err)
}
provider := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
defer provider.Shutdown(ctx) // flushes queued spans before the process exits
otel.SetTracerProvider(provider)
// ... your agent code ...
}http/protobuf is the encoding we recommend. The endpoint accepts OTLP/HTTP
in protobuf or JSON, and no gRPC (see the
contract), so where
the protocol variable applies, set it to http/protobuf. In Node,
@opentelemetry/exporter-trace-otlp-http sends JSON instead of protobuf,
which also works.
The attribute limit matters as soon as a span carries messages or tool definitions as one attribute per field; see Attribute count limit.
Emitting a well-formed LLM span
The attributes decide what analytics you get. A minimal JS/TS example with everything the platform reads:
import { trace } from '@opentelemetry/api'
const tracer = trace.getTracer('my-agent')
const span = tracer.startSpan('chat gpt-4o')
span.setAttributes({
'openinference.span.kind': 'LLM',
'gen_ai.operation.name': 'chat',
'gen_ai.request.model': 'gpt-4o',
'gen_ai.provider.name': 'openai',
})
const response = await callTheModel()
span.setAttributes({
'gen_ai.usage.input_tokens': response.usage.prompt_tokens,
'gen_ai.usage.output_tokens': response.usage.completion_tokens,
'gen_ai.response.model': response.model,
'gen_ai.output.messages': JSON.stringify([
{ role: 'assistant', parts: [{ type: 'text', content: response.text }] },
]),
})
span.end()Model, tokens, and messages make the span a full generation: it gets cost computed server-side and appears in model analytics. The complete attribute tables, including the message shape and the streaming time to first chunk, are in the span attribute reference.
Nest spans the normal OpenTelemetry way (parent context) and they arrive as one trace. Wrap each agent run in a single root span; that root’s name, duration, and status are what run-level views show.
What you give up versus a native SDK
Raw OTel emission is fully supported, permanently. The native SDKs add conveniences on top of it, not a different protocol: message normalization, mapping third-party attribute dialects onto the canonical keys, a raised attribute limit, idempotent first-token capture, export-time privacy controls, and the auto-instrumentation integrations. Code written against raw OTel keeps working unchanged if you later adopt a native SDK.
Next steps
- Span attribute reference: every attribute the platform reads, and the GenAI message shape.
- OpenTelemetry Collector: put a collector between your services and the endpoint.