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
Custom span hierarchies that don’t fit the function-level observe pattern
The Laminar.start_as_current_span method is a recommended way to create spans manually in Python.
It creates a new span and sets it as the current span using a context manager. Context manager properly starts and ends the span.
from lmnr import Laminardef 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. If not specified, it will be `'DEFAULT'` ) as span: try: # ... your code here ... 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 span.record_exception(error) # This will create exception event on the span raise
If you need absolute control over span creation and completion, Laminar provides methods to start and end spans manually.
You can use Laminar.startSpan to create a span manually.
It doesn’t set the span as the current span.
You need to manually set the span as the current span using Laminar.withSpan context manager.
You also need to manually end the span using span.end() method.
You can use Laminar.withSpan to set the manually created span as the current span.
Copy
import { Laminar } from '@lmnr-ai/lmnr';const span = Laminar.startSpan({ name: 'operation' });try { // ... your code here ... const result = await performOperation(); // defined somewhere else Laminar.withSpan(span, async () => { await observe({ name: 'nested_operation' }, async () => { // now `nested_operation` will be a child of `operation` span }); }); span.setAttributes({ 'operation.result_count': result.length, 'operation.success': true });} finally { // Always end the span span.end();}
# ✅ Good - using context manager (automatic cleanup)with Laminar.start_as_current_span(name="operation"): do_work()# ✅ Good - manual cleanup with try/finallyspan = 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()
# ✅ Good - structured, queryable attributesLaminar.set_span_attributes({ "user.id": user_id, "user.tier": "premium", "operation.batch_size": 100, "operation.retry_count": 2, "feature.experimental_enabled": True})# ❌ Bad - unstructured or missing contextLaminar.set_span_attributes({ "data": "some processing"})
Manual span creation provides the flexibility and control needed for complex tracing scenarios while maintaining compatibility with the broader OpenTelemetry ecosystem.