Skip to main content

Send OTel traces to AcruxCore with the SDK helper

What you'll do: wire the SDK's register() helper into a few different OpenTelemetry (OTel) setups — a bare pipeline, one auto-instrumented framework, several frameworks at once, and a multi-turn session — so traces reach AcruxCore's OTLP endpoint with no hand-written OTel plumbing. Both published SDKs ship this helper — acruxcore.otel.register() in Python, register() from @acruxcoreai/sdk/otel in Node — pick a language tab wherever the code differs.

AcruxCore accepts traces from any OTel source at POST /api/v1/traces/otlp — that part isn't new, and register() doesn't change what the endpoint accepts. It collapses the TracerProvider + BatchSpanProcessor + OTLPSpanExporter wiring every OTLP integration needs into one call, and optionally turns on a named framework's own OpenInference instrumentor against the result. If you'd rather not add the SDK as a dependency, the OTLP API reference shows the same four lines written out by hand — both reach the identical endpoint.

pip install 'acruxcore[otel]'
export ACRUXCORE_API_KEY=<your-acruxcore-key>
export ACRUXCORE_BASE_URL=https://api.acruxcore.com/api/v1

register() reads those two variables by default — pass apiKey/baseUrl (Python: api_key/base_url) instead if you'd rather not use env vars.

1. A bare pipeline, no framework

The plainest case: no instrumentor, just the pipeline, and a span you create and name yourself. Useful for a step no framework instrumentor covers — a retrieval call, a custom pre-processing stage:

from acruxcore.otel import register
from opentelemetry import trace

tracer_provider = register(service_name="my-script")
tracer = trace.get_tracer("my-script")

with tracer.start_as_current_span("hello-otel") as span:
span.set_attribute("input.value", "hello from a hand-rolled span")
...

tracer_provider.force_flush()

That lands in AcruxCore exactly as written — no model, no cost, because there was none:

{
"kind": "other",
"name": "hello-otel",
"status": "unset",
"durationMs": 100,
"payload": { "input": "hello from a hand-rolled span", "output": null }
}

A span with no OpenInference span-kind attribute lands as "other" — set openinference.span.kind yourself ("CHAIN", "RETRIEVER", ...) if you want a more specific kind than that.

2. Auto-instrument one framework

Pass instrument=[...] (Node: instrument: [...]) and register() turns on that framework's own OpenInference instrumentor against the provider it just built — nothing else in your code needs to change:

from openai import OpenAI
from acruxcore.otel import register

tracer_provider = register(service_name="my-script", instrument=["openai"])

client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Say hi in exactly two words."}],
)
print(response.choices[0].message.content)

tracer_provider.force_flush()

The resulting span carries the real model, provider, token counts, and cost — openinference-instrumentation-openai reads them straight off the OpenAI response:

{
"kind": "llm",
"name": "ChatCompletion",
"model": "gpt-4o-mini-2024-07-18",
"provider": "openai",
"promptTokens": 14,
"completionTokens": 3,
"costUsd": 0.0000039
}

instrument=[...] only instruments an already-installed package — it never installs anything for you. Passing a name whose package isn't installed raises AcruxCoreError naming the exact package to install; passing a name outside the supported list raises a different error listing the ones that are:

from acruxcore.otel import SUPPORTED_FRAMEWORKS

print(SUPPORTED_FRAMEWORKS)
# ('crewai', 'langchain', 'llama_index', 'openai', 'openai_agents')

3. Auto-instrument several frameworks at once

instrument takes a list because some frameworks need more than one instrumentor to see the whole picture. CrewAI is the clearest example: it orchestrates agents and tools itself, but calls the model through the plain openai Python SDK by default (the litellm integration is an opt-in extra most installs don't have) — so "crewai" alone sees the orchestration but not token counts or cost:

tracer_provider = register(
service_name="my-crew",
instrument=["crewai", "openai"],
)

This is exactly what the CrewAI trip-planner tutorial does, end to end, with a real web-search tool and a real multi-agent crew.

Node's shorter SUPPORTED_FRAMEWORKS (see section 2) has no framework needing this combination yet — the OpenAI Agents SDK tutorial's Node version gets full model, token, and cost data from instrument: ['openai_agents'] alone.

4. Group multiple calls into one session

openinference.instrumentation.using_session() sets a value on the current OTel context that OpenInference instrumentors read and stamp onto every span they create — so it works for free once you've auto-instrumented a framework:

from openinference.instrumentation import using_session

with using_session("customer-42-conversation"):
first = client.chat.completions.create(model="gpt-4o-mini", messages=[...])
second = client.chat.completions.create(model="gpt-4o-mini", messages=[...])

Both calls above land in AcruxCore as separate traces sharing one session.id — open Observability → Sessions and both show up grouped, the same way the OpenAI Agents SDK tutorial groups its two-turn conversation.

Session grouping only affects spans an OpenInference instrumentor creates — a bare hand-rolled span like the one in section 1 doesn't go through an instrumentor, so it won't pick up the context value. Set the attribute yourself instead:

with tracer.start_as_current_span("first-call") as span:
span.set_attribute("session.id", "customer-42-conversation")

5. A framework not in SUPPORTED_FRAMEWORKS

If a framework you use ships an OpenInference or plain-OTel instrumentor that isn't in the registry, skip instrument=[...] and wire it yourself against the provider register() already built:

from some_other_package import SomeInstrumentor

tracer_provider = register(service_name="my-script")
SomeInstrumentor().instrument(tracer_provider=tracer_provider)

This is the same object either path returns — instrument=[...] is a shortcut for the five frameworks in SUPPORTED_FRAMEWORKS, not a different mechanism.

What's next