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.

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.