Enriching Spans with Attributes, Metadata, and Tags

Capture additional context beyond what standard frameworks provide by enriching your traces with custom attributes, metadata, tags, session IDs, user IDs, and prompt templates.

What it is

Enriching Spans lets you attach custom key/value pairs and structured context to your OpenTelemetry spans so they carry the full picture of what’s happening in your application. You can add raw attributes with set_attribute(), use traceAI Semantic Convention constants for structured LLM data, or use context managers (using_metadata, using_tags, using_session, using_user, using_prompt_template) to propagate attributes automatically to all child spans — without modifying every instrumented function.

Use cases

  • Custom attributes — Add business-specific key/value pairs to spans for filtering and debugging in the Future AGI dashboard.
  • Semantic conventions — Use traceAI constants like OUTPUT_VALUE and LLM_OUTPUT_MESSAGES to capture LLM outputs in a structured, queryable schema.
  • Metadata enrichment — Attach experiment metadata, feature flags, or A/B test identifiers to all spans in a code block.
  • Session and user tracking — Associate spans with a session ID and user ID for session replay and per-user analytics.
  • Prompt template tracking — Record which prompt template, version, and variables were used in each LLM call.

How to

Add attributes to a span

Attributes are key/value pairs attached directly to the active span. Prefix custom attributes with your company name to avoid conflicts with semantic conventions.

from opentelemetry import trace

current_span = trace.get_current_span()

current_span.set_attribute("operation.value", 1)
current_span.set_attribute("operation.name", "Saying hello!")
current_span.set_attribute("operation.other-stuff", [1, 2])
import { trace, context } from "@opentelemetry/api";

const currentSpan = trace.getSpan(context.active());

if (currentSpan) {
    currentSpan.setAttribute("mycompany.operation.value", 1);
    currentSpan.setAttribute("mycompany.operation.name", "Saying hello!");
    currentSpan.setAttribute("mycompany.operation.other-stuff", [1, 2]);
}

Use Semantic Convention Attributes

traceAI Semantic Conventions provide structured attribute names for common LLM data. Install the instrumentation package first.

pip install fi-instrumentation-otel
npm install @traceai/fi-core @opentelemetry/api

Then set semantic attributes on the current span:

from opentelemetry import trace # Assuming span is current_span or obtained otherwise
from fi_instrumentation.fi_types import SpanAttributes, MessageAttributes # Assuming these constants and 'response' are defined

span = trace.get_current_span() # Example: get current span

if span.is_recording(): # Check if span is recording before setting attributes
    span.set_attribute(SpanAttributes.OUTPUT_VALUE, response)

    # This shows up under `output_messages` tab on the span page
    span.set_attribute(
        f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_ROLE}",
        "user",
    )
    span.set_attribute(
        f"{SpanAttributes.LLM_OUTPUT_MESSAGES}.0.{MessageAttributes.MESSAGE_CONTENT}",
        response,
    )
import { trace, context } from "@opentelemetry/api";

// Assume 'response' variable is defined, e.g.:
// const response: string = "Some LLM response from Typescript";
// String keys below should match traceAI's expected semantic conventions for Typescript.

const span = trace.getSpan(context.active());

if (span) {
    span.setAttribute("output.value", response);
    span.setAttribute("llm_output_messages.0.message_role", "user");
    span.setAttribute("llm_output_messages.0.message_content", response);
}

Use context helpers

Choose the helper that matches what you want to attach, then pick your instrumentation style.

Enrich the current OpenTelemetry context with metadata. All spans created within the block will carry the metadata as a JSON-serialized attribute.

from fi_instrumentation import using_metadata
# Assuming value_1, value_2 are defined
# value_1 = "some data"; value_2 = 123
metadata = {
    "key-1": value_1,
    "key-2": value_2,
}
with using_metadata(metadata):
    # Calls within this block will generate spans with the attributes:
    # "metadata" = "{"key-1": value_1, "key-2": value_2, ... }" # JSON serialized
    pass # Your code here
import { context, propagation } from "@opentelemetry/api";

// Assuming value_1, value_2 are defined
// const value_1 = "some_data"; const value_2 = 42;
const metadata = {
    "key-1": value_1,
    "key-2": value_2,
};

const previousContext = context.active();
const newBaggage = propagation.createBaggage({
    "metadata": { value: JSON.stringify(metadata) }
});
const newContextWithMetadata = propagation.setBaggage(previousContext, newBaggage);

context.with(newContextWithMetadata, () => {
    // Your code here. Spans created by traceAI auto-instrumentation inside this block
    // should pick up the 'metadata' attribute from baggage.
    // e.g., myInstrumentedFunction();
});
from fi_instrumentation import using_metadata
# Assuming metadata is defined as above
@using_metadata(metadata)
def call_fn(*args, **kwargs):
    # Calls within this function will generate spans with the attributes:
    # "metadata" = "{"key-1": value_1, "key-2": value_2, ... }" # JSON serialized
    pass # Your function code here

Enhance spans with categorical tags. Tags must be provided as a list of strings.

from fi_instrumentation import using_tags
# Assuming tags list is defined
# tags = ["tag_1", "tag_2"]
with using_tags(tags):
    # Calls within this block will generate spans with the attributes:
    # "tag.tags" = "["tag_1","tag_2",...]"
    pass # Your code here
import { context, propagation } from "@opentelemetry/api";

// Assuming tags list is defined, e.g.:
// const tags = ["tag_A", "tag_B"];

const previousContext = context.active();
const newBaggage = propagation.createBaggage({
    "tag.tags": { value: JSON.stringify(tags) } // Stored as JSON string
});
const newContextWithTags = propagation.setBaggage(previousContext, newBaggage);

context.with(newContextWithTags, () => {
    // Your code here. Spans created by traceAI auto-instrumentation inside this block
    // should pick up the 'tag.tags' attribute from baggage.
    // e.g., myInstrumentedFunction();
});
from fi_instrumentation import using_tags
# Assuming tags is defined as above
@using_tags(tags)
def call_fn(*args, **kwargs):
    # Calls within this function will generate spans with the attributes:
    # "tag.tags" = "["tag_1","tag_2",...]"
    pass # Your function code here

Set a session identifier for all spans within the context to group related operations under a common session.

from fi_instrumentation import using_session
# Assuming session_id is defined
# session_id = "session_123"
with using_session(session_id):
    # Calls within this block will generate spans with the attributes:
    # "session.id" = "session_123"
    pass # Your code here
import { context, propagation } from "@opentelemetry/api";

// Assuming session_id is defined, e.g.:
// const session_id = "session_123";

const previousContext = context.active();
const newBaggage = propagation.createBaggage({
    "session.id": { value: session_id }
});
const newContextWithSession = propagation.setBaggage(previousContext, newBaggage);

context.with(newContextWithSession, () => {
    // Your code here. Spans created by traceAI auto-instrumentation inside this block
    // should pick up the 'session.id' attribute from baggage.
    // e.g., myInstrumentedFunction();
});
from fi_instrumentation import using_session
# Assuming session_id is defined as above
@using_session(session_id)
def call_fn(*args, **kwargs):
    # Calls within this function will generate spans with the attributes:
    # "session.id" = "session_123"
    pass # Your function code here

Set a user identifier for all spans within the context to track operations performed by specific users.

from fi_instrumentation import using_user
# Assuming user_id is defined
# user_id = "user_456"
with using_user(user_id):
    # Calls within this block will generate spans with the attributes:
    # "user.id" = "user_456"
    pass # Your code here
import { context, propagation } from "@opentelemetry/api";

// Assuming user_id is defined, e.g.:
// const user_id = "user_456";

const previousContext = context.active();
const newBaggage = propagation.createBaggage({
    "user.id": { value: user_id }
});
const newContextWithUser = propagation.setBaggage(previousContext, newBaggage);

context.with(newContextWithUser, () => {
    // Your code here. Spans created by traceAI auto-instrumentation inside this block
    // should pick up the 'user.id' attribute from baggage.
    // e.g., myInstrumentedFunction();
});
from fi_instrumentation import using_user
# Assuming user_id is defined as above
@using_user(user_id)
def call_fn(*args, **kwargs):
    # Calls within this function will generate spans with the attributes:
    # "user.id" = "user_456"
    pass # Your function code here

Enrich spans with prompt template information to track how prompts are constructed and which variables are used.

from fi_instrumentation import using_prompt_template
# Assuming template, version, and variables are defined
# template = "Hello {name}, your age is {age}"
# version = "v1.0"
# variables = {"name": "Alice", "age": 30}
with using_prompt_template(
    template=template,
    version=version,
    variables=variables
):
    # Calls within this block will generate spans with the attributes:
    # "llm.prompt_template.template" = "Hello {name}, your age is {age}"
    # "llm.prompt_template.version" = "v1.0"
    # "llm.prompt_template.variables" = '{"name": "Alice", "age": 30}'
    pass # Your code here
import { context, propagation } from "@opentelemetry/api";

// Assuming template, version, and variables are defined, e.g.:
// const template = "Hello {name}, your age is {age}";
// const version = "v1.0";
// const variables = {"name": "Alice", "age": 30};

const previousContext = context.active();
const newBaggage = propagation.createBaggage({
    "llm.prompt_template.template": { value: template },
    "llm.prompt_template.version": { value: version },
    "llm.prompt_template.variables": { value: JSON.stringify(variables) }
});
const newContextWithPromptTemplate = propagation.setBaggage(previousContext, newBaggage);

context.with(newContextWithPromptTemplate, () => {
    // Your code here. Spans created by traceAI auto-instrumentation inside this block
    // should pick up the prompt template attributes from baggage.
    // e.g., myInstrumentedFunction();
});
from fi_instrumentation import using_prompt_template
# Assuming template, version, and variables are defined as above
@using_prompt_template(
    template=template,
    version=version,
    variables=variables
)
def call_fn(*args, **kwargs):
    # Calls within this function will generate spans with the attributes:
    # "llm.prompt_template.template" = "Hello {name}, your age is {age}"
    # "llm.prompt_template.version" = "v1.0"
    # "llm.prompt_template.variables" = '{"name": "Alice", "age": 30}'
    pass # Your function code here

Combine multiple context managers

Use multiple context managers together to set various attributes simultaneously on all spans within a block.

from fi_instrumentation import using_metadata, using_tags, using_session, using_user

metadata = {"experiment": "A/B test", "version": "2.1"}
tags = ["production", "critical"]
session_id = "session_789"
user_id = "user_101"

with using_metadata(metadata), \
     using_tags(tags), \
     using_session(session_id), \
     using_user(user_id):
    # All spans created within this block will have:
    # - metadata attributes
    # - tag attributes
    # - session.id attribute
    # - user.id attribute
    pass # Your code here
import { context, propagation } from "@opentelemetry/api";

const metadata = {"experiment": "A/B test", "version": "2.1"};
const tags = ["production", "critical"];
const session_id = "session_789";
const user_id = "user_101";

const previousContext = context.active();
const newBaggage = propagation.createBaggage({
    "metadata": { value: JSON.stringify(metadata) },
    "tag.tags": { value: JSON.stringify(tags) },
    "session.id": { value: session_id },
    "user.id": { value: user_id }
});
const newContextWithAllAttributes = propagation.setBaggage(previousContext, newBaggage);

context.with(newContextWithAllAttributes, () => {
    // All spans created within this block will have:
    // - metadata attributes
    // - tag attributes
    // - session.id attribute
    // - user.id attribute
    // e.g., myInstrumentedFunction();
});

Key concepts

  • set_attribute() — Attaches a key/value pair directly to the active span. Supports strings, numbers, and booleans. Prefix custom attributes with your company name to avoid naming conflicts.
  • Semantic Conventions — Structured attribute names defined by traceAI for common LLM data (messages, prompt templates, token counts). Use SpanAttributes and MessageAttributes constants from fi_instrumentation.fi_types.
  • Context attributes (Baggage) — Set at the OpenTelemetry context level so they propagate automatically to all child spans within the block, without modifying instrumented functions.
  • using_metadata — Attaches a JSON-serialized metadata dictionary to all spans in the context as the metadata attribute.
  • using_tags — Attaches a JSON-serialized list of tag strings to all spans as tag.tags.
  • using_session — Sets session.id on all spans in the context for session grouping.
  • using_user — Sets user.id on all spans in the context for per-user tracking.
  • using_prompt_template — Sets llm.prompt_template.template, llm.prompt_template.version, and llm.prompt_template.variables on all spans in the context.

What you can do next

Was this page helpful?

Questions & Discussion