> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselemma.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# From OpenTelemetry to Lemma

> Keep your existing OpenTelemetry instrumentation and add Lemma SDK tracing alongside it to produce the Lemma trace contract

If you already trace with OpenTelemetry, you do not have to tear anything out to adopt Lemma. The Lemma SDK is **additive**: it runs alongside your existing observability and delivers its own trace shape to Lemma. What it does *not* do is read your OTel spans and turn them into Lemma traces for you.

<Note>
  Adding the Lemma SDK next to OpenTelemetry is fully supported. OTel instrumentation **on its own** is not sufficient for Lemma, because it usually does not produce the [trace contract](/reference/trace-contract) Lemma reads.
</Note>

## Why existing instrumentation is not enough

Lemma is an opinionated sink. It reads a specific trace shape to power input/output display, model visibility, timing, tool visibility, threads, and automated issue detection. See [Overview](/tracing/overview) for how Lemma reads your traces.

Generic OpenTelemetry is a flexible observability layer. It will happily record spans, attributes, and timing, but it does not guarantee the things Lemma depends on:

* **One root trace per agent execution** with the current user input and the final output or error.
* **Typed children** — generations, tools, and generic spans, not undifferentiated spans.
* **Per-generation message history** — the full ordered message list sent to each LLM call.

Because those semantics are not guaranteed by your existing setup, the fix is to emit the Lemma trace contract explicitly with the Lemma SDK. Keep OTel for whatever you already use it for; add Lemma for the Lemma product experience.

## Concept mapping

The mental model transfers cleanly. What changes is that Lemma needs its children **typed** and its root **complete**. For the canonical field semantics and the client-vs-normalization mapping, see the [trace contract](/reference/trace-contract).

| OpenTelemetry concept                 | Lemma primitive                                                                 | What Lemma needs                                                            |
| ------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| OTel root span                        | Lemma [trace record](/reference/trace-contract#trace-record) (root)             | One agent execution = one trace, with `input` and final `output` or `error` |
| Generic child span                    | Lemma [child span](/reference/trace-contract#child-spans)                       | A typed entry in `spans[]`                                                  |
| LLM span                              | Lemma [generation](/tracing/instrumentation/generations) (`type: "generation"`) | `model` plus the full per-call message history                              |
| Tool span                             | Lemma [tool](/tracing/instrumentation/tool-calls) (`type: "tool"`)              | Tool `name`, arguments, and result or error                                 |
| Retrieval, ranking, or app-logic span | Lemma [span](/tracing/instrumentation/spans) (`type: "span"`)                   | `input` and `output` (or `error`)                                           |
| Parent / child span nesting           | `parent_id` nesting                                                             | Children under the step that caused them                                    |
| Span start / end timestamps           | `started_at` / `ended_at` / `duration_ms`                                       | Real measured bounds                                                        |

<Note>
  `type` is a discriminator. Setting a `model` on an untyped span does not make it a generation — you must set `type: "generation"`. The SDK typed helpers (`recordGeneration` / `recordTool` / `recordSpan`) do this for you.
</Note>

## What transfers and what does not

**Transfers** — the structural intuition you already have:

* Nesting: parent/child span trees map directly onto Lemma's `parent_id` nesting.
* Timing: start/end timestamps map onto Lemma's `started_at` / `ended_at` / `duration_ms`.
* One logical run: the idea that a single agent execution is one unit maps onto one Lemma trace.

**Does not transfer automatically** — Lemma needs these produced explicitly:

* A dedicated **Lemma root trace** per execution (`lemma.trace(...)`).
* **Typed** generations and tools, rather than generic spans.
* **Root input and output** (or error) on the trace record.
* **Per-generation message history** — the full ordered message list, not just the latest user string.

For the bad → better → best version of each of these, see [Building high-quality traces](/reference/building-high-quality-traces).

## Run Lemma side by side

Leave your OpenTelemetry instrumentation exactly where it is. Wrap the same agent execution in `lemma.trace()` and record the LLM calls and tools you already know about as typed Lemma children. This produces a conforming Lemma trace without disturbing your existing pipeline.

Install and configure the SDK first — see [Setup](/tracing/instrumentation/setup).

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Lemma } from "@uselemma/tracing";

    const lemma = new Lemma();

    // Your existing OpenTelemetry instrumentation stays in place.
    const answer = await lemma.trace(
      {
        name: "support-agent",
        input: userMessage,
        threadId,
        userId,
      },
      async (trace) => {
        const docs = await searchDocs(userMessage);
        trace.recordTool({
          name: "search_docs",
          input: { query: userMessage },
          output: docs,
        });

        const messages = [
          { role: "system", content: "You are a support agent." },
          ...priorMessages,
          { role: "user", content: userMessage },
          { role: "tool", content: JSON.stringify(docs) },
        ];

        const response = await callModel(messages);
        trace.recordGeneration({
          name: "answer",
          model: "gpt-4o",
          input: messages,
          output: response.text,
          llmInputMessages: messages,
        });

        return response.text;
      },
    );
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import json

    from uselemma_tracing import Lemma

    lemma = Lemma()

    # Your existing OpenTelemetry instrumentation stays in place.
    def run(trace):
        docs = search_docs(user_message)
        trace.record_tool(
            name="search_docs",
            input={"query": user_message},
            output=docs,
        )

        messages = [
            {"role": "system", "content": "You are a support agent."},
            *prior_messages,
            {"role": "user", "content": user_message},
            {"role": "tool", "content": json.dumps(docs)},
        ]

        response = call_model(messages)
        trace.record_generation(
            name="answer",
            model="gpt-4o",
            input=messages,
            output=response.text,
            llm_input_messages=messages,
        )

        return response.text

    answer = lemma.trace(
        "support-agent",
        run,
        input=user_message,
        thread_id=thread_id,
        user_id=user_id,
    )
    ```
  </Tab>
</Tabs>

Pass the **full ordered message list** for each call as both `input` and `llmInputMessages` / `llm_input_messages`. The root trace `input` is only the current user turn. See [Generations](/tracing/instrumentation/generations) and [Tool calls](/tracing/instrumentation/tool-calls) for live handles, errors, and nesting under a measured parent span.

If your run is coordinated across streaming callbacks or helpers rather than one function, use a TypeScript trace handle and end it from the terminal callback. See [Traces](/tracing/instrumentation/traces).

## Keep your OpenTelemetry setup

The Lemma SDK delivers its trace JSON over its own HTTP path; it does not register as an exporter or span processor in your OpenTelemetry pipeline. That means your existing OTel exporters can stay untouched — the two run independently and neither depends on the other.

The one place the two pipelines meet today is the **Vercel AI SDK**, which is built on OpenTelemetry. There you add `vercelAI()` to the AI SDK's telemetry `integrations` alongside any exporters you already use, and it produces the Lemma trace from AI SDK lifecycle events. See [Vercel AI SDK](/integrations/vercel-ai).

For frameworks with a first-class Lemma integration, prefer the integration over hand-rolling the mapping:

* [OpenAI Agents SDK](/integrations/openai-agents)
* [Vercel AI SDK](/integrations/vercel-ai)
* [LangChain](/integrations/langchain)
* [LangGraph](/integrations/langgraph)

Outside those framework integrations, treat OTel and Lemma as separate additive layers: keep OTel for your existing telemetry and use the Lemma SDK directly for the Lemma trace contract, as shown above.

## Pitfalls

<Warning>
  Record complete inputs and outputs only when safe. Redact secrets, credentials, and sensitive user data before tracing them.
</Warning>

| Pitfall                                   | Fix                                                                                                                                            |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Assuming OTel spans already satisfy Lemma | They usually do not carry the Lemma contract. Emit it explicitly with the Lemma SDK.                                                           |
| No Lemma root trace                       | Wrap each agent execution in `lemma.trace()` so children have one root to attach to.                                                           |
| Untyped children                          | Record LLM calls with `recordGeneration` and tools with `recordTool` so they render and filter as generations and tools.                       |
| Missing message history                   | Put the full ordered message list on each generation's `input` and `llmInputMessages` / `llm_input_messages`, not just the latest user string. |

## Related pages

<CardGroup cols={2}>
  <Card title="Setup" icon="wrench" href="/tracing/instrumentation/setup">
    Install the SDK and point it at Lemma.
  </Card>

  <Card title="Trace contract" icon="file-check" href="/reference/trace-contract">
    The exact shape and fields Lemma reads.
  </Card>

  <Card title="Building high-quality traces" icon="circle-check" href="/reference/building-high-quality-traces">
    Bad → better → best examples for the ideal path.
  </Card>

  <Card title="Integrations" icon="plug-zap" href="/integrations/vercel-ai">
    Framework integrations that produce the contract for you.
  </Card>
</CardGroup>
