# OpenTelemetry

> Send existing OTLP traces, logs, and metrics to Anectico without replacing your instrumentation.

Canonical page: https://anectico.com/docs/instrument/opentelemetry/


Anectico accepts OpenTelemetry Protocol data. Keep your current instrumentation and change the exporter
destination and authentication header.

## Endpoint and authentication

Use the Anectico ingestion host:

```text
OTLP HTTP base: https://api.anectico.com
Header:         X-Anectico-API-Key=an_...
```

Signal paths follow the OTLP HTTP convention:

```text
/v1/traces
/v1/logs
/v1/metrics
```

Example environment configuration:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.anectico.com"
export OTEL_EXPORTER_OTLP_HEADERS="X-Anectico-API-Key=an_..."
export OTEL_SERVICE_NAME="orders-api"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"
```

Use an API key containing only `ingest:write` for collectors and application exporters.

### OTLP over gRPC

**OTLP/HTTP is the transport to use. The public endpoint does not currently accept OTLP over
gRPC.** Set `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` — the default for the settings above — and
point your exporter at `https://api.anectico.com` as shown.

Some SDKs and the OpenTelemetry Collector default to gRPC, so an exporter left on its own default
will fail to connect rather than report an authentication or validation error. If telemetry is not
arriving and your collector logs a connection failure rather than a rejection, check the protocol
setting first.

The two transports are otherwise equivalent: the same key, the same `ingest:write` permission, the
same project, and the same data. A credential without that permission is refused before anything is
stored, and the refusal is permanent — an exporter will not retry it.

## Add Anectico features to an existing Python SDK

If your Python application already owns OpenTelemetry resources, sampling,
providers, processors, exporters, instrumentation, and lifecycle, do not start
a second telemetry pipeline. Keep those application providers, point their OTLP
exporters or Collector at Anectico, and add Anectico's identity-only processors before
registering the providers. The snippet is a composition template: the three `*_provider` and two
`existing_*_export_processor` names stand for objects your application already creates.

```python
import os

from opentelemetry import _logs as otel_logs
from opentelemetry import metrics, trace
import anectico

# These are the providers and export processors from your existing OTel setup.
tracer_provider.add_span_processor(anectico.BaggageIdentitySpanProcessor())
tracer_provider.add_span_processor(existing_span_export_processor)
logger_provider.add_log_record_processor(
    anectico.BaggageIdentityLogRecordProcessor()
)
logger_provider.add_log_record_processor(existing_log_export_processor)

trace.set_tracer_provider(tracer_provider)
metrics.set_meter_provider(meter_provider)
otel_logs.set_logger_provider(logger_provider)

client = anectico.AnecticoClient(
    api_key=os.environ["ANECTICO_API_KEY"],
    service_name="marketplace-api",
    open_telemetry_mode="existing",
)
client.start()
```

`ANECTICO_OTEL_MODE=existing` is equivalent. Register the identity processors before
application work emits spans or logs, and register the application providers
before starting Anectico. The processors only stamp `anectico.distinct_id`; they do not
export, register globals, or flush/shut down anything. Do not add person
identity to metric labels.

Existing mode creates no providers, exporters, export processors, metric
readers, propagator, `requests`/`httpx` instrumentation, or standard-library
logging bridge. Anectico's manual spans, captured errors, AI/tool/agent helpers,
metrics, and `log_event()` use the registered global APIs. Without a real
application Logger Provider, `log_event()` is a safe no-op. Ordinary Python
logging continues through the application's existing logging integration only.

The application owns sampling and export decisions. Anectico's managed resource,
sampler, endpoint, batching, exporter, propagation, and logging-bridge settings
do not change the existing pipeline; Anectico signal enable flags still gate helper
emission. `client.flush()` returns `False`, and `client.stop()` returns
`attempted=False`, `success=None`, because neither call may flush or shut down
application providers. After the final helper call, stop Anectico and use the
application's existing provider lifecycle hook to flush and shut down.

To roll back the Anectico feature layer, remove the client startup and the two
identity processors. The original application-owned OTel pipeline stays in
place. Restore its prior exporter/Collector destination and authentication
separately if the rollback must also stop OTLP delivery to Anectico.

## Add Anectico features to an existing Node SDK

If your application already starts `NodeSDK` or registers its own OpenTelemetry providers, keep that
code as the single owner of providers, processors, instrumentation, exporters, flush, and shutdown.
Start it first, point its OTLP exporters at Anectico, and initialize Anectico in existing-provider
mode. Adapt the exporter and processor placeholders below to the objects already in your setup:

```typescript
import { context, trace } from '@opentelemetry/api';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
  BaggageLogRecordProcessor,
  BaggageSpanProcessor,
  contextWithDistinctId,
  initNode,
} from '@anectico/sdk/node';

const otel = new NodeSDK({
  // Keep your existing resource, instrumentations, and Anectico OTLP exporters.
  // Add these identity-only processors before the application SDK starts.
  spanProcessors: [
    new BaggageSpanProcessor(),
    // ...your existing export processors
  ],
  logRecordProcessors: [
    new BaggageLogRecordProcessor(),
    // ...your existing export processors
  ],
});
otel.start();

const anectico = await initNode({
  apiKey: process.env.ANECTICO_API_KEY,
  serviceName: 'orders-api',
  openTelemetryMode: 'existing',
});

const requestContext = contextWithDistinctId(context.active(), 'customer_8842');
await context.with(requestContext, async () => {
  await trace.getTracer('orders-api').startActiveSpan('orders.create', async span => {
    // Existing application work.
    span.end();
  });
});
```

`openTelemetryMode: 'existing'` (or `ANECTICO_OTEL_MODE=existing`) does not create providers, exporters,
propagators, or auto-instrumentations and never shuts down application-owned providers. Anectico's
manual spans, errors, metrics, and logs use the globals your application registered. A signal needs
an existing provider to export: for example, `logEvent()` requires your application to have
registered an OpenTelemetry Logger Provider.

Add `BaggageSpanProcessor` and `BaggageLogRecordProcessor` to the application-owned providers before
they start when request-scoped `contextWithDistinctId()` or W3C baggage should become the
`anectico.distinct_id` attribute stored on spans and logs. These processors add identity attributes
only; they do not export, register globals, or own provider lifecycle. Metrics should remain
service-scoped and must not carry customer identity labels.

Keep lifecycle ownership in the application. On graceful or fatal shutdown, finish active work and
flush or shut down `NodeSDK` before calling `anectico.stop()` to finish Anectico identity requests and remove
its process handlers. `anectico.flush()` cannot flush providers it does not own. Do not combine the
default `managed` mode with an already-started OpenTelemetry SDK, and do not use the Anectico preload in
existing-provider mode. Removing the Anectico initialization later leaves the original OTLP-only
pipeline unchanged.

## Attach customer identity to request evidence

OTLP data without customer identity still appears in service, trace, log, and metric views. To add
traces and logs to a customer timeline, attach the stable ID as:

```text
anectico.distinct_id=user_8842
```

Use one scalar string, boolean, or number; strings are recommended. Arrays and maps are ignored as
identity values because their rendered form is not a stable customer identifier.

For distributed requests, propagate the same value through trusted W3C baggage. Do not accept a
customer identity directly from an unauthenticated public request. Do not copy customer identity
onto metric labels: identifiers create high-cardinality series, and metrics are normally
service-scoped evidence rather than one-customer timeline entries.

## Resource fields that matter

Set these consistently:

| Field | Purpose |
| --- | --- |
| `service.name` | Groups telemetry by application or service |
| `service.version` | Connects failures to a release |
| `deployment.environment` | Separates production, staging, and development |
| `anectico.distinct_id` | Connects request evidence to a customer; do not use it as a metric label |

## Agent and LLM framework compatibility

Anectico reads agent and model telemetry from the conventions below without any Anectico-specific
wrapper. Send the spans your framework already produces. Every entry is covered by a compatibility
test that replays a span in that framework's own attribute shape and checks the values Anectico
computes from it, so a change that stopped reading one of these is a build failure rather than a
quiet blank column.

| What you run | Attributes it writes | What Anectico reads from it |
| --- | --- | --- |
| Current OpenTelemetry GenAI conventions | `gen_ai.provider.name`, `gen_ai.usage.input_tokens`, … | Provider, requested and served model, operation, all five token counts, agent and workflow identity, conversation, response id, finish reason |
| Superseded OpenTelemetry GenAI conventions | `gen_ai.system`, `gen_ai.usage.prompt_tokens`, … | The same fields, read from the older key names |
| OpenInference | `openinference.span.kind`, `llm.*` | Provider, model, token detail including cache and reasoning, agent name, session, tool name and call id, cost hint |
| OpenAI Agents SDK | Either convention, depending on the instrumentation package | Provider, model, token detail; agent spans keep their agent name |
| OpenAI Responses API | `gen_ai.operation.name=fetch_response` | Model, response id, finish reason and the response's own terminal status, with no token counts — that operation does not report them |
| Anthropic Messages API | Current conventions | Provider, model, both prompt-cache counters, reasoning tokens, conversation, whether the call was streamed |
| Claude Agent SDK (Claude Code tracing) | `gen_ai.system` plus the SDK's own names | Provider, model, response id, stop reason |
| Google ADK and Gemini | `gen_ai.system`, agent and invocation attributes | Provider, model, tokens, agent name, session; the agent and model spans resolve to one agent |
| LangGraph | OpenInference plus a thread id | Session identity from the thread id |
| MCP clients | `mcp.method.name`, `mcp.session.id`, `mcp.protocol.version` | Tool name, MCP session and protocol version, filed as an MCP step rather than a plain tool call |
| MCP servers | `mcp.method.name`, `mcp.protocol.version` | The MCP step and its protocol version |
| Strands Agents | Current conventions, framework name as the provider | Provider, model, input and output tokens, operation |
| Amazon Bedrock AgentCore | The framework's spans, plus a session id the runtime injects | Everything the framework writes, plus session identity and agent name |
| Microsoft Agent Framework | `gen_ai.provider.name`, agent id and name | Provider, operation, agent id and name |
| AutoGen | `gen_ai.system`, agent id and name | Operation, agent id and name; the provider reads as the framework — see below |

### Where a framework's own choices limit what appears

These are not defects to report; they are places where a framework writes something the conventions
do not define, and Anectico declines to guess at it. Each has a one-line fix you can apply in your
own instrumentation.

- **Claude Agent SDK token counts do not appear.** The SDK writes `input_tokens`, `output_tokens`,
  `cache_read_tokens` and `cache_creation_tokens` rather than the `gen_ai.usage.*` names. Anectico
  does not read unnamespaced attributes, because doing so would capture unrelated fields from every
  other application sending data. Add the `gen_ai.usage.*` attributes alongside them to get token and
  cost figures for those calls.
- **AutoGen reports the framework as the provider.** AutoGen sets the provider attribute to
  `autogen` rather than to the model vendor, and its own tracing records no token usage, so its spans
  carry no cost. Instrument the model client as well to get provider, model and spend.
- **An MCP server that names its tool only as `mcp.tool.name` records no tool name.** The call is
  still recorded as an MCP step with its protocol version. Set `gen_ai.tool.name` as well to get the
  tool named.
- **Strands prompt-cache counters do not appear.** Strands spells them
  `gen_ai.usage.cache_read_input_tokens` and `cache_write_input_tokens`, which are not the convention's
  names. Input and output tokens are unaffected.
- **Google ADK records no agent id.** Its agent identity is the agent name plus the service name,
  which is what Anectico groups on for that framework.
- **AgentCore's own runtime, gateway and memory spans are not agent steps.** They carry AWS
  attributes rather than `gen_ai.*` ones and appear as ordinary spans. The agent framework running
  inside the runtime is what produces agent telemetry.

## Verify

Send one span, one log, and one recognizable metric, then check **Traces**, **Logs**, and **Metrics**
in the same project and environment. Allow for the metric export interval and shut down the Meter
Provider cleanly in a short-lived test. If you supplied `anectico.distinct_id` on the request evidence,
also search **Customers** for that value.

- [Verify your setup](/docs/start/verify-setup)
- [Record application metrics](/docs/instrument/metrics)
- [No data is appearing](/docs/help/no-data)
