Skip to Content
RiusInteroperabilityVanilla OTel SDKs

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=none

The 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-http

Then 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/api
import { 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/otlptracehttp
package 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

Last updated on