Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions dev.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ services:
postgres:
image: postgres:17
container_name: postgres-dev-delegator
profiles:
- core
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand All @@ -33,6 +35,8 @@ services:
# }
mockoidc:
image: ghcr.io/navikt/mock-oauth2-server:2.1.10
profiles:
- core
ports:
- 8080:8080
environment:
Expand Down Expand Up @@ -84,6 +88,8 @@ services:
image: listmonk/listmonk:latest
container_name: listmonk_app
restart: unless-stopped
profiles:
- listmonk
ports:
- '9000:9000' # To change the externally exposed port, change to: $custom_port:9000
hostname: localhost:9000 # Recommend using FQDN for hostname
Expand Down Expand Up @@ -119,6 +125,8 @@ services:
image: postgres:17-alpine
container_name: listmonk_db
restart: unless-stopped
profiles:
- listmonk
ports:
- '5433:5432' # Only bind on the local interface. To connect to Postgres externally, change this to 0.0.0.0
environment:
Expand All @@ -139,6 +147,8 @@ services:
mailpit:
image: axllent/mailpit:latest
container_name: mailpit-dev-delegator
profiles:
- core
ports:
- '1025:1025' # SMTP server
- '8025:8025' # Web UI
Expand All @@ -152,6 +162,8 @@ services:
image: bugsink/bugsink:latest
container_name: bugsink-dev-delegator
restart: unless-stopped
profiles:
- observability
ports:
- '8000:8000'
environment:
Expand All @@ -162,7 +174,25 @@ services:
volumes:
- bugsink-data:/app/data

# Jaeger - Distributed tracing backend
# UI: http://localhost:16686
# OTLP HTTP: http://localhost:4318/v1/traces
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger-dev-delegator
restart: unless-stopped
profiles:
- observability
ports:
- '16686:16686' # Jaeger UI
- '4318:4318' # OTLP HTTP receiver
environment:
COLLECTOR_OTLP_ENABLED: 'true'
volumes:
- jaeger-data:/badger

volumes:
delegator-dev:
listmonk-data:
bugsink-data:
jaeger-data:
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"dev": "concurrently \"bun run dev:server\" \"bun run dev:docker\"",
"dev:docker": "docker compose -f ./dev.docker-compose.yml up",
"dev:docker": "docker compose -f ./dev.docker-compose.yml --profile core up",
"dev:docker:listmonk": "docker compose -f ./dev.docker-compose.yml --profile core --profile listmonk up",
"dev:docker:observability": "docker compose -f ./dev.docker-compose.yml --profile core --profile observability up",
"dev:docker:full": "docker compose -f ./dev.docker-compose.yml --profile core --profile listmonk --profile observability up",
"dev:server": "while true; do svelte-kit sync && vite; echo '🔄 Server crashed, restarting...'; sleep 1; done",
"format": "prettier --write .",
"install-git-hooks": "bunx lefthook install",
Expand Down
109 changes: 60 additions & 49 deletions src/api/resolvers/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { Resource } from '@opentelemetry/resources';
import { configPrivate } from '$config/private';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { trace } from '@opentelemetry/api';
import { trace, context as otelContext } from '@opentelemetry/api';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
import type { Plugin } from 'graphql-yoga';
import {
SimpleSpanProcessor,
Expand All @@ -17,6 +18,7 @@ import {
import { configPublic } from '$config/public';
import { AttributeNames, SpanNames } from '@pothos/tracing-opentelemetry';
import { print } from 'graphql';
import { building } from '$app/environment';

class PrettyConsoleSpanExporter implements SpanExporter {
export(spans: ReadableSpan[], resultCallback: (result: any) => void): void {
Expand All @@ -35,68 +37,77 @@ class PrettyConsoleSpanExporter implements SpanExporter {
}
}

// Create fallback console exporter
const consoleExporter = new PrettyConsoleSpanExporter();
const fallbackProcessor = new SimpleSpanProcessor(consoleExporter);
// Only initialize OpenTelemetry at runtime, not during build
if (!building) {
// Create fallback console exporter
const consoleExporter = new PrettyConsoleSpanExporter();
const fallbackProcessor = new SimpleSpanProcessor(consoleExporter);

let activeProcessor: SpanProcessor | undefined;
let activeProcessor: SpanProcessor | undefined;

try {
if (configPrivate.OTEL_ENDPOINT_URL) {
const otlpExporter = new OTLPTraceExporter({
url: configPrivate.OTEL_ENDPOINT_URL,
headers: configPrivate.OTEL_AUTHORIZATION_HEADER
? { authorization: configPrivate.OTEL_AUTHORIZATION_HEADER }
: {},
timeoutMillis: 5000, // 5 second timeout
concurrencyLimit: 10 // Limit concurrent exports
});
try {
if (configPrivate.OTEL_ENDPOINT_URL) {
const otlpExporter = new OTLPTraceExporter({
url: configPrivate.OTEL_ENDPOINT_URL,
headers: configPrivate.OTEL_AUTHORIZATION_HEADER
? { authorization: configPrivate.OTEL_AUTHORIZATION_HEADER }
: {},
timeoutMillis: 5000, // 5 second timeout
concurrencyLimit: 10 // Limit concurrent exports
});

// Configure processor based on environment
activeProcessor =
configPrivate.NODE_ENV === 'production'
? new BatchSpanProcessor(otlpExporter, {
maxQueueSize: 2000,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 5000
})
: new SimpleSpanProcessor(otlpExporter);

// Configure processor based on environment
activeProcessor =
configPrivate.NODE_ENV === 'production'
? new BatchSpanProcessor(otlpExporter, {
maxQueueSize: 2000,
scheduledDelayMillis: 5000,
exportTimeoutMillis: 5000
})
: new SimpleSpanProcessor(otlpExporter);
} else {
console.info('No OTEL exporter configured, using console logging fallback');
console.info(
`OpenTelemetry initialized with OTLP exporter: ${configPrivate.OTEL_ENDPOINT_URL}`
);
} else {
console.info('No OTEL exporter configured, using console logging fallback');
activeProcessor = fallbackProcessor;
}
} catch (error) {
console.warn('Failed to initialize OTLP exporter, using console logging fallback:', error);
activeProcessor = fallbackProcessor;
}
} catch (error) {
console.warn('Failed to initialize OTLP exporter, using console logging fallback:', error);
activeProcessor = fallbackProcessor;
}

const provider = new NodeTracerProvider({
resource: new Resource({
[ATTR_SERVICE_NAME]: configPrivate.OTEL_SERVICE_NAME,
[ATTR_SERVICE_VERSION]: configPrivate.OTEL_SERVICE_VERSION ?? configPublic.PUBLIC_VERSION ?? 'unknown'
})
});

// Always add fallback processor first
provider.addSpanProcessor(fallbackProcessor);
const provider = new NodeTracerProvider({
resource: new Resource({
[ATTR_SERVICE_NAME]: configPrivate.OTEL_SERVICE_NAME,
[ATTR_SERVICE_VERSION]:
configPrivate.OTEL_SERVICE_VERSION ?? configPublic.PUBLIC_VERSION ?? 'unknown'
})
});

// Add OTLP processor if available and different from fallback
if (activeProcessor && activeProcessor !== fallbackProcessor) {
provider.addSpanProcessor(activeProcessor);
}
// Add the active processor (either OTLP or console fallback, not both)
if (activeProcessor) {
provider.addSpanProcessor(activeProcessor);
}

registerInstrumentations({
tracerProvider: provider,
instrumentations: [new HttpInstrumentation()]
});
registerInstrumentations({
tracerProvider: provider,
instrumentations: [new HttpInstrumentation()]
});

provider.register();
provider.register({
propagator: new W3CTraceContextPropagator()
});
}

export const tracer = trace.getTracer('graphql');

export const graphqlYogaTracerPlugin: Plugin = {
onExecute: ({ setExecuteFn, executeFn }) => {
setExecuteFn((options) => {
const activeContext = otelContext.active();

return tracer.startActiveSpan(
SpanNames.EXECUTE,
{
Expand All @@ -105,10 +116,10 @@ export const graphqlYogaTracerPlugin: Plugin = {
[AttributeNames.SOURCE]: print(options.document)
}
},
activeContext, // Link to parent HTTP span
async (span) => {
try {
const result = await executeFn(options);

return result;
} catch (error) {
span.recordException(error as Error);
Expand Down
4 changes: 4 additions & 0 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Initialize OpenTelemetry tracing FIRST - before any other imports that make HTTP connections
// This ensures HttpInstrumentation can properly patch the http modules
import '$api/resolvers/tracer';

import * as Sentry from '@sentry/sveltekit';
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
Expand Down