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
  • 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.

Basic Usage

from lmnr import Laminar


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. 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

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']): type of the span. If not specified, it will be 'DEFAULT'

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.
It’s highly recommended to set input of manual LLM spans in the OpenAI’s messages format to get see LLM calls-specific rendering in the UI.
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();

Fine-grained control over span creation

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.
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();
}

Custom Span Hierarchies

Example of creating complex span hierarchies for detailed tracing of multi-step operations:
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();
  }
};

Capturing errors and setting span attributes

To properly handle errors and set span attributes for better observability, you can use the following pattern.
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();
}

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()

Meaningful Span Names

# ✅ Good - descriptive, hierarchical names
with Laminar.start_as_current_span(name="user_registration.email_validation"):
    validate_email()

with Laminar.start_as_current_span(name="user_registration.password_hashing"):
    hash_password()

# ❌ Bad - generic or unclear names  
with Laminar.start_as_current_span(name="process"):
    validate_email()

Rich Attributes

# ✅ Good - structured, queryable attributes
Laminar.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 context
Laminar.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.