Skip to main content

Flue

Flue's OpenTelemetry adapter, @flue/opentelemetry, traces every agent run, model turn, tool call and subagent task in GenAI semantic conventions, with the Flue conversation id on each span as gen_ai.conversation.id.

New to Oodle?

Oodle is a managed observability platform for metrics, logs, traces and agent traces. It ingests OpenTelemetry natively, so the snippet on this page is the complete setup.

Each trace shows the full transcript, the token counts and cost of every call, the agent structure (runs, steps and tool calls) and Signals: labels for loops, rate limits, refusals and tool failures, detected as the trace arrives.

Sign up for free to get an instance ID and an API key, or see the Agent Observability overview first.

You will need:

  • OODLE_INSTANCE: your Oodle instance ID (ap1, us1)
  • OODLE_API_KEY: an Oodle API key (ap1, us1)
  • OTLP_ENDPOINT: your OTLP collector domain, shown on the tile

Open the Flue tile on the ap1, us1 page to get these filled in for you, or let an agent do the setup with /oodle-onboarding set up the llm_observability_flue integration.

Install​

Install Flue's OpenTelemetry adapter with the OTel SDK. Keep the versions: the newest OTLP exporter and the Node tracer provider ask for different @opentelemetry/api versions, so npm cannot install them together unpinned:

npm install @flue/opentelemetry @opentelemetry/[email protected] \
@opentelemetry/sdk-trace-node@2 @opentelemetry/resources@2 \
@opentelemetry/[email protected]

Instrument​

Configure the OTel SDK, then register Flue's instrumentation once, in a module app.ts imports first. Flue then traces every agent run, model turn, tool call and subagent task in GenAI semantic conventions, with the conversation id on each span:

// src/telemetry.ts
import { createOpenTelemetryInstrumentation } from '@flue/opentelemetry';
import { instrument, observe } from '@flue/runtime';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { BatchSpanProcessor, NodeTracerProvider } from '@opentelemetry/sdk-trace-node';

// NodeTracerProvider does not read OTEL_SERVICE_NAME, so name
// the service here or every trace lands under
// unknown_service:node.
const provider = new NodeTracerProvider({
resource: resourceFromAttributes({ 'service.name': 'my-llm-app' }),
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())],
});

// Makes spans your own tools start join the agent trace.
provider.register();

// Prompts, replies, tool arguments and results are sent by
// default. Pass `content: false` to send spans without them.
instrument(
createOpenTelemetryInstrumentation({
tracer: provider.getTracer('@flue/opentelemetry'),
}),
);

// The Flue server exits as soon as it stops, before a flush
// started on SIGTERM ends. Send each turn when it settles.
observe((event) => {
if (event.type === 'submission_settled') void provider.forceFlush();
});

// src/app.ts - import the telemetry module first.
import './telemetry.ts';
import { createAgentRouter } from '@flue/runtime/routing';
import { Hono } from 'hono';
import { Assistant } from './agents/assistant.ts';

const app = new Hono();
app.route('/agents/assistant', createAgentRouter(Assistant));

export default app;

Environment​

Point the application at Oodle:

# OTLP endpoint (points straight at Oodle)
export OTEL_EXPORTER_OTLP_ENDPOINT=https://<OTLP_ENDPOINT>

export OTEL_EXPORTER_OTLP_HEADERS="X-API-KEY=<OODLE_API_KEY>,X-OODLE-INSTANCE=<OODLE_INSTANCE>"

# Compress the export: prompt payloads are large
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

What each run becomes​

Flue activitySpan
A message to an agent, or a skillinvoke_agent <agent>, the root of the trace
A model turnchat <model>, with the messages, system instructions, tool definitions and token counts
A tool callexecute_tool <tool>, with its arguments and result
A subagent taskinvoke_agent <subagent>, under the agent that delegated it

Each message is one trace. Every turn of one Flue conversation carries the same gen_ai.conversation.id, so the Sessions view groups them. That id is the one Flue makes (conv_...), not the id in the agent URL: the URL id is on each span as flue.instance.id, so filter on that attribute to find a conversation by your own id. Only the chat spans carry token counts, so totals are not counted twice.

Under flue run​

flue run loads only the agent module and never app.ts, so a telemetry module that app.ts imports does not run there. To trace flue run, import the telemetry module from the agent module as well. vite dev and the built server load app.ts.

Restarts​

The Flue server exits as soon as it stops, so the snippet sends each turn when its submission settles rather than on SIGTERM. A turn that a restart interrupts sends no spans: they never end. With a database, Flue resumes that work on the next start.

Content​

The adapter sends prompts, replies, system instructions, tool arguments and results, and exception messages by default. Pass content: false to createOpenTelemetryInstrumentation() to send spans without them, or content: { transform } to redact them in code.

Verify​

Run your application, then open ap1, us1. Spans carry gen_ai.* attributes: the model, token counts, and the prompt and response content. Click a trace for the Transcript, the waterfall, and the cost breakdown.

Trace detail showing the Transcript tab with system, user, and assistant messages

If nothing arrives, check that the exporter can reach https://<OTLP_ENDPOINT> and that the instance and key are set: the OTLP gateway answers 401 without them.


Support

If you need assistance or have any questions, please reach out to us through: