Manual Instrumentation

This guide covers advanced manual span management techniques for TypeScript/JavaScript applications when you need fine-grained control over observability beyond the automatic withActiveSpan method.

withActiveSpan Method

The withActiveSpan method is the recommended approach for most manual instrumentation needs. It automatically handles context propagation, error handling, and span cleanup, making it both safer and easier to use than manual span management. For consistent attribute naming, combine this with Semantic Conventions.

Basic Usage

import { getLangWatchTracer, SpanStatusCode } from "langwatch";

const tracer = getLangWatchTracer("my-service");

// Simple usage with automatic cleanup
await tracer.withActiveSpan("my-operation", async (span) => {
  span.setType("llm");
  span.setInput("Hello, world!");
  
  // Your business logic here
  const result = await processRequest("Hello, world!");
  
  span.setOutput(result);
  span.setStatus({ code: SpanStatusCode.OK });
  
  return result;
});

Error Handling

withActiveSpan automatically handles errors and ensures proper span cleanup:
await tracer.withActiveSpan("risky-operation", async (span) => {
  span.setType("external_api");
  span.setInput({ userId: "123", action: "update_profile" });
  
  try {
    // This might throw an error
    const result = await externalApiCall();
    span.setOutput(result);
    span.setStatus({ code: SpanStatusCode.OK });
    return result;
  } catch (error) {
    // Error is automatically recorded and span status is set to ERROR
    span.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message
    });
    span.recordException(error);
    throw error; // Re-throw to maintain error flow
  }
  // Span is automatically ended in finally block
});

Context Propagation

withActiveSpan automatically propagates span context to child operations:
async function processUserRequest(userId: string) {
  return await tracer.withActiveSpan("process-user-request", async (span) => {
    span.setType("user_operation");
    span.setInput({ userId });
    
    // Child operations automatically inherit the span context
    const userData = await fetchUserData(userId);
    const userProfile = await updateUserProfile(userId);
    
    const result = { userData, userProfile };
    span.setOutput(result);
    span.setStatus({ code: SpanStatusCode.OK });
    
    return result;
  });
}

// Child operations automatically create child spans
async function fetchUserData(userId: string) {
  return await tracer.withActiveSpan("fetch-user-data", async (span) => {
    span.setType("database_query");
    // This span is automatically a child of the parent span
    // ... database logic ...
  });
}

Custom Attributes and Events

Add rich metadata to your spans:
await tracer.withActiveSpan("custom-operation", async (span) => {
  // Set span type
  span.setType("llm");
  
  // Add custom attributes for filtering and analysis
  span.setAttributes({
    "custom.business_unit": "marketing",
    "custom.campaign_id": "summer-2024",
    "custom.user_tier": "premium",
    "custom.operation_type": "batch_processing",
    "llm.model": "gpt-5-mini",
    "llm.temperature": 0.7
  });
  
  // Add events to track important milestones
  span.addEvent("processing_started", {
    timestamp: Date.now(),
    batch_size: 1000
  });
  
  // Your business logic
  const result = await processBatch();
  
  span.addEvent("processing_completed", {
    timestamp: Date.now(),
    processed_count: result.length
  });
  
  span.setOutput(result);
  span.setStatus({ code: SpanStatusCode.OK });
  
  return result;
});
For consistent attribute naming and TypeScript autocomplete support, use semantic conventions. See our Semantic Conventions guide for best practices.

Conditional Span Creation

Create spans conditionally based on your application logic:
async function conditionalOperation(shouldTrace: boolean, data: any) {
  if (shouldTrace) {
    return await tracer.withActiveSpan("conditional-operation", async (span) => {
      span.setType("conditional");
      span.setInput(data);
      
      const result = await processData(data);
      
      span.setOutput(result);
      span.setStatus({ code: SpanStatusCode.OK });
      
      return result;
    });
  } else {
    // No tracing overhead when not needed
    return await processData(data);
  }
}

Basic Manual Span Management

When you need fine-grained control over spans beyond what withActiveSpan provides, you can manually manage span lifecycle, attributes, and context propagation.

Using startActiveSpan

startActiveSpan provides automatic context management but requires manual error handling:
// Using startActiveSpan (automatic context management)
tracer.startActiveSpan("my-operation", (span) => {
  try {
    span.setType("llm");
    span.setInput("Hello, world!");
    // ... your business logic ...
    span.setOutput("Hello! How can I help you?");
    span.setStatus({ code: SpanStatusCode.OK });
  } catch (error) {
    span.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message
    });
    span.recordException(error);
    throw error;
  } finally {
    span.end();
  }
});

Using startSpan (Complete Manual Control)

startSpan gives you complete control but requires manual context management:
// Using startSpan (complete manual control)
const span = tracer.startSpan("my-operation");
try {
  span.setType("llm");
  span.setInput("Hello, world!");
  // ... your business logic ...
  span.setOutput("Hello! How can I help you?");
  span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
  span.setStatus({
    code: SpanStatusCode.ERROR,
    message: error.message
  });
  span.recordException(error);
  throw error;
} finally {
  span.end();
}

Span Context Propagation

Manually propagate span context across async boundaries and service boundaries when withActiveSpan isn’t sufficient:
import { context, trace } from "@opentelemetry/api";

async function processWithContext(userId: string) {
  const span = tracer.startSpan("process-user");
  const ctx = trace.setSpan(context.active(), span);
  
  try {
    // Propagate context to async operations
    await context.with(ctx, async () => {
      await processUserData(userId);
      await updateUserProfile(userId);
    });
    
    span.setStatus({ code: SpanStatusCode.OK });
  } catch (error) {
    span.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message
    });
    span.recordException(error);
    throw error;
  } finally {
    span.end();
  }
}

Error Handling Patterns

Implement robust error handling for manual span management:
class SpanManager {
  private tracer = getLangWatchTracer("my-service");

  async executeWithSpan<T>(
    operationName: string,
    operation: (span: Span) => Promise<T>
  ): Promise<T> {
    const span = this.tracer.startSpan(operationName);
    
    try {
      const result = await operation(span);
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: error.message
      });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  }
}

// Usage example
const spanManager = new SpanManager();
const result = await spanManager.executeWithSpan("my-operation", async (span) => {
  span.setType("llm");
  span.setInput("Hello");
  // ... your business logic ...
  return "World";
});

Custom Span Processors

Create custom span processors for specialized processing needs, filtering, and multiple export destinations.

Custom Exporters

Configure custom exporters alongside LangWatch:
import { setupObservability } from "langwatch/observability/setup/node";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

const handle = await setupObservability({  
  // Use custom span processors
  spanProcessors: [
    new BatchSpanProcessor(new OTLPTraceExporter({
      url: 'https://custom-collector.com/v1/traces'
    }))
  ],
  
  // Or use a single trace exporter
  traceExporter: new OTLPTraceExporter({
    url: 'https://custom-collector.com/v1/traces'
  })
});

Span Filtering

Implement span filtering to control which spans are processed:
import { FilterableBatchSpanProcessor, LangWatchExporter } from "langwatch";

const processor = new FilterableBatchSpanProcessor(
  new LangWatchExporter({
    apiKey: "your-api-key",
  }),
  [
    { attribute: "http.url", value: "/health" },
    { attribute: "span.type", value: "health" },
    { attribute: "custom.ignore", value: "true" }
  ]
);

const handle = await setupObservability({
  langwatch: 'disabled',
  spanProcessors: [processor]
});

Multiple Exporters

Configure multiple exporters for different destinations:
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { JaegerExporter } from "@opentelemetry/exporter-jaeger";
import { LangWatchExporter } from "langwatch";

const handle = await setupObservability({
  serviceName: "my-service",
  spanProcessors: [
    // Send to Jaeger for debugging
    new BatchSpanProcessor(new JaegerExporter({
      endpoint: "http://localhost:14268/api/traces"
    })),
    // Send to LangWatch for production monitoring
    new BatchSpanProcessor(new LangWatchExporter({
      apiKey: process.env.LANGWATCH_API_KEY
    }))
  ]
});

Batch Processing Configuration

Optimize batch processing for high-volume applications:
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { LangWatchExporter } from "langwatch";

const batchProcessor = new BatchSpanProcessor(
  new LangWatchExporter({
    apiKey: process.env.LANGWATCH_API_KEY
  }),
  {
    maxQueueSize: 2048, // Maximum number of spans in queue
    maxExportBatchSize: 512, // Maximum spans per batch
    scheduledDelayMillis: 5000, // Export interval
    exportTimeoutMillis: 30000, // Export timeout
  }
);

const handle = await setupObservability({
  langwatch: 'disabled', // Disabled we report to LangWatch via the `batchProcessor`
  spanProcessors: [batchProcessor]
});

Performance Considerations

When using manual span management, consider these performance implications:
Manual span management requires careful attention to memory usage and proper cleanup to avoid memory leaks.
  1. Memory Usage: Manually created spans consume memory until explicitly ended
  2. Context Propagation: Manual context management can be error-prone and impact performance
  3. Error Handling: Ensure spans are always ended, even when exceptions occur
  4. Batch Processing: Use batch processors for high-volume applications to reduce overhead
  5. Sampling: Implement sampling to reduce overhead in production environments

Best Practices

Use withActiveSpan

  • Prefer withActiveSpan for most use cases
  • Automatic context propagation and error handling
  • Guaranteed span cleanup

Manual Control

  • Use manual span management only when needed
  • Always end spans in finally blocks
  • Use try-catch-finally patterns consistently

Context Management

  • Propagate span context across async boundaries
  • Use context.with() for async operations
  • Maintain span hierarchy properly

Attributes and Events

  • Add meaningful custom attributes for filtering
  • Use consistent attribute naming conventions
  • Include relevant business context

Performance

  • Implement appropriate sampling strategies
  • Use batch processors for high volume
  • Monitor observability overhead

Error Handling

  • Set appropriate status codes and error messages
  • Record exceptions with context
  • Maintain error flow in your application

When to Use Each Approach

For most use cases, the withActiveSpan method provides the best balance of ease of use, safety, and functionality. Only use manual span management when you need specific control over span lifecycle or context propagation that withActiveSpan cannot provide.
For more advanced observability patterns and best practices:
Combine manual instrumentation with Semantic Conventions for consistent, maintainable observability across your application.