Skip to content

feat: Initial tracing setup (peer deps + utils) #13899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"sirv": "^3.0.0"
},
"devDependencies": {
"@opentelemetry/api": "^1.0.0",
"@playwright/test": "catalog:",
"@sveltejs/vite-plugin-svelte": "catalog:",
"@types/connect": "^3.4.38",
Expand All @@ -47,9 +48,15 @@
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0",
"@opentelemetry/api": "^1.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
}
},
"bin": {
"svelte-kit": "svelte-kit.js"
},
Expand Down
27 changes: 27 additions & 0 deletions packages/kit/src/runtime/telemetry/get_tracer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/** @import { Tracer } from '@opentelemetry/api' */
import { DEV } from 'esm-env';
import { noop_tracer } from './noop.js';
import { load_otel } from './load_otel.js';

/**
* @param {Object} [options={}] - Configuration options
* @param {boolean} [options.is_enabled=false] - Whether tracing is enabled
* @returns {Promise<Tracer>} The tracer instance
*/
export async function get_tracer({ is_enabled = false } = {}) {
if (!is_enabled) {
return noop_tracer;
}

const otel = await load_otel();
if (otel === null) {
if (DEV) {
console.warn(
'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?'
);
}
return noop_tracer;
}

return otel.tracer;
}
31 changes: 31 additions & 0 deletions packages/kit/src/runtime/telemetry/get_tracer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, test, expect, beforeEach, vi } from 'vitest';
import { get_tracer } from './get_tracer.js';
import { noop_tracer } from './noop.js';
import * as load_otel from './load_otel.js';

describe('get_tracer', () => {
beforeEach(() => {
vi.resetAllMocks();
});

test('returns noop tracer if tracing is disabled', async () => {
const tracer = await get_tracer({ is_enabled: false });
expect(tracer).toBe(noop_tracer);
});

test('returns noop tracer if @opentelemetry/api is not installed, warning', async () => {
vi.spyOn(load_otel, 'load_otel').mockResolvedValue(null);
const console_warn_spy = vi.spyOn(console, 'warn');

const tracer = await get_tracer({ is_enabled: true });
expect(tracer).toBe(noop_tracer);
expect(console_warn_spy).toHaveBeenCalledWith(
'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?'
);
});

test('returns otel tracer if @opentelemetry/api is installed', async () => {
const tracer = await get_tracer({ is_enabled: true });
expect(tracer).not.toBe(noop_tracer);
});
});
18 changes: 18 additions & 0 deletions packages/kit/src/runtime/telemetry/load_otel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** @import { Tracer, SpanStatusCode } from '@opentelemetry/api' */

/** @type {Promise<{ tracer: Tracer, SpanStatusCode: typeof SpanStatusCode } | null> | null} */
let otel_result = null;

export function load_otel() {
if (otel_result) return otel_result;
otel_result = import('@opentelemetry/api')
.then((module) => {
const { trace, SpanStatusCode } = module;
return {
tracer: trace.getTracer('sveltekit'),
SpanStatusCode
};
})
.catch(() => null);
return otel_result;
}
81 changes: 81 additions & 0 deletions packages/kit/src/runtime/telemetry/noop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/** @import { Tracer, Span, SpanContext } from '@opentelemetry/api' */

/**
* Tracer implementation that does nothing (null object).
* @type {Tracer}
*/
export const noop_tracer = {
/**
* @returns {Span}
*/
startSpan() {
return noop_span;
},

/**
* @param {unknown} _name
* @param {unknown} arg_1
* @param {unknown} [arg_2]
* @param {Function} [arg_3]
* @returns {unknown}
*/
startActiveSpan(_name, arg_1, arg_2, arg_3) {
if (typeof arg_1 === 'function') {
return arg_1(noop_span);
}
if (typeof arg_2 === 'function') {
return arg_2(noop_span);
}
if (typeof arg_3 === 'function') {
return arg_3(noop_span);
}
}
};

/**
* @type {Span}
*/
export const noop_span = {
spanContext() {
return noop_span_context;
},
setAttribute() {
return this;
},
setAttributes() {
return this;
},
addEvent() {
return this;
},
setStatus() {
return this;
},
updateName() {
return this;
},
end() {
return this;
},
isRecording() {
return false;
},
recordException() {
return this;
},
addLink() {
return this;
},
addLinks() {
return this;
}
};

/**
* @type {SpanContext}
*/
const noop_span_context = {
traceId: '',
spanId: '',
traceFlags: 0
};
75 changes: 75 additions & 0 deletions packages/kit/src/runtime/telemetry/record_span.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/** @import { Attributes, Span, Tracer } from '@opentelemetry/api' */
import { HttpError, Redirect } from '@sveltejs/kit/internal';
import { load_otel } from './load_otel.js';
import { noop_span } from './noop.js';

/**
* @template T
* @param {Object} options
* @param {string} options.name
* @param {Tracer} options.tracer
* @param {Attributes} options.attributes
* @param {function(Span): Promise<T>} options.fn
* @returns {Promise<T>}
*/
export async function record_span({ name, tracer, attributes, fn }) {
const otel = await load_otel();
if (otel === null) {
return fn(noop_span);
}

const { SpanStatusCode } = otel;

return tracer.startActiveSpan(name, { attributes }, async (span) => {
try {
const result = await fn(span);
span.end();
return result;
} catch (error) {
if (error instanceof HttpError) {
span.setAttributes({
[`${name}.result.type`]: 'known_error',
[`${name}.result.status`]: error.status,
[`${name}.result.message`]: error.body.message
});
if (error.status >= 500) {
span.recordException({
name: 'HttpError',
message: error.body.message
});
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.body.message
});
}
} else if (error instanceof Redirect) {
span.setAttributes({
[`${name}.result.type`]: 'redirect',
[`${name}.result.status`]: error.status,
[`${name}.result.location`]: error.location
});
} else if (error instanceof Error) {
span.setAttributes({
[`${name}.result.type`]: 'unknown_error'
});
span.recordException({
name: error.name,
message: error.message,
stack: error.stack
});
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
} else {
span.setAttributes({
[`${name}.result.type`]: 'unknown_error'
});
span.setStatus({ code: SpanStatusCode.ERROR });
}
span.end();

throw error;
}
});
}
Loading
Loading