Skip to content

feat: Native tracing #13892

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

Closed
wants to merge 5 commits into from
Closed
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 @@ -33,6 +33,7 @@
"vitefu": "^1.0.6"
},
"devDependencies": {
"@opentelemetry/api": "^1.0.0",
"@playwright/test": "catalog:",
"@sveltejs/vite-plugin-svelte": "^5.0.1",
"@types/connect": "^3.4.38",
Expand All @@ -47,10 +48,16 @@
"vitest": "catalog:"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3 || ^6.0.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
}
},
"bin": {
"svelte-kit": "svelte-kit.js"
},
Expand Down
23 changes: 21 additions & 2 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { get_message, get_status } from '../../utils/error.js';
import { writable } from 'svelte/store';
import { page, update, navigating } from './state.svelte.js';
import { add_data_suffix, add_resolution_suffix } from '../pathname.js';
import { record_span } from '../server/telemetry/record-span.js';
import { get_tracer } from '../server/telemetry/get-tracer.js';

export { load_css };

Expand Down Expand Up @@ -753,10 +755,27 @@ async function load_node({ loader, parent, url, params, route, server_data_node
}
};

async function traced_load() {
const tracer = get_tracer({ is_enabled: true }); // TODO: Make this configurable

return record_span({
name: 'sveltekit.load.universal',
tracer,
attributes: {
'sveltekit.load.node_id': 'client-load', // TODO: can we actually get an id here?
'sveltekit.load.type': 'universal',
'sveltekit.load.environment': 'client',
'sveltekit.route.id': route.id || 'unknown'
},
fn: async () => (await node.universal?.load?.call(null, load_input)) ?? null
});
}

if (DEV) {
try {
lock_fetch();
data = (await node.universal.load.call(null, load_input)) ?? null;
data = await traced_load();

if (data != null && Object.getPrototypeOf(data) !== Object.prototype) {
throw new Error(
`a load function related to route '${route.id}' returned ${
Expand All @@ -774,7 +793,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
unlock_fetch();
}
} else {
data = (await node.universal.load.call(null, load_input)) ?? null;
data = await traced_load();
}
}

Expand Down
27 changes: 26 additions & 1 deletion packages/kit/src/runtime/server/page/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { is_form_content_type, negotiate } from '../../../utils/http.js';
import { HttpError, Redirect, ActionFailure, SvelteKitError } from '../../control.js';
import { handle_error_and_jsonify } from '../utils.js';
import { with_event } from '../../app/server/event.js';
import { record_span } from '../telemetry/record-span.js';
import { get_tracer } from '../telemetry/get-tracer.js';

/** @param {import('@sveltejs/kit').RequestEvent} event */
export function is_action_json_request(event) {
Expand Down Expand Up @@ -247,7 +249,30 @@ async function call_action(event, actions) {
);
}

return with_event(event, () => action(event));
const tracer = get_tracer({ is_enabled: true }); // TODO: Make this configurable

return record_span({
name: 'sveltekit.action',
tracer,
attributes: {
'sveltekit.action.name': name,
'sveltekit.route.id': event.route.id || 'unknown'
},
fn: async (action_span) => {
const result = await with_event(event, () => action(event));
if (result instanceof ActionFailure) {
action_span.setAttributes({
'sveltekit.action.result.type': 'failure',
'sveltekit.action.result.status': result.status
});
} else {
action_span.setAttributes({
'sveltekit.action.result.type': 'success'
});
}
return result;
}
});
}

/** @param {any} data */
Expand Down
215 changes: 125 additions & 90 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { disable_search, make_trackable } from '../../../utils/url.js';
import { validate_depends } from '../../shared.js';
import { b64_encode } from '../../utils.js';
import { with_event } from '../../app/server/event.js';
import { record_span } from '../telemetry/record-span.js';
import { get_tracer } from '../telemetry/get-tracer.js';

/**
* Calls the user's server `load` function.
Expand Down Expand Up @@ -68,94 +70,109 @@ export async function load_server_data({ event, state, node, parent }) {

let done = false;

const result = await with_event(event, () =>
load.call(null, {
...event,
fetch: (info, init) => {
const url = new URL(info instanceof Request ? info.url : info, event.url);
const tracer = get_tracer({ is_enabled: true }); // TODO: Make this configurable

if (DEV && done && !uses.dependencies.has(url.href)) {
console.warn(
`${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated`
);
}

// Note: server fetches are not added to uses.depends due to security concerns
return event.fetch(info, init);
},
/** @param {string[]} deps */
depends: (...deps) => {
for (const dep of deps) {
const { href } = new URL(dep, event.url);

if (DEV) {
validate_depends(node.server_id || 'missing route ID', dep);

if (done && !uses.dependencies.has(href)) {
const result = await record_span({
name: 'sveltekit.load.server',
tracer,
attributes: {
'sveltekit.load.node_id': node.server_id || 'unknown',
'sveltekit.load.type': 'server',
'sveltekit.route.id': event.route.id || 'unknown'
},
fn: async () => {
const result = await with_event(event, () =>
load.call(null, {
...event,
fetch: (info, init) => {
const url = new URL(info instanceof Request ? info.url : info, event.url);

if (DEV && done && !uses.dependencies.has(url.href)) {
console.warn(
`${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated`
`${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated`
);
}
}

uses.dependencies.add(href);
}
},
params: new Proxy(event.params, {
get: (target, key) => {
if (DEV && done && typeof key === 'string' && !uses.params.has(key)) {
console.warn(
`${node.server_id}: Accessing \`params.${String(
key
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes`
);
}

if (is_tracking) {
uses.params.add(key);
}
return target[/** @type {string} */ (key)];
}
}),
parent: async () => {
if (DEV && done && !uses.parent) {
console.warn(
`${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes`
);
}
// Note: server fetches are not added to uses.depends due to security concerns
return event.fetch(info, init);
},
/** @param {string[]} deps */
depends: (...deps) => {
for (const dep of deps) {
const { href } = new URL(dep, event.url);

if (DEV) {
validate_depends(node.server_id || 'missing route ID', dep);

if (done && !uses.dependencies.has(href)) {
console.warn(
`${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated`
);
}
}

uses.dependencies.add(href);
}
},
params: new Proxy(event.params, {
get: (target, key) => {
if (DEV && done && typeof key === 'string' && !uses.params.has(key)) {
console.warn(
`${node.server_id}: Accessing \`params.${String(
key
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes`
);
}

if (is_tracking) {
uses.params.add(key);
}
return target[/** @type {string} */ (key)];
}
}),
parent: async () => {
if (DEV && done && !uses.parent) {
console.warn(
`${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes`
);
}

if (is_tracking) {
uses.parent = true;
}
return parent();
},
route: new Proxy(event.route, {
get: (target, key) => {
if (DEV && done && typeof key === 'string' && !uses.route) {
console.warn(
`${node.server_id}: Accessing \`route.${String(
key
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes`
);
if (is_tracking) {
uses.parent = true;
}
return parent();
},
route: new Proxy(event.route, {
get: (target, key) => {
if (DEV && done && typeof key === 'string' && !uses.route) {
console.warn(
`${node.server_id}: Accessing \`route.${String(
key
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes`
);
}

if (is_tracking) {
uses.route = true;
}
return target[/** @type {'id'} */ (key)];
}
}),
url,
untrack(fn) {
is_tracking = false;
try {
return fn();
} finally {
is_tracking = true;
}
}
})
);

if (is_tracking) {
uses.route = true;
}
return target[/** @type {'id'} */ (key)];
}
}),
url,
untrack(fn) {
is_tracking = false;
try {
return fn();
} finally {
is_tracking = true;
}
}
})
);
return result;
}
});

if (__SVELTEKIT_DEV__) {
validate_load_response(result, node.server_id);
Expand Down Expand Up @@ -201,16 +218,34 @@ export async function load_data({
return server_data_node?.data ?? null;
}

const result = await node.universal.load.call(null, {
url: event.url,
params: event.params,
data: server_data_node?.data ?? null,
route: event.route,
fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts),
setHeaders: event.setHeaders,
depends: () => {},
parent,
untrack: (fn) => fn()
const { load } = node.universal;

const tracer = get_tracer({ is_enabled: true }); // TODO: Make this configurable

const result = await record_span({
name: 'sveltekit.load.universal',
tracer,
attributes: {
'sveltekit.load.node_id': node.universal_id || 'unknown',
'sveltekit.load.type': 'universal',
'sveltekit.load.environment': 'client',
'sveltekit.route.id': event.route.id || 'unknown'
},
fn: async () => {
const result = await load.call(null, {
url: event.url,
params: event.params,
data: server_data_node?.data ?? null,
route: event.route,
fetch: create_universal_fetch(event, state, fetched, csr, resolve_opts),
setHeaders: event.setHeaders,
depends: () => {},
parent,
untrack: (fn) => fn()
});

return result;
}
});

if (__SVELTEKIT_DEV__) {
Expand Down
Loading
Loading