· 10 Min read

SvelteKit OpenTelemetry Setup: Complete Observability Guide

SvelteKit OpenTelemetry Setup: Complete Observability Guide

SvelteKit's recent integration with OpenTelemetry marks a significant leap forward in application observability. With the introduction of instrumentation.server.ts and built-in tracing capabilities in SvelteKit 2.31+, developers can now gain deep insights into their application's performance without complex third-party integrations.

This tutorial walks you through implementing comprehensive observability in your SvelteKit application, from basic trace collection to advanced monitoring dashboards. By the end, you'll have a production-ready observability setup that tracks everything from load functions to form actions.

Link to section: Understanding SvelteKit's OpenTelemetry IntegrationUnderstanding SvelteKit's OpenTelemetry Integration

OpenTelemetry provides standardized observability across your entire stack. SvelteKit's implementation automatically instruments several key areas of your application, eliminating the manual work typically required for comprehensive tracing.

The framework now emits spans for handle hooks, load functions (both server and universal when running server-side), form actions, and remote functions. Each span includes contextual attributes like http.route and associated file paths, making it easier to correlate traces with your codebase.

The instrumentation.server.ts file serves as the guaranteed entry point for your observability setup. This file loads before any application code, ensuring your tracing infrastructure is properly initialized regardless of how your application scales or where it deploys.

Link to section: Initial Project Setup and DependenciesInitial Project Setup and Dependencies

Start by creating a new SvelteKit project or working with an existing one. Ensure you're running SvelteKit 2.31 or higher:

npm create svelte@latest observability-demo
cd observability-demo
npm install

Install the required OpenTelemetry dependencies:

npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-proto import-in-the-middle

For development and testing, you'll also want Jaeger for trace visualization:

docker run -d --name jaeger \
  -p 16686:16686 \
  -p 14268:14268 \
  jaegertracing/all-in-one:latest

This runs Jaeger with the web interface accessible at http://localhost:16686 and the collector endpoint at http://localhost:14268.

Link to section: Configuring SvelteKit for ObservabilityConfiguring SvelteKit for Observability

Enable the experimental observability features in your svelte.config.js:

import adapter from '@sveltejs/adapter-auto';
 
/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter(),
    experimental: {
      tracing: {
        server: true
      },
      instrumentation: {
        server: true
      }
    }
  }
};
 
export default config;

These flags activate SvelteKit's built-in tracing and enable the instrumentation.server.ts file. The tracing.server option tells SvelteKit to emit OpenTelemetry spans, while instrumentation.server ensures your custom instrumentation code loads first.

Link to section: Creating the Instrumentation FileCreating the Instrumentation File

Create src/instrumentation.server.js (or .ts if using TypeScript) with the basic OpenTelemetry setup:

import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { createAddHookMessageChannel } from 'import-in-the-middle';
import { register } from 'module';
 
// Set up message channel for import hooks
const { addHookMessagePort } = createAddHookMessageChannel();
register('import-in-the-middle/hook.mjs', {
  data: { addHookMessagePort },
  transferList: [addHookMessagePort]
});
 
// Configure trace exporter
const traceExporter = new OTLPTraceExporter({
  url: 'http://localhost:14268/api/traces',
});
 
// Initialize SDK
const sdk = new NodeSDK({
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations({
    '@opentelemetry/instrumentation-fs': {
      enabled: false,
    },
  })],
});
 
sdk.start();
 
console.log('OpenTelemetry instrumentation initialized');

This configuration exports traces to your local Jaeger instance. The getNodeAutoInstrumentations function automatically instruments common Node.js libraries like HTTP, filesystem operations, and database connections. We disable filesystem instrumentation to reduce noise in development.

Jaeger dashboard showing SvelteKit traces

Link to section: Building a Traced SvelteKit ApplicationBuilding a Traced SvelteKit Application

Create a sample application that demonstrates different types of operations SvelteKit can trace. Start with a page that includes both server and client-side data loading:

// src/routes/+page.server.js
export async function load({ fetch }) {
  // This will be automatically traced by SvelteKit
  const userResponse = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const user = await userResponse.json();
  
  // Simulate some processing time
  await new Promise(resolve => setTimeout(resolve, 100));
  
  const postsResponse = await fetch('https://jsonplaceholder.typicode.com/posts?userId=1');
  const posts = await postsResponse.json();
  
  return {
    user,
    posts: posts.slice(0, 5)
  };
}
 
export const actions = {
  createPost: async ({ request, fetch }) => {
    const data = await request.formData();
    const title = data.get('title');
    const body = data.get('body');
    
    // This form action will also be traced
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        title,
        body,
        userId: 1,
      }),
    });
    
    const result = await response.json();
    
    return {
      success: true,
      post: result
    };
  }
};

Create the corresponding page component:

<!-- src/routes/+page.svelte -->
<script>
  import { enhance } from '$app/forms';
  
  export let data;
  export let form;
  
  let submitting = false;
</script>
 
<svelte:head>
  <title>Observability Demo</title>
</svelte:head>
 
<h1>User Profile: {data.user.name}</h1>
<p>Email: {data.user.email}</p>
<p>Website: {data.user.website}</p>
 
<h2>Recent Posts</h2>
<ul>
  {#each data.posts as post}
    <li><strong>{post.title}</strong> - {post.body}</li>
  {/each}
</ul>
 
<h2>Create New Post</h2>
<form 
  method="POST" 
  action="?/createPost"
  use:enhance={() => {
    submitting = true;
    return async ({ update }) => {
      await update();
      submitting = false;
    };
  }}
>
  <div>
    <label for="title">Title:</label>
    <input id="title" name="title" type="text" required />
  </div>
  
  <div>
    <label for="body">Body:</label>
    <textarea id="body" name="body" required></textarea>
  </div>
  
  <button type="submit" disabled={submitting}>
    {submitting ? 'Creating...' : 'Create Post'}
  </button>
</form>
 
{#if form?.success}
  <p>Post created successfully!</p>
{/if}

Link to section: Advanced Instrumentation ConfigurationAdvanced Instrumentation Configuration

For production applications, you'll want more sophisticated instrumentation. Create an enhanced version of your instrumentation file that includes custom spans and better error handling:

// src/instrumentation.server.js
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { createAddHookMessageChannel } from 'import-in-the-middle';
import { register } from 'module';
 
const { addHookMessagePort } = createAddHookMessageChannel();
register('import-in-the-middle/hook.mjs', {
  data: { addHookMessagePort },
  transferList: [addHookMessagePort]
});
 
// Configure resource attributes
const resource = new Resource({
  [SemanticResourceAttributes.SERVICE_NAME]: 'sveltekit-app',
  [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
  [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
});
 
// Configure trace exporter with retry logic
const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || 'http://localhost:14268/api/traces',
  headers: {},
});
 
// Configure span processor for batching
const spanProcessor = new BatchSpanProcessor(traceExporter, {
  maxExportBatchSize: 100,
  maxQueueSize: 1000,
  scheduledDelayMillis: 500,
});
 
const sdk = new NodeSDK({
  resource,
  spanProcessor,
  instrumentations: [getNodeAutoInstrumentations({
    '@opentelemetry/instrumentation-fs': {
      enabled: false,
    },
    '@opentelemetry/instrumentation-http': {
      requestHook: (span, request) => {
        span.setAttributes({
          'http.request.header.user-agent': request.getHeader('user-agent'),
          'custom.request.id': request.headers['x-request-id'] || 'unknown',
        });
      },
    },
  })],
});
 
// Handle SDK shutdown gracefully
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry terminated'))
    .catch((error) => console.log('Error terminating OpenTelemetry', error))
    .finally(() => process.exit(0));
});
 
sdk.start();
console.log('OpenTelemetry instrumentation initialized');

This enhanced configuration includes service metadata, batched span processing for better performance, and custom HTTP instrumentation that captures additional request headers.

Link to section: Working with Custom SpansWorking with Custom Spans

While SvelteKit automatically traces many operations, you may want to add custom spans for specific business logic. Here's how to create and manage custom spans within your application:

// src/lib/analytics.js
import { trace } from '@opentelemetry/api';
 
const tracer = trace.getTracer('sveltekit-app', '1.0.0');
 
export async function analyzeUserBehavior(userId, action) {
  return tracer.startActiveSpan('user.behavior.analysis', async (span) => {
    try {
      span.setAttributes({
        'user.id': userId,
        'user.action': action,
        'analysis.version': '2.1.0'
      });
      
      // Simulate complex analysis
      await new Promise(resolve => setTimeout(resolve, 200));
      
      const result = {
        recommendation: 'show_premium_features',
        confidence: 0.85,
        factors: ['engagement_high', 'feature_usage_diverse']
      };
      
      span.setAttributes({
        'analysis.recommendation': result.recommendation,
        'analysis.confidence': result.confidence,
        'analysis.factors': result.factors.join(',')
      });
      
      span.setStatus({ code: 1 }); // OK
      return result;
    } catch (error) {
      span.recordException(error);
      span.setStatus({ code: 2, message: error.message }); // ERROR
      throw error;
    } finally {
      span.end();
    }
  });
}

Use this custom tracing in your load functions:

// src/routes/dashboard/+page.server.js
import { analyzeUserBehavior } from '$lib/analytics.js';
 
export async function load({ params, url }) {
  const userId = url.searchParams.get('userId') || '1';
  
  // This will create a nested span under SvelteKit's load span
  const analysis = await analyzeUserBehavior(userId, 'page_view');
  
  return {
    userId,
    analysis
  };
}

Link to section: Production Deployment ConsiderationsProduction Deployment Considerations

For production deployments, you'll need to configure your observability stack differently depending on your hosting provider. Here's how to set up observability for common deployment targets:

Link to section: Vercel DeploymentVercel Deployment

For Vercel, you'll need to use an external tracing service since you can't run Jaeger locally. Configure your instrumentation for a service like Honeycomb or Jaeger Cloud:

// src/instrumentation.server.js (Vercel production)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
 
const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
  headers: {
    'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
  },
});
 
const sdk = new NodeSDK({
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations()],
});
 
sdk.start();

Add the required environment variables to your Vercel project:

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.honeycomb.io/v1/traces
HONEYCOMB_API_KEY=your_api_key_here

Link to section: Docker DeploymentDocker Deployment

For Docker deployments, create a docker-compose.yml that includes both your application and observability infrastructure:

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:14268/api/traces
    depends_on:
      - jaeger
 
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"
      - "14268:14268"
    environment:
      - COLLECTOR_OTLP_ENABLED=true

Link to section: Monitoring and Alerting SetupMonitoring and Alerting Setup

Once you have traces flowing, set up monitoring and alerting for key metrics. Create a monitoring dashboard that tracks important SvelteKit-specific metrics:

// src/lib/monitoring.js
import { metrics } from '@opentelemetry/api';
 
const meter = metrics.getMeter('sveltekit-app', '1.0.0');
 
// Create custom metrics
const pageLoadDuration = meter.createHistogram('page_load_duration', {
  description: 'Time taken for pages to load',
  unit: 'ms',
});
 
const formSubmissionCount = meter.createCounter('form_submissions_total', {
  description: 'Total number of form submissions',
});
 
const errorCount = meter.createCounter('errors_total', {
  description: 'Total number of errors',
});
 
export function recordPageLoad(route, duration) {
  pageLoadDuration.record(duration, { route });
}
 
export function recordFormSubmission(action, success) {
  formSubmissionCount.add(1, { 
    action, 
    status: success ? 'success' : 'error' 
  });
}
 
export function recordError(error, context) {
  errorCount.add(1, { 
    error_type: error.constructor.name,
    context 
  });
}

This monitoring approach allows you to track performance trends and catch issues before they impact users. Recent framework updates have made this kind of comprehensive monitoring more accessible than ever.

Link to section: Troubleshooting Common IssuesTroubleshooting Common Issues

Several common issues can arise when setting up OpenTelemetry in SvelteKit. Here are the most frequent problems and their solutions:

Missing Traces: If you don't see any traces in Jaeger, verify that the experimental flags are enabled in svelte.config.js and that your instrumentation file is being loaded. Check the console for initialization messages.

Import Hook Errors: The import-in-the-middle package can cause import resolution issues. Ensure you're using the correct version and that the message channel setup matches the example exactly.

Performance Impact: Extensive tracing can impact performance. In production, use sampling to reduce trace volume:

const sdk = new NodeSDK({
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations()],
  sampler: new TraceIdRatioBasedSampler(0.1), // Sample 10% of traces
});

Memory Leaks: Improperly configured span processors can cause memory issues. Always use BatchSpanProcessor in production and ensure proper cleanup on application shutdown.

Link to section: Advanced Patterns and Best PracticesAdvanced Patterns and Best Practices

For complex applications, consider implementing distributed tracing patterns that span multiple services. Use correlation IDs to track requests across service boundaries:

// src/hooks.server.js
import { trace, context, propagation } from '@opentelemetry/api';
import { v4 as uuidv4 } from 'uuid';
 
export async function handle({ event, resolve }) {
  // Generate correlation ID for this request
  const correlationId = event.request.headers.get('x-correlation-id') || uuidv4();
  
  // Extract trace context from incoming request
  const parentContext = propagation.extract(context.active(), event.request.headers);
  
  return context.with(parentContext, async () => {
    const span = trace.getActiveSpan();
    if (span) {
      span.setAttributes({
        'request.correlation_id': correlationId,
        'request.path': event.url.pathname,
        'request.method': event.request.method,
      });
    }
    
    // Add correlation ID to response headers
    const response = await resolve(event);
    response.headers.set('x-correlation-id', correlationId);
    
    return response;
  });
}

This pattern ensures that all operations related to a single user request are properly correlated, making debugging much easier when issues span multiple components or services.

The integration of OpenTelemetry into SvelteKit represents a significant step forward in making production-ready applications easier to monitor and debug. By following this guide, you now have the foundation for comprehensive observability that scales from development through production deployment.

Remember that observability is not just about collecting data, but about making that data actionable. Focus on the metrics that matter most to your users and business objectives, and iterate on your monitoring setup as your application grows and evolves.