Skip to main content

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 spanStart spanWith span / use spanStart 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

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.

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

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

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

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

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