Overview
Manual span creation gives you fine-grained control over span lifecycle, attributes, and hierarchies. This is useful for:
- Fine-grained control over span lifecycle and attributes
- Integration with existing tracing in codebases that already use OpenTelemetry
- Passing span context to other functions and services. (Learn more)
Choosing the right method
It is crucial to understand the difference between creating a span and creating an active span.
Start Active Span (withSpan / start_as_current_span)
Active Spans empower you to automatically capture the entire execution context. By activating a span, any subsequent operations—including those in third-party libraries—are automatically recorded as children of that span.
Analogy: You create a folder and immediately open it. Any file you save now goes inside that folder automatically.
Start Manual Span (startSpan)
Manual Spans give you control over detached or asynchronous operations. This is useful when you need to pass a span to a completely different part of your system or when the parent-child relationship isn’t defined by the call stack.
Analogy: It’s like creating a folder on your desktop but not opening it. If you save a file, it still goes to the desktop, not inside your new folder.
Summary
Active spans are the preferred way to create spans, as they automatically handle nesting. Manual spans are useful for advanced use cases where you need precise control over the span’s lifecycle and context.
Span creation options
There are several options to create and manage spans manually.
Refer to the Span lifecycle to understand the differences between the options.
Summary of the options
| Start as current span | Start span | With span / use span | Start active span |
|---|
| Creates a new span (starts it) | ✅ | ✅ | ❌ | ✅ |
| Activates the span (sets it as current) | ✅ | ❌ | ✅ | ✅ |
| Ends the span | ✅ | ❌ | ❔ (if end_on_exit is passed) | ❌ |
| Available in Python | ✅ | ✅ | ✅ | ✅ |
| Available in JavaScript | ❌ (observe can be used instead) | ✅ | ✅ | ✅ |
Start as current span
JavaScript/TypeScript
Python
Laminar TypeScript SDK does not have a context manager equivalent to start_as_current_span.You can use observe function wrapper instead, as it achieves the same effect.import { Laminar, observe } from '@lmnr-ai/lmnr';
await observe({ name: 'custom_operation' }, async () => {
// Any spans created here will be children of the "custom_operation" span
});
Learn more about observe here. The Laminar.start_as_current_span method is a recommended way to create spans manually in Python.
This is a context manager that creates a new span, sets it as active (that is, all spans created inside it will be children of this span), and ends the span when the context manager exits.from lmnr import Laminar
Laminar.initialize()
def process_data(input_data):
# ... your code here ...
with Laminar.start_as_current_span(
name="custom_operation", # name of the span
input=input_data, # input of the span
span_type="DEFAULT" # type of the span.
) as span:
try:
# ... your code here ...
# Any spans created here will be children of
# the "custom_operation" span
result = process_data(input_data)
# Set span output and custom attributes
Laminar.set_span_output(result)
Laminar.set_span_attributes({
"custom.result_count": len(result)
})
except Exception as error:
# Record error on span. This will create exception event on the
# span.
span.record_exception(error)
raise
Parameters
name (str): name of the span
input (Any): input to the span. It will be serialized to JSON and recorded as span input
span_type (Literal['DEFAULT'] | Literal['LLM'] | Literal['TOOL']): type of the span. If not specified, it will be 'DEFAULT'
tags (list[str]): list of tags to add to the span.
parent_span_context (LaminarSpanContext): [Advanced] a span context to use as the parent of the new span. (See Using Span Context Serialization)
context (opentelemetry.context.Context): [Advanced] an OTEL context to use for the span.
Start span
This function creates a new span and returns it. It does not activate the span, so there are two main use cases:
- As a leaf span, for an operation that does not have children spans.
- To pass the span object to other functions. Learn more about passing span objects.
JavaScript/TypeScript
Python
import { Laminar } from '@lmnr-ai/lmnr';
Laminar.initialize();
const sum = (a: number, b: number): number => a + b;
const span = Laminar.startSpan({ name: 'custom_operation' });
const result = sum(1, 2);
span.setAttributes({
'custom.operation.result': result,
});
span.end();
Parameters
name (str): name of the span
input (any): input to the span. It will be serialized to JSON and recorded as span input
spanType ('DEFAULT' | 'LLM' | 'TOOL'): type of the span. If not specified, it will be 'DEFAULT'
tags (string[]): list of tags to add to the span.
userId (string): user ID for the span.
sessionId (string): session ID for the span.
metadata (Record<string, any>): metadata for the span. Must be json serializable.
parentSpanContext (LaminarSpanContext): [Advanced] a span context to use as the parent of the new span. (See Using Span Context Serialization)
context (Context (from @opentelemetry/api)): [Advanced] an OTEL context to use for the span.
from lmnr import Laminar
Laminar.initialize()
def sum(a: int, b: int) -> int:
return a + b
span = Laminar.startSpan(name="custom_operation")
result = sum(1, 2)
span.set_attribute("custom.operation.result", result)
span.end()
Parameters
name (str): name of the span
input (Any): input to the span. It will be serialized to JSON and recorded as span input
span_type (Literal['DEFAULT'] | Literal['LLM'] | Literal['TOOL']): type of the span. If not specified, it will be 'DEFAULT'
tags (list[str]): list of tags to add to the span.
parent_span_context (LaminarSpanContext): [Advanced] a span context to use as the parent of the new span. (See Using Span Context Serialization)
context (opentelemetry.context.Context): [Advanced] an OTEL context to use for the span.
With span / use span
This function accepts an existing span and activates it for the scope of the function / context manager. That is, all spans created inside it will be children of the passed span.
Any spans created outside of it will not be children of the passed span (unless the passed span is already activated elsewhere).
JavaScript/TypeScript
Python
import { Laminar } from '@lmnr-ai/lmnr';
Laminar.initialize();
const parentSpan = Laminar.startSpan({ name: 'parent' });
await Laminar.withSpan(span, async () => {
// 'parent' span is active here
const childSpan = Laminar.startSpan({ name: 'child' });
// ... your code here ...
childSpan.end();
});
// 'parent' span is not active here anymore
// 'other' span is not a child of 'parent' span, because 'parent'
// span is not active
const otherSpan = Laminar.startSpan({ name: 'other' });
// ... your code here ...
otherSpan.end();
// you can use withSpan multiple times
await Laminar.withSpan(span, async () => {
const childSpan2 = Laminar.startSpan({ name: 'child2' });
// ... your code here ...
childSpan2.end();
});
parentSpan.end();
Parameters
span (Span): the span to activate
fn (Function): the function to run within the span context.
endOnExit (bool): whether to end the passed span when the context manager exits.
from lmnr import Laminar
Laminar.initialize()
parent_span = Laminar.start_span(name="parent")
with Laminar.use_span(parent_span):
# 'parent' span is active here
child_span = Laminar.start_span(name="child")
# ... your code here ...
child_span.end()
# 'parent' span is not active here anymore
# 'other' span is not a child of 'parent' span, because 'parent'
# span is not active
other_span = Laminar.start_span(name="other")
# ... your code here ...
other_span.end()
# you can use use_span multiple times
with Laminar.use_span(parent_span):
child_span2 = Laminar.start_span(name="child2")
# ... your code here ...
child_span2.end()
parent_span.end()
Parameters
span (Span): the span to activate
end_on_exit (bool): whether to end the passed span when the context manager exits.
Start active span
This function creates a new span, activates it immediately, and returns it.
Despite its simplicity, this function needs to be used with extreme caution.
In Python, you must make sure that the span is ended within the same thread (or async context) that it was created in.
Otherwise, the behavior is undefined, and the trace may have a corrupted hierarchy.
JavaScript/TypeScript
Python
import { Laminar } from '@lmnr-ai/lmnr';
Laminar.initialize();
const span = Laminar.startActiveSpan({ name: 'custom_operation' });
// ... your code here ...
// Any spans created here will be children of the 'custom_operation' span
span.end();
Parameters
name (str): name of the span
input (any): input to the span. It will be serialized to JSON and recorded as span input
spanType ('DEFAULT' | 'LLM' | 'TOOL'): type of the span. If not specified, it will be 'DEFAULT'
tags (string[]): list of tags to add to the span.
userId (string): user ID for the span.
sessionId (string): session ID for the span.
metadata (Record<string, any>): metadata for the span. Must be json serializable.
parentSpanContext (LaminarSpanContext): [Advanced] a span context to use as the parent of the new span. (See Using Span Context Serialization)
context (Context (from @opentelemetry/api)): [Advanced] an OTEL context to use for the span.
from lmnr import Laminar
Laminar.initialize()
span = Laminar.start_active_span(name="custom_operation")
# ... your code here ...
# Any spans created here will be children of the 'custom_operation' span
span.end()
Parameters
name (str): name of the span
input (Any): input to the span. It will be serialized to JSON and recorded as span input
span_type (Literal['DEFAULT'] | Literal['LLM'] | Literal['TOOL']): type of the span. If not specified, it will be 'DEFAULT'
tags (list[str]): list of tags to add to the span.
parent_span_context (LaminarSpanContext): [Advanced] a span context to use as the parent of the new span. (See Using Span Context Serialization)
context (opentelemetry.context.Context): [Advanced] an OTEL context to use for the span.
Configuration examples
Manually creating an LLM span
To manually create an LLM span, set span_type="LLM" and properly set the span attributes related to LLM calls.
JavaScript/TypeScript
Python
import { Laminar, LaminarAttributes } from '@lmnr-ai/lmnr';
const span = Laminar.startSpan({ name: 'custom_llm_call', span_type: 'LLM' });
const response = await fetch('https://api.custom-llm.com/v1/completions', {
method: 'POST',
body: JSON.stringify({
model: 'custom-model-1',
messages: [
{ role: 'user', content: 'What is the longest river in the world?' },
],
}),
});
const data = await response.json();
span.setAttributes({
[LaminarAttributes.PROVIDER]: 'custom-llm.com',
[LaminarAttributes.REQUEST_MODEL]: 'custom-model-1',
[LaminarAttributes.RESPONSE_MODEL]: data.model,
[LaminarAttributes.INPUT_TOKEN_COUNT]: data.usage.input_tokens,
[LaminarAttributes.OUTPUT_TOKEN_COUNT]: data.usage.output_tokens,
});
span.end();
from lmnr import Laminar, Attributes
import requests
messages = [{
"role": "user",
"content": "What is the longest river in the world?"
}]
with Laminar.start_as_current_span(
name="custom_llm_call",
input=messages,
span_type="LLM"
):
response = requests.post(
"https://api.custom-llm.com/v1/completions",
json={
"model": "custom-model-1",
"messages": messages,
},
).json()
Laminar.set_span_output(response["choices"][0]["message"]["content"])
# Set LLM-specific attributes
Laminar.set_span_attributes({
Attributes.PROVIDER: "custom-llm.com",
Attributes.REQUEST_MODEL: "custom-model-1",
Attributes.RESPONSE_MODEL: response["model"],
Attributes.INPUT_TOKEN_COUNT: response["usage"]["input_tokens"],
Attributes.OUTPUT_TOKEN_COUNT: response["usage"]["output_tokens"],
})
Custom Span Hierarchies
Example of creating complex span hierarchies for detailed tracing of multi-step operations:
JavaScript/TypeScript
Python
import { Laminar } from '@lmnr-ai/lmnr';
const processWorkflow = async (workflowData) => {
const workflowSpan = Laminar.startSpan({
name: 'workflow-execution',
metadata: { workflowId: workflowData.id }
});
try {
await Laminar.withSpan(workflowSpan, async () => {
// Step 1: Validation
const validationSpan = Laminar.startSpan({ name: 'validation' });
await Laminar.withSpan(validationSpan, async () => {
await validateWorkflow(workflowData);
});
validationSpan.end();
// Step 2: Processing
const processingSpan = Laminar.startSpan({ name: 'processing' });
await Laminar.withSpan(processingSpan, async () => {
const result = await processWithLLM(workflowData);
processingSpan.setAttributes({
'processing.items_count': result.items.length,
'processing.duration_ms': result.duration
});
});
processingSpan.end();
// Step 3: Finalization
const finalizationSpan = Laminar.startSpan({ name: 'finalization' });
await Laminar.withSpan(finalizationSpan, async () => {
await finalizeWorkflow(workflowData);
});
finalizationSpan.end();
});
workflowSpan.setAttributes({
'workflow.status': 'completed',
'workflow.total_steps': 3
});
} catch (error) {
workflowSpan.recordException(error);
throw error;
} finally {
workflowSpan.end();
}
};
from opentelemetry import trace
from lmnr import Laminar
def process_workflow(workflow_data):
with Laminar.start_as_current_span(
name="workflow_execution",
input={"workflow_id": workflow_data["id"]}
) as workflow_span:
try:
# Step 1: Validation
with Laminar.start_as_current_span(name="validation"):
validate_workflow(workflow_data)
# Step 2: Processing
with Laminar.start_as_current_span(
name="processing",
) as processing_span:
result = process_with_llm(workflow_data)
processing_span.set_attribute(
"processing.items_count",
len(result["items"])
)
processing_span.set_attribute(
"processing.duration_ms",
result["duration"]
)
# Step 3: Finalization
with Laminar.start_as_current_span(name="finalization"):
finalize_workflow(workflow_data)
# Set workflow-level attributes
workflow_span.set_attribute("workflow.status", "completed")
workflow_span.set_attribute("workflow.total_steps", 3)
except Exception as error:
workflow_span.record_exception(error)
workflow_span.set_status(
trace.Status(trace.StatusCode.ERROR, str(error)),
)
raise
Capturing errors and setting span attributes
To properly handle errors and set span attributes for better observability, you can use the following pattern.
JavaScript/TypeScript
Python
const span = Laminar.startSpan({ name: 'risky-operation' });
try {
const result = await riskyOperation();
span.setAttributes({ 'operation.result': 'success' });
} catch (error) {
// Record the exception details
span.recordException(error);
throw error; // Re-throw if needed
} finally {
span.end();
}
from opentelemetry import trace
with Laminar.start_as_current_span(name="risky_operation") as span:
try:
result = risky_operation()
# Set success status explicitly if needed
span.set_attribute("operation.result", "success")
except Exception as error:
# Record the exception details
span.record_exception(error)
raise # Re-raise if needed
Best Practices
Always End Spans
# ✅ Good - using context manager (automatic cleanup)
with Laminar.start_as_current_span(name="operation"):
do_work()
# ✅ Good - manual cleanup with try/finally
span = Laminar.start_span(name="operation")
try:
do_work()
finally:
span.end()
# ❌ Bad - no cleanup (span never ends)
span = Laminar.start_span(name="operation")
do_work()