diff --git a/package.json b/package.json index e36ec2939797..c50d3c8c8e70 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@parcel/watcher", "esbuild", "netlify-cli", + "protobufjs", "rolldown", "sharp", "svelte-preprocess", diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 419f30416d9c..d1a954ac2f87 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -76,6 +76,9 @@ const get_defaults = (prefix = '') => ({ publicPrefix: 'PUBLIC_', privatePrefix: '' }, + experimental: { + tracing: undefined + }, files: { assets: join(prefix, 'static'), hooks: { @@ -404,3 +407,60 @@ test('errors on loading config with incorrect default export', async () => { 'The Svelte config file must have a configuration object as its default export. See https://svelte.dev/docs/kit/configuration' ); }); + +test('accepts valid tracing values', () => { + assert.doesNotThrow(() => { + validate_config({ + kit: { + experimental: { + tracing: 'server' + } + } + }); + }); + + assert.doesNotThrow(() => { + validate_config({ + kit: { + experimental: { + tracing: undefined + } + } + }); + }); +}); + +test('errors on invalid tracing values', () => { + assert.throws(() => { + validate_config({ + kit: { + experimental: { + // @ts-expect-error - given value expected to throw + tracing: true + } + } + }); + }, /^config\.kit\.experimental\.tracing should be undefined or "server"$/); + + assert.throws(() => { + validate_config({ + kit: { + experimental: { + // @ts-expect-error - given value expected to throw + tracing: false + } + } + }); + }, /^config\.kit\.experimental\.tracing should be undefined or "server"$/); + + assert.throws(() => { + validate_config({ + kit: { + experimental: { + // @ts-expect-error - given value expected to throw + tracing: 'client' + } + } + }); + }, /^config\.kit\.experimental\.tracing should be undefined or "server"$/); +}); diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index a2b9bb81759d..cd937a5af528 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -120,6 +120,15 @@ const options = object( privatePrefix: string('') }), + experimental: object({ + tracing: validate(undefined, (input, keypath) => { + if (input !== 'server') { + throw new Error(`${keypath} should be undefined or "server"`); + } + return input; + }) + }), + files: object({ assets: string('static'), hooks: object({ diff --git a/packages/kit/src/core/sync/write_server.js b/packages/kit/src/core/sync/write_server.js index 5e93d5c1cd25..a2d6f3b99eb3 100644 --- a/packages/kit/src/core/sync/write_server.js +++ b/packages/kit/src/core/sync/write_server.js @@ -33,6 +33,11 @@ import { set_building, set_prerendering } from '__sveltekit/environment'; import { set_assets } from '__sveltekit/paths'; import { set_manifest, set_read_implementation } from '__sveltekit/server'; import { set_private_env, set_public_env, set_safe_public_env } from '${runtime_directory}/shared-server.js'; +import { get_tracer, enable_tracing } from '${runtime_directory}/telemetry/get_tracer.js'; + +if (${s(config.kit.experimental.tracing === 'server')}) { + enable_tracing(); +} export const options = { app_template_contains_nonce: ${template.includes('%sveltekit.nonce%')}, @@ -60,6 +65,7 @@ export const options = { .replace(/%sveltekit\.status%/g, '" + status + "') .replace(/%sveltekit\.error\.message%/g, '" + message + "')} }, + tracer: get_tracer(), version_hash: ${s(hash(config.kit.version.name))} }; diff --git a/packages/kit/src/exports/hooks/sequence.js b/packages/kit/src/exports/hooks/sequence.js index 2c0fc3460007..443be6c24cd4 100644 --- a/packages/kit/src/exports/hooks/sequence.js +++ b/packages/kit/src/exports/hooks/sequence.js @@ -1,3 +1,9 @@ +/** @import { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit' */ +/** @import { MaybePromise } from 'types' */ +import { with_event } from '../../runtime/app/server/event.js'; +import { get_tracer } from '../../runtime/telemetry/get_tracer.js'; +import { record_span } from '../../runtime/telemetry/record_span.js'; + /** * A helper function for sequencing multiple `handle` calls in a middleware-like manner. * The behavior for the `handle` options is as follows: @@ -66,56 +72,77 @@ * first post-processing * ``` * - * @param {...import('@sveltejs/kit').Handle} handlers The chain of `handle` functions - * @returns {import('@sveltejs/kit').Handle} + * @param {...Handle} handlers The chain of `handle` functions + * @returns {Handle} */ export function sequence(...handlers) { const length = handlers.length; if (!length) return ({ event, resolve }) => resolve(event); - return ({ event, resolve }) => { + return async ({ event, resolve }) => { + // there's an assumption here that people aren't doing something insane like sequence(() => {}, sequence(() => {})) + // worst case there is that future spans get a lower-down span as their root span -- the tracing would still work, + // it'd just look a little weird + const { rootSpan } = event.tracing; + const tracer = await get_tracer(); return apply_handle(0, event, {}); /** * @param {number} i - * @param {import('@sveltejs/kit').RequestEvent} event - * @param {import('@sveltejs/kit').ResolveOptions | undefined} parent_options - * @returns {import('types').MaybePromise} + * @param {RequestEvent} event + * @param {ResolveOptions | undefined} parent_options + * @returns {MaybePromise} */ function apply_handle(i, event, parent_options) { const handle = handlers[i]; - return handle({ - event, - resolve: (event, options) => { - /** @type {import('@sveltejs/kit').ResolveOptions['transformPageChunk']} */ - const transformPageChunk = async ({ html, done }) => { - if (options?.transformPageChunk) { - html = (await options.transformPageChunk({ html, done })) ?? ''; - } + return record_span({ + tracer, + name: 'sveltekit.handle.child', + attributes: { + 'sveltekit.handle.child.index': i + }, + fn: async (span) => { + const traced_event = { ...event, tracing: { rootSpan, currentSpan: span } }; + return await with_event(traced_event, () => + handle({ + event: traced_event, + resolve: (event, options) => { + /** @type {ResolveOptions['transformPageChunk']} */ + const transformPageChunk = async ({ html, done }) => { + if (options?.transformPageChunk) { + html = (await options.transformPageChunk({ html, done })) ?? ''; + } - if (parent_options?.transformPageChunk) { - html = (await parent_options.transformPageChunk({ html, done })) ?? ''; - } + if (parent_options?.transformPageChunk) { + html = (await parent_options.transformPageChunk({ html, done })) ?? ''; + } - return html; - }; + return html; + }; - /** @type {import('@sveltejs/kit').ResolveOptions['filterSerializedResponseHeaders']} */ - const filterSerializedResponseHeaders = - parent_options?.filterSerializedResponseHeaders ?? - options?.filterSerializedResponseHeaders; + /** @type {ResolveOptions['filterSerializedResponseHeaders']} */ + const filterSerializedResponseHeaders = + parent_options?.filterSerializedResponseHeaders ?? + options?.filterSerializedResponseHeaders; - /** @type {import('@sveltejs/kit').ResolveOptions['preload']} */ - const preload = parent_options?.preload ?? options?.preload; + /** @type {ResolveOptions['preload']} */ + const preload = parent_options?.preload ?? options?.preload; - return i < length - 1 - ? apply_handle(i + 1, event, { - transformPageChunk, - filterSerializedResponseHeaders, - preload - }) - : resolve(event, { transformPageChunk, filterSerializedResponseHeaders, preload }); + return i < length - 1 + ? apply_handle(i + 1, event, { + transformPageChunk, + filterSerializedResponseHeaders, + preload + }) + : resolve(event, { + transformPageChunk, + filterSerializedResponseHeaders, + preload + }); + } + }) + ); } }); } diff --git a/packages/kit/src/exports/hooks/sequence.spec.js b/packages/kit/src/exports/hooks/sequence.spec.js index 0829e90a92e6..79a9eee95aa7 100644 --- a/packages/kit/src/exports/hooks/sequence.spec.js +++ b/packages/kit/src/exports/hooks/sequence.spec.js @@ -4,6 +4,10 @@ import { installPolyfills } from '../node/polyfills.js'; installPolyfills(); +const dummy_event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({ + tracing: { rootSpan: {} } +}); + test('applies handlers in sequence', async () => { /** @type {string[]} */ const order = []; @@ -29,10 +33,9 @@ test('applies handlers in sequence', async () => { } ); - const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({}); const response = new Response(); - assert.equal(await handler({ event, resolve: () => response }), response); + assert.equal(await handler({ event: dummy_event, resolve: () => response }), response); expect(order).toEqual(['1a', '2a', '3a', '3b', '2b', '1b']); }); @@ -47,9 +50,8 @@ test('uses transformPageChunk option passed to non-terminal handle function', as async ({ event, resolve }) => resolve(event) ); - const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({}); const response = await handler({ - event, + event: dummy_event, resolve: async (_event, opts = {}) => { let html = ''; @@ -84,9 +86,8 @@ test('merges transformPageChunk option', async () => { } ); - const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({}); const response = await handler({ - event, + event: dummy_event, resolve: async (_event, opts = {}) => { let html = ''; @@ -117,9 +118,8 @@ test('uses first defined preload option', async () => { } ); - const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({}); const response = await handler({ - event, + event: dummy_event, resolve: (_event, opts = {}) => { let html = ''; @@ -150,9 +150,8 @@ test('uses first defined filterSerializedResponseHeaders option', async () => { } ); - const event = /** @type {import('@sveltejs/kit').RequestEvent} */ ({}); const response = await handler({ - event, + event: dummy_event, resolve: (_event, opts = {}) => { let html = ''; diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index afc5f1d6d450..d98f1fb11786 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -18,6 +18,7 @@ import { } from '../types/private.js'; import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from 'types'; import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; +import { Span } from '@opentelemetry/api'; export { PrerenderOption } from '../types/private.js'; @@ -401,6 +402,15 @@ export interface KitConfig { */ privatePrefix?: string; }; + /** Experimental features. Here be dragons. Breaking changes may occur in minor releases. */ + experimental?: { + /** + * Whether to enable server-side [OpenTelemetry](https://opentelemetry.io/) tracing for SvelteKit operations including the [`handle` hook](https://svelte.dev/docs/kit/hooks#Server-hooks-handle), [`load` functions](https://svelte.dev/docs/kit/load), and [form actions](https://svelte.dev/docs/kit/form-actions). + * @default undefined + * @since 2.26.0 // TODO: update this before publishing + */ + tracing?: 'server'; + }; /** * Where to find various files within your project. */ @@ -967,6 +977,17 @@ export interface LoadEvent< * ``` */ untrack: (fn: () => T) => T; + + /** + * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing.\ + * @since 2.26.0 // TODO: update this before publishing + */ + tracing: { + /** The root span for the request. This span is named `sveltekit.handle.root`. */ + rootSpan: Span; + /** The span associated with the current `load` function. */ + currentSpan: Span; + }; } export interface NavigationEvent< @@ -1242,6 +1263,17 @@ export interface RequestEvent< * `true` for `+server.js` calls coming from SvelteKit without the overhead of actually making an HTTP request. This happens when you make same-origin `fetch` requests on the server. */ isSubRequest: boolean; + + /** + * Access to spans for tracing. If tracing is not enabled, these spans will do nothing. + * @since 2.26.0 // TODO: update this before publishing + */ + tracing: { + /** The root span for the request. This span is named `sveltekit.handle.root`. */ + rootSpan: Span; + /** The span associated with the current `handle` hook, `load` function, or form action. */ + currentSpan: Span; + }; } /** @@ -1398,6 +1430,17 @@ export interface ServerLoadEvent< * ``` */ untrack: (fn: () => T) => T; + + /** + * Access to spans for tracing. If tracing is not enabled, these spans will do nothing. + * @since 2.26.0 // TODO: update this before publishing + */ + tracing: { + /** The root span for the request. This span is named `sveltekit.handle.root`. */ + rootSpan: Span; + /** The span associated with the current server `load` function. */ + currentSpan: Span; + }; } /** diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 6e027cb57677..d70003bf9c25 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -39,11 +39,17 @@ import { } from './constants.js'; import { validate_page_exports } from '../../utils/exports.js'; import { compact } from '../../utils/array.js'; -import { INVALIDATED_PARAM, TRAILING_SLASH_PARAM, validate_depends } from '../shared.js'; +import { + INVALIDATED_PARAM, + TRAILING_SLASH_PARAM, + validate_depends, + validate_load_response +} from '../shared.js'; 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 { noop_span } from '../telemetry/noop.js'; export { load_css }; @@ -670,6 +676,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node /** @type {import('@sveltejs/kit').LoadEvent} */ const load_input = { + tracing: { rootSpan: noop_span, currentSpan: noop_span }, route: new Proxy(route, { get: (target, key) => { if (is_tracking) { @@ -762,19 +769,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node try { lock_fetch(); data = (await node.universal.load.call(null, load_input)) ?? null; - if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { - throw new Error( - `a load function related to route '${route.id}' returned ${ - typeof data !== 'object' - ? `a ${typeof data}` - : data instanceof Response - ? 'a Response object' - : Array.isArray(data) - ? 'an array' - : 'a non-plain object' - }, but must return a plain object at the top level (i.e. \`return {...}\`)` - ); - } + validate_load_response(data, `related to route '${route.id}'`); } finally { unlock_fetch(); } diff --git a/packages/kit/src/runtime/server/data/index.js b/packages/kit/src/runtime/server/data/index.js index 0c5d8f1d0ecf..c63fc8654256 100644 --- a/packages/kit/src/runtime/server/data/index.js +++ b/packages/kit/src/runtime/server/data/index.js @@ -63,6 +63,7 @@ export async function render_data( event: new_event, state, node, + tracer: await options.tracer, parent: async () => { /** @type {Record} */ const data = {}; diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index bd44060bfd2a..3db90d24d740 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -1,3 +1,6 @@ +/** @import { Tracer } from '@opentelemetry/api' */ +/** @import { RequestEvent, ActionResult, Actions } from '@sveltejs/kit' */ +/** @import { SSROptions, SSRNode, ServerNode, ServerHooks } from 'types' */ import * as devalue from 'devalue'; import { DEV } from 'esm-env'; import { json } from '@sveltejs/kit'; @@ -6,8 +9,9 @@ import { get_status, normalize_error } from '../../../utils/error.js'; import { is_form_content_type, negotiate } from '../../../utils/http.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'; -/** @param {import('@sveltejs/kit').RequestEvent} event */ +/** @param {RequestEvent} event */ export function is_action_json_request(event) { const accept = negotiate(event.request.headers.get('accept') ?? '*/*', [ 'application/json', @@ -18,9 +22,9 @@ export function is_action_json_request(event) { } /** - * @param {import('@sveltejs/kit').RequestEvent} event - * @param {import('types').SSROptions} options - * @param {import('types').SSRNode['server'] | undefined} server + * @param {RequestEvent} event + * @param {SSROptions} options + * @param {SSRNode['server'] | undefined} server */ export async function handle_action_json_request(event, options, server) { const actions = server?.actions; @@ -51,7 +55,7 @@ export async function handle_action_json_request(event, options, server) { check_named_default_separate(actions); try { - const data = await call_action(event, actions); + const data = await call_action(event, actions, await options.tracer); if (__SVELTEKIT_DEV__) { validate_action_return(data); @@ -111,7 +115,7 @@ function check_incorrect_fail_use(error) { } /** - * @param {import('@sveltejs/kit').Redirect} redirect + * @param {Redirect} redirect */ export function action_json_redirect(redirect) { return action_json({ @@ -122,7 +126,7 @@ export function action_json_redirect(redirect) { } /** - * @param {import('@sveltejs/kit').ActionResult} data + * @param {ActionResult} data * @param {ResponseInit} [init] */ function action_json(data, init) { @@ -130,18 +134,19 @@ function action_json(data, init) { } /** - * @param {import('@sveltejs/kit').RequestEvent} event + * @param {RequestEvent} event */ export function is_action_request(event) { return event.request.method === 'POST'; } /** - * @param {import('@sveltejs/kit').RequestEvent} event - * @param {import('types').SSRNode['server'] | undefined} server - * @returns {Promise} + * @param {RequestEvent} event + * @param {SSRNode['server'] | undefined} server + * @param {Tracer} tracer + * @returns {Promise} */ -export async function handle_action_request(event, server) { +export async function handle_action_request(event, server, tracer) { const actions = server?.actions; if (!actions) { @@ -164,7 +169,7 @@ export async function handle_action_request(event, server) { check_named_default_separate(actions); try { - const data = await call_action(event, actions); + const data = await call_action(event, actions, tracer); if (__SVELTEKIT_DEV__) { validate_action_return(data); @@ -203,7 +208,7 @@ export async function handle_action_request(event, server) { } /** - * @param {import('@sveltejs/kit').Actions} actions + * @param {Actions} actions */ function check_named_default_separate(actions) { if (actions.default && Object.keys(actions).length > 1) { @@ -214,11 +219,12 @@ function check_named_default_separate(actions) { } /** - * @param {import('@sveltejs/kit').RequestEvent} event - * @param {NonNullable} actions + * @param {RequestEvent} event + * @param {NonNullable} actions + * @param {Tracer} tracer * @throws {Redirect | HttpError | SvelteKitError | Error} */ -async function call_action(event, actions) { +async function call_action(event, actions, tracer) { const url = new URL(event.request.url); let name = 'default'; @@ -247,7 +253,32 @@ async function call_action(event, actions) { ); } - return with_event(event, () => action(event)); + return record_span({ + name: 'sveltekit.action', + tracer, + attributes: { + 'sveltekit.action.name': name, + 'http.route': event.route.id || 'unknown' + }, + fn: async (action_span) => { + const traced_event = { + ...event, + tracing: { + rootSpan: event.tracing.rootSpan, + currentSpan: action_span + } + }; + const result = await with_event(traced_event, () => action(traced_event)); + if (result instanceof ActionFailure) { + action_span.setAttributes({ + 'sveltekit.action.result.type': 'failure', + 'sveltekit.action.result.status': result.status + }); + } + + return result; + } + }); } /** @param {any} data */ @@ -265,7 +296,7 @@ function validate_action_return(data) { * Try to `devalue.uneval` the data object, and if it fails, return a proper Error with context * @param {any} data * @param {string} route_id - * @param {import('types').ServerHooks['transport']} transport + * @param {ServerHooks['transport']} transport */ export function uneval_action_response(data, route_id, transport) { const replacer = (/** @type {any} */ thing) => { @@ -284,7 +315,7 @@ export function uneval_action_response(data, route_id, transport) { * Try to `devalue.stringify` the data object, and if it fails, return a proper Error with context * @param {any} data * @param {string} route_id - * @param {import('types').ServerHooks['transport']} transport + * @param {ServerHooks['transport']} transport */ function stringify_action_response(data, route_id, transport) { const encoders = Object.fromEntries( diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js index de17e7666b0d..3283e836ef37 100644 --- a/packages/kit/src/runtime/server/page/index.js +++ b/packages/kit/src/runtime/server/page/index.js @@ -56,7 +56,7 @@ export async function render_page(event, page, options, manifest, state, nodes, if (is_action_request(event)) { // for action requests, first call handler in +page.server.js // (this also determines status code) - action_result = await handle_action_request(event, leaf_node.server); + action_result = await handle_action_request(event, leaf_node.server, await options.tracer); if (action_result?.type === 'redirect') { return redirect_response(action_result.status, action_result.location); } @@ -166,7 +166,8 @@ export async function render_page(event, page, options, manifest, state, nodes, if (parent) Object.assign(data, parent.data); } return data; - } + }, + tracer: await options.tracer }); } catch (e) { load_error = /** @type {Error} */ (e); @@ -194,7 +195,8 @@ export async function render_page(event, page, options, manifest, state, nodes, resolve_opts, server_data_promise: server_promises[i], state, - csr + csr, + tracer: await options.tracer }); } catch (e) { load_error = /** @type {Error} */ (e); diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 74bd7444af4f..45ec2209b7cd 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -1,8 +1,10 @@ import { DEV } from 'esm-env'; import { disable_search, make_trackable } from '../../../utils/url.js'; -import { validate_depends } from '../../shared.js'; +import { validate_depends, validate_load_response } 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_node_type } from '../utils.js'; /** * Calls the user's server `load` function. @@ -11,10 +13,11 @@ import { with_event } from '../../app/server/event.js'; * state: import('types').SSRState; * node: import('types').SSRNode | undefined; * parent: () => Promise>; + * tracer: import('@opentelemetry/api').Tracer; * }} opts * @returns {Promise} */ -export async function load_server_data({ event, state, node, parent }) { +export async function load_server_data({ event, state, node, parent, tracer }) { if (!node?.server) return null; let is_tracking = true; @@ -68,97 +71,113 @@ 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); - - 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', + tracer, + attributes: { + 'sveltekit.load.node_id': node.server_id || 'unknown', + 'sveltekit.load.node_type': get_node_type(node.server_id), + 'sveltekit.load.environment': 'server', + 'http.route': event.route.id || 'unknown' + }, + fn: async (span) => { + const rootSpan = event.tracing.rootSpan; + const traced_event = { ...event, tracing: { rootSpan, currentSpan: span } }; + const result = await with_event(traced_event, () => + load.call(null, { + ...traced_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); + validate_load_response(result, `in ${node.server_id}`); } done = true; @@ -182,6 +201,7 @@ export async function load_server_data({ event, state, node, parent }) { * server_data_promise: Promise; * state: import('types').SSRState; * csr: boolean; + * tracer: import('@opentelemetry/api').Tracer; * }} opts * @returns {Promise> | null>} */ @@ -193,7 +213,8 @@ export async function load_data({ server_data_promise, state, resolve_opts, - csr + csr, + tracer }) { const server_data_node = await server_data_promise; @@ -201,20 +222,39 @@ 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 result = await record_span({ + name: 'sveltekit.load', + tracer, + attributes: { + 'sveltekit.load.node_id': node.universal_id || 'unknown', + 'sveltekit.load.node_type': get_node_type(node.universal_id), + 'sveltekit.load.environment': 'server', + 'http.route': event.route.id || 'unknown' + }, + fn: async (span) => { + const rootSpan = event.tracing.rootSpan; + const tracing = { rootSpan, currentSpan: span }; + const result = await load.call(null, { + tracing, + 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__) { - validate_load_response(result, node.universal_id); + validate_load_response(result, `in ${node.universal_id}`); } return result ?? null; @@ -398,23 +438,3 @@ async function stream_to_string(stream) { } return result; } - -/** - * @param {any} data - * @param {string} [id] - */ -function validate_load_response(data, id) { - if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { - throw new Error( - `a load function in ${id} returned ${ - typeof data !== 'object' - ? `a ${typeof data}` - : data instanceof Response - ? 'a Response object' - : Array.isArray(data) - ? 'an array' - : 'a non-plain object' - }, but must return a plain object at the top level (i.e. \`return {...}\`)` - ); - } -} diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js index bb3e99054507..670870a35291 100644 --- a/packages/kit/src/runtime/server/page/respond_with_error.js +++ b/packages/kit/src/runtime/server/page/respond_with_error.js @@ -46,11 +46,13 @@ export async function respond_with_error({ if (ssr) { state.error = true; + const tracer = await options.tracer; const server_data_promise = load_server_data({ event, state, node: default_layout, + tracer, // eslint-disable-next-line @typescript-eslint/require-await parent: async () => ({}) }); @@ -66,7 +68,8 @@ export async function respond_with_error({ resolve_opts, server_data_promise, state, - csr + csr, + tracer }); branch.push( diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index d37659cf899b..38e71af812bf 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -34,6 +34,7 @@ import { strip_resolution_suffix } from '../pathname.js'; import { with_event } from '../app/server/event.js'; +import { record_span } from '../telemetry/record_span.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ /* global __SVELTEKIT_DEV__ */ @@ -362,32 +363,68 @@ export async function respond(request, options, manifest, state) { disable_search(url); } - const response = await with_event(event, () => - options.hooks.handle({ - event, - resolve: (event, opts) => - // counter-intuitively, we need to clear the event, so that it's not - // e.g. accessible when loading modules needed to handle the request - with_event(null, () => - resolve(event, page_nodes, opts).then((response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } - - add_cookies_to_headers(response.headers, Object.values(new_cookies)); - - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); - } - - return response; - }) - ) - }) - ); + const tracer = await options.tracer; + + const response = await record_span({ + name: 'sveltekit.handle.root', + tracer, + attributes: { + 'http.route': event.route.id || 'unknown', + 'http.method': event.request.method, + 'http.url': event.url.href, + 'sveltekit.is_data_request': is_data_request, + 'sveltekit.is_sub_request': event.isSubRequest + }, + fn: async (rootSpan) => { + const traced_event = { ...event, tracing: { rootSpan, currentSpan: rootSpan } }; + return await with_event(traced_event, () => + options.hooks.handle({ + event: traced_event, + resolve: (event, opts) => { + return record_span({ + name: 'sveltekit.resolve.root', + tracer, + attributes: { + 'http.route': event.route.id || 'unknown' + }, + fn: async (resolveSpan) => { + // counter-intuitively, we need to clear the event, so that it's not + // e.g. accessible when loading modules needed to handle the request + return with_event(null, () => + resolve( + { ...event, tracing: { rootSpan, currentSpan: resolveSpan } }, + page_nodes, + opts + ).then((response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } + + add_cookies_to_headers(response.headers, Object.values(new_cookies)); + + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } + + resolveSpan.setAttributes({ + 'http.response.status_code': response.status, + 'http.response.body.size': + response.headers.get('content-length') || 'unknown' + }); + + return response; + }) + ); + } + }); + } + }) + ); + } + }); // respond with 304 if etag matches if (response.status === 200 && response.headers.has('etag')) { diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js index 663584760dd7..92ef239bf3e3 100644 --- a/packages/kit/src/runtime/server/utils.js +++ b/packages/kit/src/runtime/server/utils.js @@ -183,3 +183,16 @@ export function has_prerendered_path(manifest, pathname) { (pathname.at(-1) === '/' && manifest._.prerendered_routes.has(pathname.slice(0, -1))) ); } + +/** + * Returns the filename without the extension. e.g., `+page.server`, `+page`, etc. + * @param {string | undefined} node_id + * @returns {string} + */ +export function get_node_type(node_id) { + const parts = node_id?.split('/'); + const filename = parts?.at(-1); + if (!filename) return 'unknown'; + const dot_parts = filename.split('.'); + return dot_parts.slice(0, -1).join('.'); +} diff --git a/packages/kit/src/runtime/shared.js b/packages/kit/src/runtime/shared.js index b5c559b4292c..fc88526fa442 100644 --- a/packages/kit/src/runtime/shared.js +++ b/packages/kit/src/runtime/shared.js @@ -14,3 +14,23 @@ export function validate_depends(route_id, dep) { export const INVALIDATED_PARAM = 'x-sveltekit-invalidated'; export const TRAILING_SLASH_PARAM = 'x-sveltekit-trailing-slash'; + +/** + * @param {any} data + * @param {string} [location_description] + */ +export function validate_load_response(data, location_description) { + if (data != null && Object.getPrototypeOf(data) !== Object.prototype) { + throw new Error( + `a load function ${location_description} returned ${ + typeof data !== 'object' + ? `a ${typeof data}` + : data instanceof Response + ? 'a Response object' + : Array.isArray(data) + ? 'an array' + : 'a non-plain object' + }, but must return a plain object at the top level (i.e. \`return {...}\`)` + ); + } +} diff --git a/packages/kit/src/runtime/telemetry/get_tracer.js b/packages/kit/src/runtime/telemetry/get_tracer.js index 5c7fac29b412..d4198b9330ea 100644 --- a/packages/kit/src/runtime/telemetry/get_tracer.js +++ b/packages/kit/src/runtime/telemetry/get_tracer.js @@ -3,12 +3,23 @@ import { DEV } from 'esm-env'; import { noop_tracer } from './noop.js'; import { load_otel } from './load_otel.js'; +// this is controlled via a global flag because we need to access it in locations where we don't have access to the config +// (specifically, in `sequence`-d handle functions) +// since this is a global flag with a static value, it's safe to set it during server initialization +let is_enabled = false; + +export function enable_tracing() { + is_enabled = true; +} + +export function disable_tracing() { + is_enabled = false; +} + /** - * @param {Object} [options={}] - Configuration options - * @param {boolean} [options.is_enabled=false] - Whether tracing is enabled * @returns {Promise} The tracer instance */ -export async function get_tracer({ is_enabled = false } = {}) { +export async function get_tracer() { if (!is_enabled) { return noop_tracer; } diff --git a/packages/kit/src/runtime/telemetry/get_tracer.spec.js b/packages/kit/src/runtime/telemetry/get_tracer.spec.js index 01a8dc74a9b1..1452527f47f8 100644 --- a/packages/kit/src/runtime/telemetry/get_tracer.spec.js +++ b/packages/kit/src/runtime/telemetry/get_tracer.spec.js @@ -1,23 +1,25 @@ import { describe, test, expect, beforeEach, vi } from 'vitest'; -import { get_tracer } from './get_tracer.js'; +import { disable_tracing, enable_tracing, 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(); + disable_tracing(); }); test('returns noop tracer if tracing is disabled', async () => { - const tracer = await get_tracer({ is_enabled: false }); + const tracer = await get_tracer(); expect(tracer).toBe(noop_tracer); }); test('returns noop tracer if @opentelemetry/api is not installed, warning', async () => { + enable_tracing(); vi.spyOn(load_otel, 'load_otel').mockResolvedValue(null); const console_warn_spy = vi.spyOn(console, 'warn'); - const tracer = await get_tracer({ is_enabled: true }); + const tracer = await get_tracer(); expect(tracer).toBe(noop_tracer); expect(console_warn_spy).toHaveBeenCalledWith( 'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?' @@ -25,7 +27,8 @@ describe('get_tracer', () => { }); test('returns otel tracer if @opentelemetry/api is installed', async () => { - const tracer = await get_tracer({ is_enabled: true }); + enable_tracing(); + const tracer = await get_tracer(); expect(tracer).not.toBe(noop_tracer); }); }); diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 17e2425e3c17..4354fa2fe055 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -29,6 +29,7 @@ import { RequestOptions, TrailingSlash } from './private.js'; +import { Tracer } from '@opentelemetry/api'; export interface ServerModule { Server: typeof InternalServer; @@ -435,6 +436,7 @@ export interface SSROptions { }): string; error(values: { message: string; status: number }): string; }; + tracer: Promise; version_hash: string; } diff --git a/packages/kit/test/apps/basics/.gitignore b/packages/kit/test/apps/basics/.gitignore index fad4d3e1518d..7bc5d5fff974 100644 --- a/packages/kit/test/apps/basics/.gitignore +++ b/packages/kit/test/apps/basics/.gitignore @@ -1,3 +1,4 @@ /test/errors.json !/.env -/src/routes/routing/symlink-from \ No newline at end of file +/src/routes/routing/symlink-from +/test/spans.jsonl \ No newline at end of file diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json index fb8fa5350dd8..bcd7c3973834 100644 --- a/packages/kit/test/apps/basics/package.json +++ b/packages/kit/test/apps/basics/package.json @@ -17,6 +17,9 @@ "test:server-side-route-resolution:build": "node test/setup.js && node -e \"fs.rmSync('test/errors.json', { force: true })\" && cross-env PUBLIC_PRERENDERING=false ROUTER_RESOLUTION=server playwright test" }, "devDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-node": "^2.0.1", "@sveltejs/kit": "workspace:^", "@sveltejs/vite-plugin-svelte": "catalog:", "cross-env": "catalog:", diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index 5976df3f5763..4c13cf54da2b 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -5,6 +5,7 @@ import fs from 'node:fs'; import { COOKIE_NAME } from './routes/cookies/shared'; import { _set_from_init } from './routes/init-hooks/+page.server'; import { getRequestEvent } from '$app/server'; +import '$lib/tracing-config'; /** * Transform an error into a POJO, by copying its `name`, `message` @@ -49,6 +50,13 @@ export const handleError = ({ event, error: e, status, message }) => { }; export const handle = sequence( + ({ event, resolve }) => { + const test_id = !building && event.url.searchParams.get('test_id'); + if (test_id) { + event.tracing.rootSpan.setAttribute('test_id', test_id); + } + return resolve(event); + }, ({ event, resolve }) => { event.locals.key = event.route.id; event.locals.params = event.params; diff --git a/packages/kit/test/apps/basics/src/lib/tracing-config.js b/packages/kit/test/apps/basics/src/lib/tracing-config.js new file mode 100644 index 000000000000..b944279edecc --- /dev/null +++ b/packages/kit/test/apps/basics/src/lib/tracing-config.js @@ -0,0 +1,48 @@ +/** @import {SpanExporter} from '@opentelemetry/sdk-trace-node' */ +/** @import {SpanData} from '../../../../types' */ +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node'; +import fs from 'node:fs'; + +/** @implements {SpanExporter} */ +class FilesystemSpanExporter { + #path; + + constructor(path) { + fs.rmSync(path, { force: true }); + this.#path = path; + } + + /** @param {import('@opentelemetry/sdk-trace-node').ReadableSpan[]} spans */ + export(spans) { + // spans have circular references so they can't be naively json-ified + const serialized_spans = spans.map((span) => { + const span_context = span.spanContext(); + /** @type {SpanData} */ + const span_data = { + name: span.name, + status: span.status, + start_time: span.startTime, + end_time: span.endTime, + attributes: span.attributes, + links: span.links, + trace_id: span_context.traceId, + span_id: span_context.spanId, + parent_span_id: span.parentSpanContext?.spanId + }; + return JSON.stringify(span_data); + }); + + fs.appendFileSync(this.#path, serialized_spans.join('\n') + '\n'); + } + shutdown() { + return Promise.resolve(); + } +} + +const filesystemSpanExporter = new FilesystemSpanExporter('test/spans.jsonl'); +const spanProcessor = new SimpleSpanProcessor(filesystemSpanExporter); +export const sdk = new NodeSDK({ + spanProcessor +}); +sdk.start(); diff --git a/packages/kit/test/apps/basics/src/routes/tracing/http-error/+page.server.js b/packages/kit/test/apps/basics/src/routes/tracing/http-error/+page.server.js new file mode 100644 index 000000000000..cdb7c46a0e31 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/tracing/http-error/+page.server.js @@ -0,0 +1,5 @@ +import { error } from '@sveltejs/kit'; + +export async function load() { + error(500, 'Internal server error from tracing test'); +} diff --git a/packages/kit/test/apps/basics/src/routes/tracing/http-error/+page.svelte b/packages/kit/test/apps/basics/src/routes/tracing/http-error/+page.svelte new file mode 100644 index 000000000000..f32044fa6f36 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/tracing/http-error/+page.svelte @@ -0,0 +1 @@ +

This should not render due to load error

diff --git a/packages/kit/test/apps/basics/src/routes/tracing/non-error-object/+page.server.js b/packages/kit/test/apps/basics/src/routes/tracing/non-error-object/+page.server.js new file mode 100644 index 000000000000..1d678adad533 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/tracing/non-error-object/+page.server.js @@ -0,0 +1,3 @@ +export async function load() { + throw 'string error from tracing test'; +} diff --git a/packages/kit/test/apps/basics/src/routes/tracing/one/two/three/[...four]/+page.svelte b/packages/kit/test/apps/basics/src/routes/tracing/one/two/three/[...four]/+page.svelte new file mode 100644 index 000000000000..13c34178c381 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/tracing/one/two/three/[...four]/+page.svelte @@ -0,0 +1 @@ +

Tracing

diff --git a/packages/kit/test/apps/basics/src/routes/tracing/redirect/+page.server.js b/packages/kit/test/apps/basics/src/routes/tracing/redirect/+page.server.js new file mode 100644 index 000000000000..42f1a7375b87 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/tracing/redirect/+page.server.js @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export async function load({ url }) { + redirect(307, `/tracing/one/two/three/four/five${url.search}`); +} diff --git a/packages/kit/test/apps/basics/src/routes/tracing/regular-error/+page.server.js b/packages/kit/test/apps/basics/src/routes/tracing/regular-error/+page.server.js new file mode 100644 index 000000000000..209e1233ac85 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/tracing/regular-error/+page.server.js @@ -0,0 +1,3 @@ +export async function load() { + throw new Error('Regular error from tracing test'); +} diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index d2193940f0ab..12f8ebb08a6b 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -37,9 +37,12 @@ const config = { version: { name: 'TEST_VERSION' }, + router: { resolution: /** @type {'client' | 'server'} */ (process.env.ROUTER_RESOLUTION) || 'client' - } + }, + + experimental: { tracing: 'server' } } }; diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index e265115c53c2..148a2a821b74 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -768,3 +768,470 @@ test.describe('$app/environment', () => { expect(code).not.toContain('browser'); }); }); + +test.describe('tracing', () => { + // Helper function to find the resolve.root span deep in the handle.child chain + /** @param {import('@opentelemetry/sdk-trace-node').ReadableSpan} span */ + /** @returns {import('@opentelemetry/sdk-trace-node').ReadableSpan | null} */ + function findResolveRootSpan(span) { + if (span.name === 'sveltekit.resolve.root') { + return span; + } + for (const child of span.children || []) { + const found = findResolveRootSpan(child); + if (found) return found; + } + return null; + } + + function rand() { + // node 18 doesn't have crypto.randomUUID() and we run tests in node 18 + return Math.random().toString(36).substring(2, 15); + } + + test('correct spans are created for a regular navigation', async ({ page, read_traces }) => { + const test_id = rand(); + await page.goto(`/tracing/one/two/three/four/five?test_id=${test_id}`); + const traces = read_traces(test_id); + expect(traces.length).toBeGreaterThan(0); + + const trace = traces[0]; + const trace_id = trace.trace_id; + + // Verify root span structure + expect(trace).toEqual({ + name: 'sveltekit.handle.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/one/two/three/[...four]', + 'http.method': 'GET', + 'http.url': expect.stringContaining(`/tracing/one/two/three/four/five?test_id=${test_id}`), + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': false, + test_id + }, + links: [], + trace_id, + span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.handle.child', + attributes: expect.objectContaining({ + 'sveltekit.handle.child.index': 0 + }) + }) + ]) + }); + + // Find and verify the resolve.root span + const resolveRootSpan = findResolveRootSpan(trace); + expect(resolveRootSpan).not.toBeNull(); + expect(resolveRootSpan).toEqual({ + name: 'sveltekit.resolve.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/one/two/three/[...four]', + 'http.response.status_code': 200, + 'http.response.body.size': expect.stringMatching(/^\d+$/) + }, + links: [], + trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: [ + { + name: 'sveltekit.load', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'sveltekit.load.node_id': 'src/routes/+layout.server.js', + 'sveltekit.load.node_type': '+layout.server', + 'sveltekit.load.environment': 'server', + 'http.route': '/tracing/one/two/three/[...four]' + }, + links: [], + trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: [] + }, + { + name: 'sveltekit.load', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'sveltekit.load.node_id': 'src/routes/+layout.js', + 'sveltekit.load.node_type': '+layout', + 'sveltekit.load.environment': 'server', + 'http.route': '/tracing/one/two/three/[...four]' + }, + links: [], + trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: [] + } + ] + }); + }); + + test('correct spans are created for HttpError', async ({ page, read_traces }) => { + const test_id = rand(); + const response = await page.goto(`/tracing/http-error?test_id=${test_id}`); + expect(response?.status()).toBe(500); + + const traces = read_traces(test_id); + const trace_id = traces[0].trace_id; + const trace = traces[0]; + + // Verify root span structure + expect(trace).toEqual({ + name: 'sveltekit.handle.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/http-error', + 'http.method': 'GET', + 'http.url': expect.stringContaining(`/tracing/http-error?test_id=${test_id}`), + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': false, + test_id + }, + links: [], + trace_id, + span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.handle.child', + attributes: expect.objectContaining({ + 'sveltekit.handle.child.index': 0 + }) + }) + ]) + }); + + // Find and verify the resolve.root span + const resolveRootSpan = findResolveRootSpan(trace); + expect(resolveRootSpan).not.toBeNull(); + expect(resolveRootSpan).toEqual({ + name: 'sveltekit.resolve.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/http-error', + 'http.response.status_code': 500, + 'http.response.body.size': expect.stringMatching(/^\d+$/) + }, + links: [], + trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.load', + status: { code: 2, message: 'Internal server error from tracing test' }, + attributes: expect.objectContaining({ + 'sveltekit.load.node_id': 'src/routes/tracing/http-error/+page.server.js', + 'sveltekit.load.result.type': 'known_error', + 'sveltekit.load.result.status': 500, + 'sveltekit.load.result.message': 'Internal server error from tracing test' + }) + }) + ]) + }); + }); + + test('correct spans are created for Redirect', async ({ page, read_traces }) => { + const test_id = rand(); + const response = await page.goto(`/tracing/redirect?test_id=${test_id}`); + expect(response?.status()).toBe(200); + + const traces = read_traces(test_id); + expect(traces).toHaveLength(2); + const redirect_trace_id = traces[0].trace_id; + const destination_trace_id = traces[1].trace_id; + + const redirectTrace = traces[0]; + const destinationTrace = traces[1]; + + // Verify redirect trace root span structure + expect(redirectTrace).toEqual({ + name: 'sveltekit.handle.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/redirect', + 'http.method': 'GET', + 'http.url': expect.stringContaining(`/tracing/redirect?test_id=${test_id}`), + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': false, + test_id + }, + links: [], + trace_id: redirect_trace_id, + span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.handle.child', + attributes: expect.objectContaining({ + 'sveltekit.handle.child.index': 0 + }) + }) + ]) + }); + + // Find and verify the redirect resolve.root span + const redirectResolveRootSpan = findResolveRootSpan(redirectTrace); + expect(redirectResolveRootSpan).not.toBeNull(); + expect(redirectResolveRootSpan).toEqual({ + name: 'sveltekit.resolve.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/redirect', + 'http.response.status_code': 307, + 'http.response.body.size': expect.stringMatching(/^\d+$|^unknown$/) + }, + links: [], + trace_id: redirect_trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.load', + status: { code: 0 }, + attributes: expect.objectContaining({ + 'sveltekit.load.node_id': 'src/routes/tracing/redirect/+page.server.js', + 'sveltekit.load.result.type': 'redirect', + 'sveltekit.load.result.status': 307, + 'sveltekit.load.result.location': `/tracing/one/two/three/four/five?test_id=${test_id}` + }) + }) + ]) + }); + + // Verify destination trace root span structure + expect(destinationTrace).toEqual({ + name: 'sveltekit.handle.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/one/two/three/[...four]', + 'http.method': 'GET', + 'http.url': expect.stringContaining(`/tracing/one/two/three/four/five?test_id=${test_id}`), + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': false, + test_id + }, + links: [], + trace_id: destination_trace_id, + span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.handle.child', + attributes: expect.objectContaining({ + 'sveltekit.handle.child.index': 0 + }) + }) + ]) + }); + + // Find and verify the destination resolve.root span + const destinationResolveRootSpan = findResolveRootSpan(destinationTrace); + expect(destinationResolveRootSpan).not.toBeNull(); + expect(destinationResolveRootSpan).toEqual({ + name: 'sveltekit.resolve.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/one/two/three/[...four]', + 'http.response.status_code': 200, + 'http.response.body.size': expect.stringMatching(/^\d+$/) + }, + links: [], + trace_id: destination_trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: [ + { + name: 'sveltekit.load', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'sveltekit.load.node_id': 'src/routes/+layout.server.js', + 'sveltekit.load.node_type': '+layout.server', + 'sveltekit.load.environment': 'server', + 'http.route': '/tracing/one/two/three/[...four]' + }, + links: [], + trace_id: destination_trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: [] + }, + { + name: 'sveltekit.load', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'sveltekit.load.node_id': 'src/routes/+layout.js', + 'sveltekit.load.node_type': '+layout', + 'sveltekit.load.environment': 'server', + 'http.route': '/tracing/one/two/three/[...four]' + }, + links: [], + trace_id: destination_trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: [] + } + ] + }); + }); + + test('correct spans are created for regular Error', async ({ page, read_traces }) => { + const test_id = rand(); + const response = await page.goto(`/tracing/regular-error?test_id=${test_id}`); + expect(response?.status()).toBe(500); + + const traces = read_traces(test_id); + const trace_id = traces[0].trace_id; + const trace = traces[0]; + + // Verify root span structure + expect(trace).toEqual({ + name: 'sveltekit.handle.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/regular-error', + 'http.method': 'GET', + 'http.url': expect.stringContaining(`/tracing/regular-error?test_id=${test_id}`), + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': false, + test_id + }, + links: [], + trace_id, + span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.handle.child', + attributes: expect.objectContaining({ + 'sveltekit.handle.child.index': 0 + }) + }) + ]) + }); + + // Find and verify the resolve.root span + const resolveRootSpan = findResolveRootSpan(trace); + expect(resolveRootSpan).not.toBeNull(); + expect(resolveRootSpan).toEqual({ + name: 'sveltekit.resolve.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/regular-error', + 'http.response.status_code': 500, + 'http.response.body.size': expect.stringMatching(/^\d+$/) + }, + links: [], + trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.load', + status: { code: 2, message: 'Regular error from tracing test' }, + attributes: expect.objectContaining({ + 'sveltekit.load.node_id': 'src/routes/tracing/regular-error/+page.server.js', + 'sveltekit.load.result.type': 'unknown_error' + }) + }) + ]) + }); + }); + + test('correct spans are created for non-error object', async ({ page, read_traces }) => { + const test_id = rand(); + const response = await page.goto(`/tracing/non-error-object?test_id=${test_id}`); + expect(response?.status()).toBe(500); + + const traces = read_traces(test_id); + const trace_id = traces[0].trace_id; + const trace = traces[0]; + + // Verify root span structure + expect(trace).toEqual({ + name: 'sveltekit.handle.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/non-error-object', + 'http.method': 'GET', + 'http.url': expect.stringContaining(`/tracing/non-error-object?test_id=${test_id}`), + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': false, + test_id + }, + links: [], + trace_id, + span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.handle.child', + attributes: expect.objectContaining({ + 'sveltekit.handle.child.index': 0 + }) + }) + ]) + }); + + // Find and verify the resolve.root span + const resolveRootSpan = findResolveRootSpan(trace); + expect(resolveRootSpan).not.toBeNull(); + expect(resolveRootSpan).toEqual({ + name: 'sveltekit.resolve.root', + status: { code: 0 }, + start_time: [expect.any(Number), expect.any(Number)], + end_time: [expect.any(Number), expect.any(Number)], + attributes: { + 'http.route': '/tracing/non-error-object', + 'http.response.status_code': 500, + 'http.response.body.size': expect.stringMatching(/^\d+$/) + }, + links: [], + trace_id, + span_id: expect.any(String), + parent_span_id: expect.any(String), + children: expect.arrayContaining([ + expect.objectContaining({ + name: 'sveltekit.load', + status: { code: 2 }, + attributes: expect.objectContaining({ + 'sveltekit.load.node_id': 'src/routes/tracing/non-error-object/+page.server.js', + 'sveltekit.load.result.type': 'unknown_error' + }) + }) + ]) + }); + }); +}); diff --git a/packages/kit/test/types.d.ts b/packages/kit/test/types.d.ts new file mode 100644 index 000000000000..24083ce19bf3 --- /dev/null +++ b/packages/kit/test/types.d.ts @@ -0,0 +1,21 @@ +export interface SpanData { + name: string; + status: { + code: number; + message?: string; + }; + start_time: [number, number]; // HrTime tuple: [seconds, nanoseconds] + end_time: [number, number]; // HrTime tuple: [seconds, nanoseconds] + attributes: Record>; + links: Array<{ + context: any; + attributes?: Record>; + }>; + trace_id: string; + span_id: string; + parent_span_id: string | undefined; +} + +export type SpanTree = Omit & { + children: SpanTree[]; +}; diff --git a/packages/kit/test/utils.d.ts b/packages/kit/test/utils.d.ts index de03974f5baa..95a3f7f11c21 100644 --- a/packages/kit/test/utils.d.ts +++ b/packages/kit/test/utils.d.ts @@ -10,6 +10,7 @@ import { import { IncomingMessage, ServerResponse } from 'node:http'; import '../types/index'; import { AfterNavigate, BeforeNavigate } from '@sveltejs/kit'; +import { SpanTree } from './types'; export const test: TestType< PlaywrightTestArgs & @@ -30,6 +31,7 @@ export const test: TestType< * `handleError` defines the shape */ read_errors(href: string): Record; + read_traces(test_id: string): SpanTree[]; start_server( handler: (req: IncomingMessage, res: ServerResponse) => void ): Promise<{ port: number }>; diff --git a/packages/kit/test/utils.js b/packages/kit/test/utils.js index e7c84a51069c..eba4a5fd1a03 100644 --- a/packages/kit/test/utils.js +++ b/packages/kit/test/utils.js @@ -1,3 +1,4 @@ +/** @import {SpanData, SpanTree} from './types' */ import fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; @@ -144,6 +145,27 @@ export const test = base.extend({ await use(read_errors); }, + // eslint-disable-next-line no-empty-pattern -- Playwright doesn't let us use `_` as a parameter name. It must be a destructured object + read_traces: async ({}, use) => { + /** @param {string} test_id */ + function read_traces(test_id) { + const raw = fs.readFileSync('test/spans.jsonl', 'utf8').split('\n').filter(Boolean); + const traces = /** @type {SpanData[]} */ (raw.map((line) => JSON.parse(line))); + const root_traces = traces.filter( + (trace) => trace.parent_span_id === undefined && trace.attributes.test_id === test_id + ); + if (root_traces.length === 0) { + return []; + } + return root_traces.map((root_trace) => { + const child_traces = traces.filter((span) => span.trace_id === root_trace.trace_id); + return build_span_tree(root_trace, child_traces); + }); + } + + await use(read_traces); + }, + // eslint-disable-next-line no-empty-pattern -- Playwright doesn't let us use `_` as a parameter name. It must be a destructured object start_server: async ({}, use) => { /** @@ -288,3 +310,16 @@ export const config = { testDir: 'test', testMatch: /(.+\.)?(test|spec)\.[jt]s/ }; + +/** + * @param {SpanData} current_span + * @param {SpanData[]} child_spans + * @returns {SpanTree} + */ +function build_span_tree(current_span, child_spans) { + const children = child_spans.filter((span) => span.parent_span_id === current_span.span_id); + return { + ...current_span, + children: children.map((child) => build_span_tree(child, child_spans)) + }; +} diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 28a0b6c4a56a..9aca4624bbf5 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -3,6 +3,7 @@ declare module '@sveltejs/kit' { import type { SvelteConfig } from '@sveltejs/vite-plugin-svelte'; + import type { Span } from '@opentelemetry/api'; /** * [Adapters](https://svelte.dev/docs/kit/adapters) are responsible for taking the production build and turning it into something that can be deployed to a platform of your choosing. */ @@ -383,6 +384,15 @@ declare module '@sveltejs/kit' { */ privatePrefix?: string; }; + /** Experimental features. Here be dragons. Breaking changes may occur in minor releases. */ + experimental?: { + /** + * Whether to enable server-side [OpenTelemetry](https://opentelemetry.io/) tracing for SvelteKit operations including the [`handle` hook](https://svelte.dev/docs/kit/hooks#Server-hooks-handle), [`load` functions](https://svelte.dev/docs/kit/load), and [form actions](https://svelte.dev/docs/kit/form-actions). + * @default undefined + * @since 2.26.0 // TODO: update this before publishing + */ + tracing?: 'server'; + }; /** * Where to find various files within your project. */ @@ -949,6 +959,17 @@ declare module '@sveltejs/kit' { * ``` */ untrack: (fn: () => T) => T; + + /** + * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing.\ + * @since 2.26.0 // TODO: update this before publishing + */ + tracing: { + /** The root span for the request. This span is named `sveltekit.handle.root`. */ + rootSpan: Span; + /** The span associated with the current `load` function. */ + currentSpan: Span; + }; } export interface NavigationEvent< @@ -1224,6 +1245,17 @@ declare module '@sveltejs/kit' { * `true` for `+server.js` calls coming from SvelteKit without the overhead of actually making an HTTP request. This happens when you make same-origin `fetch` requests on the server. */ isSubRequest: boolean; + + /** + * Access to spans for tracing. If tracing is not enabled, these spans will do nothing. + * @since 2.26.0 // TODO: update this before publishing + */ + tracing: { + /** The root span for the request. This span is named `sveltekit.handle.root`. */ + rootSpan: Span; + /** The span associated with the current `handle` hook, `load` function, or form action. */ + currentSpan: Span; + }; } /** @@ -1380,6 +1412,17 @@ declare module '@sveltejs/kit' { * ``` */ untrack: (fn: () => T) => T; + + /** + * Access to spans for tracing. If tracing is not enabled, these spans will do nothing. + * @since 2.26.0 // TODO: update this before publishing + */ + tracing: { + /** The root span for the request. This span is named `sveltekit.handle.root`. */ + rootSpan: Span; + /** The span associated with the current server `load` function. */ + currentSpan: Span; + }; } /** @@ -2049,6 +2092,7 @@ declare module '@sveltejs/kit' { } declare module '@sveltejs/kit/hooks' { + import type { Handle } from '@sveltejs/kit'; /** * A helper function for sequencing multiple `handle` calls in a middleware-like manner. * The behavior for the `handle` options is as follows: @@ -2119,7 +2163,7 @@ declare module '@sveltejs/kit/hooks' { * * @param handlers The chain of `handle` functions * */ - export function sequence(...handlers: import("@sveltejs/kit").Handle[]): import("@sveltejs/kit").Handle; + export function sequence(...handlers: Handle[]): Handle; export {}; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ed9775338ca..fbb60debb4a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -526,6 +526,15 @@ importers: packages/kit/test/apps/basics: devDependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/sdk-node': + specifier: ^0.203.0 + version: 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.0.1 + version: 2.0.1(@opentelemetry/api@1.9.0) '@sveltejs/kit': specifier: workspace:^ version: link:../../.. @@ -1822,6 +1831,15 @@ packages: '@fontsource/libre-barcode-128-text@5.1.0': resolution: {integrity: sha512-MC7foQFRT0NDcsqBWQua2T3gs/fh/uTowTxfoPqGQWjqroiMxRZhQh7jerjnpcI+Xi3yR5bwCo6W2uwCza1FRw==} + '@grpc/grpc-js@1.13.4': + resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2099,6 +2117,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -2370,6 +2391,10 @@ packages: '@octokit/types@14.1.0': resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@opentelemetry/api-logs@0.203.0': + resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.8.0': resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} engines: {node: '>=8.0.0'} @@ -2378,6 +2403,160 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@opentelemetry/context-async-hooks@2.0.1': + resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.1': + resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.203.0': + resolution: {integrity: sha512-g/2Y2noc/l96zmM+g0LdeuyYKINyBwN6FJySoU15LHPLcMN/1a0wNk2SegwKcxrRdE7Xsm7fkIR5n6XFe3QpPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.203.0': + resolution: {integrity: sha512-s0hys1ljqlMTbXx2XiplmMJg9wG570Z5lH7wMvrZX6lcODI56sG4HL03jklF63tBeyNwK2RV1/ntXGo3HgG4Qw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.203.0': + resolution: {integrity: sha512-nl/7S91MXn5R1aIzoWtMKGvqxgJgepB/sH9qW0rZvZtabnsjbf8OQ1uSx3yogtvLr0GzwD596nQKz2fV7q2RBw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0': + resolution: {integrity: sha512-FCCj9nVZpumPQSEI57jRAA89hQQgONuoC35Lt+rayWY/mzCAc6BQT7RFyFaZKJ2B7IQ8kYjOCPsF/HGFWjdQkQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.203.0': + resolution: {integrity: sha512-HFSW10y8lY6BTZecGNpV3GpoSy7eaO0Z6GATwZasnT4bEsILp8UJXNG5OmEsz4SdwCSYvyCbTJdNbZP3/8LGCQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.203.0': + resolution: {integrity: sha512-OZnhyd9npU7QbyuHXFEPVm3LnjZYifuKpT3kTnF84mXeEQ84pJJZgyLBpU4FSkSwUkt/zbMyNAI7y5+jYTWGIg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.203.0': + resolution: {integrity: sha512-2jLuNuw5m4sUj/SncDf/mFPabUxMZmmYetx5RKIMIQyPnl6G6ooFzfeE8aXNRf8YD1ZXNlCnRPcISxjveGJHNg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.203.0': + resolution: {integrity: sha512-322coOTf81bm6cAA8+ML6A+m4r2xTCdmAZzGNTboPXRzhwPt4JEmovsFAs+grpdarObd68msOJ9FfH3jxM6wqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.203.0': + resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.203.0': + resolution: {integrity: sha512-1xwNTJ86L0aJmWRwENCJlH4LULMG2sOXWIVw+Szta4fkqKVY50Eo4HoVKKq6U9QEytrWCr8+zjw0q/ZOeXpcAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.0.1': + resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation@0.203.0': + resolution: {integrity: sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.203.0': + resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.203.0': + resolution: {integrity: sha512-te0Ze1ueJF+N/UOFl5jElJW4U0pZXQ8QklgSfJ2linHN0JJsuaHG8IabEUi2iqxY8ZBDlSiz1Trfv5JcjWWWwQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.203.0': + resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.0.1': + resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.0.1': + resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.203.0': + resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.203.0': + resolution: {integrity: sha512-zRMvrZGhGVMvAbbjiNQW3eKzW/073dlrSiAKPVWmkoQzah9wfynpVPeL55f9fVIm0GaBxTLcPeukWGy0/Wj7KQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.0.1': + resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.36.0': + resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} + engines: {node: '>=14'} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -2494,6 +2673,36 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@publint/pack@0.1.0': resolution: {integrity: sha512-NvV5jPAQIMCoHvaJ0ZhfouBJ2woFYYf+o6B7dCHGh/tLKSPVoxhjffi35xPuMHgOv65aTOKUzML5XwQF9EkDAA==} engines: {node: '>=18'} @@ -3429,6 +3638,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + clean-deep@3.4.0: resolution: {integrity: sha512-Lo78NV5ItJL/jl+B5w0BycAisaieJGXK1qYi/9m4SjR8zbqmrUtO7Yhro40wEShGmmxs/aJLI/A+jNhdkXK8mw==} engines: {node: '>=4'} @@ -4605,6 +4817,9 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -5015,6 +5230,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} @@ -5070,6 +5288,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} @@ -5245,6 +5466,9 @@ packages: engines: {node: '>=18'} hasBin: true + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + moize@6.1.6: resolution: {integrity: sha512-vSKdIUO61iCmTqhdoIDrqyrtp87nWZUmBPniNjO0fX49wEYmyDO4lvlnFXiGcaH1JLE/s/9HbiK4LSHsbiUY6Q==} @@ -5796,6 +6020,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5943,6 +6171,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + require-package-name@2.0.1: resolution: {integrity: sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==} @@ -7547,6 +7779,18 @@ snapshots: '@fontsource/libre-barcode-128-text@5.1.0': {} + '@grpc/grpc-js@1.13.4': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.3 + yargs: 17.7.2 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -7759,6 +8003,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@js-sdsl/ordered-map@4.4.2': {} + '@lukeed/ms@2.0.2': {} '@manypkg/find-root@1.1.0': @@ -8315,10 +8561,235 @@ snapshots: dependencies: '@octokit/openapi-types': 25.1.0 + '@opentelemetry/api-logs@0.203.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api@1.8.0': {} '@opentelemetry/api@1.9.0': {} + '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.3 + + '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.36.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -8414,6 +8885,29 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@publint/pack@0.1.0': {} '@rollup/plugin-commonjs@28.0.1(rollup@4.40.1)': @@ -9064,6 +9558,10 @@ snapshots: dependencies: acorn: 8.14.1 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -9415,6 +9913,8 @@ snapshots: dependencies: consola: 3.2.3 + cjs-module-lexer@1.4.3: {} + clean-deep@3.4.0: dependencies: lodash.isempty: 4.4.0 @@ -10680,6 +11180,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} indent-string@5.0.0: {} @@ -11076,6 +11583,8 @@ snapshots: lodash-es@4.17.21: {} + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} lodash.includes@4.3.0: {} @@ -11131,6 +11640,8 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + long@5.3.2: {} + loupe@3.1.3: {} lower-case@2.0.2: @@ -11274,6 +11785,8 @@ snapshots: ast-module-types: 6.0.1 node-source-walk: 7.0.1 + module-details-from-path@1.0.4: {} + moize@6.1.6: dependencies: fast-equals: 3.0.3 @@ -11912,6 +12425,21 @@ snapshots: proto-list@1.2.4: {} + protobufjs@7.5.3: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 18.19.119 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -12068,6 +12596,14 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.1(supports-color@10.0.0) + module-details-from-path: 1.0.4 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + require-package-name@2.0.1: {} requires-port@1.0.0: {}