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

# Customize Your Traces

> Add semantic conventions, structured inputs/outputs, custom attributes, events, exceptions, and prompt templates to your spans

Make traces useful for your app. Auto-instrumentation captures the basics — inputs, outputs, tokens, latency. But your app has context that matters: customer tier, A/B test variant, prompt template version, error details. This page covers all the ways to add it.

Start with the standard attribute names Arize AX expects:

# Semantic Conventions

[OpenInference Semantic Conventions](https://github.com/Arize-ai/openinference/blob/main/python/openinference-semantic-conventions/README.md) are the standardized attribute names that Arize AX uses to render your trace data correctly — model name, messages, token counts, span kinds, and more. When you use these attributes, your data shows up in the right places in the UI.

<Tabs>
  <Tab title="Python">
    Install the semantic conventions package:

    ```bash theme={null}
    pip install openinference-semantic-conventions
    ```

    Use `SpanAttributes` to set standardized attribute names on your spans:

    ```python theme={null}
    from openinference.semconv.trace import SpanAttributes, MessageAttributes

    span.set_attribute(SpanAttributes.OUTPUT_VALUE, response)

    span.set_attribute(
        f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_ROLE}",
        "assistant",
    )
    span.set_attribute(
        f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_CONTENT}",
        response,
    )
    ```
  </Tab>

  <Tab title="JS/TS">
    Install the semantic conventions package:

    ```bash theme={null}
    npm install --save @arizeai/openinference-semantic-conventions
    ```

    Use `SemanticConventions` to set standardized attributes:

    ```typescript theme={null}
    import {
      MimeType, OpenInferenceSpanKind, SemanticConventions,
    } from "@arizeai/openinference-semantic-conventions";

    tracer.startActiveSpan("chat chain", async (span) => {
       span.setAttributes({
          [SemanticConventions.OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.CHAIN,
          [SemanticConventions.INPUT_VALUE]: message,
          [SemanticConventions.INPUT_MIME_TYPE]: MimeType.TEXT,
          [SemanticConventions.METADATA]: JSON.stringify({
            "userId": req.query.userId,
          })
        });
        span.setStatus({ code: SpanStatusCode.OK });
        span.end();
    });
    ```
  </Tab>

  <Tab title="Java">
    In Java, use the attribute strings directly:

    ```java theme={null}
    singleAttrSpan.setAttribute("openinference.span.kind", "CHAIN");
    singleAttrSpan.setAttribute("input.value", input);
    singleAttrSpan.setAttribute("output.value", output);
    ```
  </Tab>

  <Tab title="Go">
    Install the [semantic-conventions package](https://github.com/Arize-ai/openinference/tree/main/go/openinference-semantic-conventions):

    ```bash theme={null}
    go get github.com/Arize-ai/openinference/go/openinference-semantic-conventions
    ```

    Set attributes using typed constants instead of raw strings:

    ```go theme={null}
    import (
        "go.opentelemetry.io/otel/attribute"

        semconv "github.com/Arize-ai/openinference/go/openinference-semantic-conventions"
    )

    span.SetAttributes(
        attribute.String(semconv.OpenInferenceSpanKind, semconv.SpanKindChain),
        attribute.String(semconv.InputValue, input),
        attribute.String(semconv.InputMimeType, semconv.MimeTypeText),
        attribute.String(semconv.Metadata, `{"userId": "abc-123"}`),
    )
    ```
  </Tab>
</Tabs>

Beyond attribute names, every span has built-in primitives for signaling outcome and marking moments during execution:

# Status, Events, and Exceptions

### Set Status

Signal whether a span succeeded or failed. Every span carries a status — `OK`, `ERROR`, or `UNSET`.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from opentelemetry import trace
    from opentelemetry.trace import Status, StatusCode

    current_span = trace.get_current_span()
    current_span.set_status(Status(StatusCode.OK))
    # or Status(StatusCode.ERROR) on failure
    ```
  </Tab>

  <Tab title="JS/TS">
    ```javascript theme={null}
    import { trace, SpanStatusCode } from "@opentelemetry/api";

    const currentSpan = trace.getActiveSpan();
    if (currentSpan) {
        currentSpan.setStatus({ code: SpanStatusCode.OK });
    }
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    import (
        "go.opentelemetry.io/otel/codes"
        "go.opentelemetry.io/otel/trace"
    )

    currentSpan := trace.SpanFromContext(ctx)
    currentSpan.SetStatus(codes.Ok, "")
    // or codes.Error with a message on failure
    ```
  </Tab>
</Tabs>

### Add Events

[Span Events](https://opentelemetry.io/docs/concepts/signals/traces/#span-events) are lightweight log messages attached to a span at a point in time.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from opentelemetry import trace

    current_span = trace.get_current_span()
    current_span.add_event("Doing something")

    current_span.add_event("some log", {
        "log.severity": "error",
        "log.message": "Data not found",
        "request.id": request_id,
    })
    ```
  </Tab>

  <Tab title="JS/TS">
    ```javascript theme={null}
    import { trace } from "@opentelemetry/api";

    const currentSpan = trace.getActiveSpan();
    if (currentSpan) {
        currentSpan.addEvent("Gonna try it!");
        currentSpan.addEvent("Did it!");
    }
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    singleAttrSpan.addEvent("Doing Something");
    singleAttrSpan.addEvent("Doing another thing");
    singleAttrSpan.end();
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    import (
        "go.opentelemetry.io/otel/attribute"
        "go.opentelemetry.io/otel/trace"
    )

    currentSpan := trace.SpanFromContext(ctx)
    currentSpan.AddEvent("Doing something")

    currentSpan.AddEvent("some log", trace.WithAttributes(
        attribute.String("log.severity", "error"),
        attribute.String("log.message", "Data not found"),
        attribute.String("request.id", requestID),
    ))
    ```
  </Tab>
</Tabs>

### Record Exceptions

Capture exception details and mark the span as failed in one flow:

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from opentelemetry import trace
    from opentelemetry.trace import Status, StatusCode

    current_span = trace.get_current_span()

    try:
        # something that might fail
        pass
    except Exception as ex:
        current_span.set_status(Status(StatusCode.ERROR))
        current_span.record_exception(ex)
    ```
  </Tab>

  <Tab title="JS/TS">
    ```javascript theme={null}
    import { trace, SpanStatusCode } from "@opentelemetry/api";

    const currentSpan = trace.getActiveSpan();

    try {
        // something that might fail
    } catch (error) {
        if (currentSpan) {
            currentSpan.setStatus({ code: SpanStatusCode.ERROR });
            currentSpan.recordException(error);
        }
    }
    ```
  </Tab>

  <Tab title="Go">
    Errors in Go are values, not exceptions — `RecordError` captures one and `SetStatus` marks the span as failed:

    ```go theme={null}
    import (
        "go.opentelemetry.io/otel/codes"
        "go.opentelemetry.io/otel/trace"
    )

    currentSpan := trace.SpanFromContext(ctx)

    if err := doSomething(); err != nil {
        currentSpan.RecordError(err)
        currentSpan.SetStatus(codes.Error, err.Error())
    }
    ```
  </Tab>
</Tabs>

With outcome primitives covered, the next layer is how inputs and outputs are captured with structure:

# Log Structured Inputs and Outputs

Set `input.value` / `output.value` for the table view, and `llm.input_messages` / `llm.output_messages` for structured chat messages:

```python theme={null}
from openinference.semconv.trace import MessageAttributes, SpanAttributes
from opentelemetry.trace import Span

def set_input_attrs(span: Span, messages: list) -> None:
    span.set_attribute(SpanAttributes.INPUT_VALUE, messages[-1].get("content", ""))
    for idx, msg in enumerate(messages):
        span.set_attribute(
            f"{SpanAttributes.LLM_INPUT_MESSAGES}.{idx}.{MessageAttributes.MESSAGE_ROLE}",
            msg["role"],
        )
        span.set_attribute(
            f"{SpanAttributes.LLM_INPUT_MESSAGES}.{idx}.{MessageAttributes.MESSAGE_CONTENT}",
            msg.get("content", ""),
        )

def set_output_attrs(span: Span, response_message: dict) -> None:
    span.set_attribute(SpanAttributes.OUTPUT_VALUE, response_message.get("content", ""))
    span.set_attribute(
        f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_ROLE}",
        response_message["role"],
    )
    span.set_attribute(
        f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_CONTENT}",
        response_message.get("content", ""),
    )
```

Semantic conventions and structured I/O cover standard LLM data. But your app has its own context that doesn't fit any standard attribute:

# Custom Attributes

Customer tier, environment, feature flags, A/B test variants — custom attributes let you attach this app-specific data to spans so you can filter, group, and analyze by it in Arize AX.

Best practice: vendor your attributes (e.g., `mycompany.`) so they don't clash with semantic conventions.

<Tabs>
  <Tab title="Python">
    Get the current span and set your custom attributes:

    ```python theme={null}
    from opentelemetry import trace

    current_span = trace.get_current_span()

    current_span.set_attribute("mycompany.customer_tier", "enterprise")
    current_span.set_attribute("mycompany.ab_variant", "v2")
    current_span.set_attribute("mycompany.feature_flag", "new-retrieval")
    ```
  </Tab>

  <Tab title="JS/TS">
    Set attributes when creating a span or on an active span:

    ```typescript theme={null}
    tracer.startActiveSpan(
      'app.new-span',
      { attributes: { 'mycompany.customer_tier': 'enterprise' } },
      (span) => {
        span.setAttribute('mycompany.ab_variant', 'v2');
        span.end();
      },
    );
    ```
  </Tab>

  <Tab title="Java">
    Set attributes directly on the span:

    ```java theme={null}
    singleAttrSpan.setAttribute("mycompany.customer_tier", "enterprise");
    singleAttrSpan.end();
    ```
  </Tab>

  <Tab title="Go">
    Get the current span and set attributes — `attribute.String` / `attribute.Int` / `attribute.Bool` cover most cases:

    ```go theme={null}
    import (
        "go.opentelemetry.io/otel/attribute"
        "go.opentelemetry.io/otel/trace"
    )

    currentSpan := trace.SpanFromContext(ctx)
    currentSpan.SetAttributes(
        attribute.String("mycompany.customer_tier", "enterprise"),
        attribute.String("mycompany.ab_variant", "v2"),
        attribute.String("mycompany.feature_flag", "new-retrieval"),
    )
    ```
  </Tab>
</Tabs>

**When to use a custom attribute vs. metadata:**

* **Custom attribute** — attached to a single span, each one a distinct filterable field in the UI. Use for values you filter or group by: customer tier, A/B variant, feature flag.
* **Metadata** (via `using_metadata` in Python, `setMetadata` in JS/TS, or `WithMetadata` in Go) — propagates to every child span in a context and is stored as a single JSON field. Use for request-wide context: request ID, experiment name, pipeline version.

# Propagate Attributes to All Child Spans

Set attributes once on OpenTelemetry Context, and [tracing integrations](/ax/integrations) will propagate them to all child spans automatically.

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install openinference-instrumentation
    ```

    ### `using_metadata`

    ```python theme={null}
    from openinference.instrumentation import using_metadata

    with using_metadata({"key-1": "value_1", "key-2": "value_2"}):
        # All child spans get: "metadata" = '{"key-1": "value_1", ...}'
        ...
    ```

    ### `using_tags`

    ```python theme={null}
    from openinference.instrumentation import using_tags

    with using_tags(["tag_1", "tag_2"]):
        # All child spans get: "tag.tags" = '["tag_1","tag_2"]'
        ...
    ```

    ### `using_attributes`

    Convenience — combines `using_session`, `using_user`, `using_metadata`, `using_tags`, and `using_prompt_template`:

    ```python theme={null}
    from openinference.instrumentation import using_attributes

    with using_attributes(
        session_id="my-session-id",
        user_id="my-user-id",
        metadata={"key-1": "value_1"},
        tags=["tag_1", "tag_2"],
        prompt_template="Please describe the weather forecast for {city} on {date}",
        prompt_template_version="v1.0",
        prompt_template_variables={"city": "Johannesburg", "date": "July 11"},
    ):
        ...
    ```

    ### `get_attributes_from_context`

    Read context attributes and attach them to manually created spans:

    ```python theme={null}
    from openinference.instrumentation import get_attributes_from_context

    span.set_attributes(dict(get_attributes_from_context()))
    ```
  </Tab>

  <Tab title="JS/TS">
    ```bash theme={null}
    npm install --save @arizeai/openinference-core @opentelemetry/api
    ```

    ### `setMetadata`

    ```typescript theme={null}
    import { context } from "@opentelemetry/api"
    import { setMetadata } from "@arizeai/openinference-core"

    context.with(
      setMetadata(context.active(), { key1: "value1", key2: "value2" }),
      () => { /* spans get: "metadata" = '{"key1": "value1", ...}' */ }
    )
    ```

    ### `setTags`

    ```typescript theme={null}
    import { context } from "@opentelemetry/api"
    import { setTags } from "@arizeai/openinference-core"

    context.with(
      setTags(context.active(), ["value1", "value2"]),
      () => { /* spans get: "tag.tags" = '["value1", "value2"]' */ }
    )
    ```

    ### `setAttributes`

    Combine with other setters:

    ```typescript theme={null}
    import { context } from "@opentelemetry/api"
    import { setAttributes, setSession } from "@arizeai/openinference-core"

    context.with(
      setAttributes(
        setSession(context.active(), { sessionId: "session-id"}),
        { myAttribute: "test" }
      ),
      () => { /* spans get both attributes */ }
    )
    ```

    ### `getAttributesFromContext`

    ```typescript theme={null}
    import { getAttributesFromContext } from "@arizeai/openinference-core";
    import { context, trace } from "@opentelemetry/api"

    const contextAttributes = getAttributesFromContext(context.active())
    const span = trace.getTracer("example").startSpan("example span")
    span.setAttributes(contextAttributes)
    span.end();
    ```
  </Tab>

  <Tab title="Go">
    ```bash theme={null}
    go get github.com/Arize-ai/openinference/go/openinference-instrumentation
    ```

    The helpers stash values on `context.Context` (via unexported keys, not OTel baggage), so they flow through your in-process call graph but never leak out as `baggage` HTTP headers on downstream requests. The per-provider instrumentors (`openinference-instrumentation-openai-go`, `openinference-instrumentation-anthropic-sdk-go`) read them back and apply them to every LLM span descended from the context, even when the call is several layers deep.

    ### `WithMetadata`

    `WithMetadata` takes a pre-serialized JSON string — Go has no native dict literal, so callers `json.Marshal` their own map. (Python `using_metadata` and JS `setMetadata` take dicts/objects because both languages have built-in JSON encoding for them.)

    ```go theme={null}
    import (
        "encoding/json"

        instrumentation "github.com/Arize-ai/openinference/go/openinference-instrumentation"
    )

    metadataJSON, err := json.Marshal(map[string]any{
        "key-1": "value_1",
        "key-2": "value_2",
    })
    if err != nil { /* ... */ }

    ctx = instrumentation.WithMetadata(ctx, string(metadataJSON))
    // All child LLM spans get: "metadata" = '{"key-1":"value_1", ...}'
    ```

    ### `WithTags`

    ```go theme={null}
    ctx = instrumentation.WithTags(ctx, "tag_1", "tag_2")
    // All child LLM spans get: "tag.tags" = ["tag_1", "tag_2"]  (OTel string-slice)
    ```

    ### `WithSession` / `WithUser`

    ```go theme={null}
    ctx = instrumentation.WithSession(ctx, "session-abc")
    ctx = instrumentation.WithUser(ctx, "user-xyz")
    // All child LLM spans get: "session.id" and "user.id"

    resp, _ := client.Chat.Completions.New(ctx, params)
    ```

    ### `ApplyContextAttributes`

    Read context attributes and attach them to manually created spans:

    ```go theme={null}
    ctx, span := tracer.Start(ctx, "my-op")
    defer span.End()
    instrumentation.ApplyContextAttributes(ctx, span)
    ```

    ### `WithSuppression`

    Exclude evaluator or grader calls from the customer's trace — descendant calls through an instrumented client emit no span:

    ```go theme={null}
    suppressedCtx := instrumentation.WithSuppression(ctx)
    _, _ = evalClient.Chat.Completions.New(suppressedCtx, params)
    ```

    <Note>
      Go has no combined setter analogous to Python's `using_attributes` — chain the individual `With*` helpers when you need to set multiple values. The order doesn't matter; each returns a derived context.
    </Note>
  </Tab>
</Tabs>

<Info>
  `using_tags` / `setTags` set `tag.tags` on spans. For project- or dataset-level tags (a separate platform feature), see [Tags](/ax/security-and-settings/tags).
</Info>

Prompt templates have their own dedicated propagation helper:

# Prompt Templates and Variables

Instrument prompt templates so you can experiment with changes in the [Prompt Playground](/ax/prompts/prompt-playground).

<Info>
  Recommended for LLM spans only.
</Info>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from openinference.instrumentation import using_prompt_template
    from openai import OpenAI

    client = OpenAI()
    prompt_template = "Please describe the best activity for me to do in {city} on {date}"
    prompt_template_variables = {"city": "Johannesburg", "date": "July 11"}

    with using_prompt_template(
        template=prompt_template,
        variables=prompt_template_variables,
        version="v1.0",
    ):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt_template.format(**prompt_template_variables)}],
        )
    ```
  </Tab>

  <Tab title="JS/TS">
    ```typescript theme={null}
    import { context } from "@opentelemetry/api"
    import { setPromptTemplate } from "@arizeai/openinference-core"

    context.with(
      setPromptTemplate(context.active(), {
        template: "Please describe the best activity for me to do in {{city}}",
        variables: { city: "Johannesburg" },
        version: "v1.0"
      }),
      () => { /* spans get prompt template attributes */ }
    )
    ```
  </Tab>

  <Tab title="Go">
    There's no context helper for prompt templates in Go — set the attributes directly on the LLM span using semantic-conventions constants:

    ```go theme={null}
    import (
        "encoding/json"

        "go.opentelemetry.io/otel/attribute"

        semconv "github.com/Arize-ai/openinference/go/openinference-semantic-conventions"
    )

    variables := map[string]string{"city": "Johannesburg", "date": "July 11"}
    varsJSON, err := json.Marshal(variables)
    if err != nil {
        span.RecordError(err)
    }

    span.SetAttributes(
        attribute.String(semconv.LLMPromptTemplate, "Please describe the best activity for me to do in {city} on {date}"),
        attribute.String(semconv.LLMPromptTemplateVersion, "v1.0"),
        attribute.String(semconv.LLMPromptTemplateVariables, string(varsJSON)),
    )
    ```
  </Tab>
</Tabs>

Prompt templates are set at span-creation time. For data that arrives later — review status, corrections, labels added after generation — patch the span after it's been ingested:

# Log Latent Metadata

Useful when your system enriches data after generation time — for example, adding review status, corrections, or labels that weren't available when the trace was created.

```python theme={null}
from arize import ArizeClient
import pandas as pd

client = ArizeClient(api_key="your-arize-api-key")

metadata_df = pd.DataFrame({
    "context.span_id": ["span1"],
    "patch_document": [{"status": "reviewed"}],
})

response = client.spans.update_metadata(
    space_id="your-arize-space-id",
    project_name="your-project-name",
    dataframe=metadata_df,
)
```

| Input Type               | Behavior                                              |
| :----------------------- | :---------------------------------------------------- |
| `string`, `int`, `float` | Fully supported                                       |
| `bool`                   | Converted to string (`"true"` / `"false"`)            |
| Objects / Arrays         | Serialized to JSON strings                            |
| `None` / `null`          | Stored as JSON `null` (does **not** remove the field) |

***

## Next step

Group multi-turn conversations together with sessions:

<Card title="Next: Set Up Sessions" icon="arrow-right" href="/ax/instrument/set-up-sessions" />
