Skip to content

Commit 1472161

Browse files
feat: Initial tracing setup (peer deps + utils)
1 parent 293392b commit 1472161

File tree

8 files changed

+425
-0
lines changed

8 files changed

+425
-0
lines changed

packages/kit/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"vitefu": "^1.0.6"
3434
},
3535
"devDependencies": {
36+
"@opentelemetry/api": "^1.0.0",
3637
"@playwright/test": "catalog:",
3738
"@sveltejs/vite-plugin-svelte": "^5.0.1",
3839
"@types/connect": "^3.4.38",
@@ -47,10 +48,16 @@
4748
"vitest": "catalog:"
4849
},
4950
"peerDependencies": {
51+
"@opentelemetry/api": "^1.0.0",
5052
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
5153
"svelte": "^4.0.0 || ^5.0.0-next.0",
5254
"vite": "^5.0.3 || ^6.0.0"
5355
},
56+
"peerDependenciesMeta": {
57+
"@opentelemetry/api": {
58+
"optional": true
59+
}
60+
},
5461
"bin": {
5562
"svelte-kit": "svelte-kit.js"
5663
},
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/** @import { Tracer } from '@opentelemetry/api' */
2+
import { DEV } from 'esm-env';
3+
import { noop_tracer } from './noop.js';
4+
import { load_tracer } from './load_otel.js';
5+
6+
/**
7+
* @param {Object} [options={}] - Configuration options
8+
* @param {boolean} [options.is_enabled=false] - Whether tracing is enabled
9+
* @returns {Promise<Tracer>} The tracer instance
10+
*/
11+
export async function get_tracer({ is_enabled = false } = {}) {
12+
if (!is_enabled) {
13+
return noop_tracer;
14+
}
15+
16+
const otel_tracer = await load_tracer();
17+
if (otel_tracer === null) {
18+
if (DEV) {
19+
console.warn(
20+
'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?'
21+
);
22+
}
23+
return noop_tracer;
24+
}
25+
26+
return otel_tracer;
27+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, test, expect, beforeEach, vi } from 'vitest';
2+
import { get_tracer } from './get_tracer.js';
3+
import { noop_tracer } from './noop.js';
4+
import * as load_otel from './load_otel.js';
5+
6+
describe('validateHeaders', () => {
7+
beforeEach(() => {
8+
vi.resetAllMocks();
9+
});
10+
11+
test('returns noop tracer if tracing is disabled', async () => {
12+
const tracer = await get_tracer({ is_enabled: false });
13+
expect(tracer).toBe(noop_tracer);
14+
});
15+
16+
test('returns noop tracer if @opentelemetry/api is not installed, warning', async () => {
17+
vi.spyOn(load_otel, 'load_tracer').mockResolvedValue(null);
18+
const console_warn_spy = vi.spyOn(console, 'warn');
19+
20+
const tracer = await get_tracer({ is_enabled: true });
21+
expect(tracer).toBe(noop_tracer);
22+
expect(console_warn_spy).toHaveBeenCalledWith(
23+
'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?'
24+
);
25+
});
26+
27+
test('returns otel tracer if @opentelemetry/api is installed', async () => {
28+
const tracer = await get_tracer({ is_enabled: true });
29+
expect(tracer).not.toBe(noop_tracer);
30+
});
31+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @returns {Promise<import('@opentelemetry/api').Tracer | null>}
3+
*/
4+
export async function load_tracer() {
5+
try {
6+
const { trace } = await import('@opentelemetry/api');
7+
return trace.getTracer('sveltekit');
8+
} catch {
9+
return null;
10+
}
11+
}
12+
13+
/**
14+
* @returns {Promise<typeof import('@opentelemetry/api').SpanStatusCode | null>}
15+
*/
16+
export async function load_status_code() {
17+
try {
18+
const { SpanStatusCode } = await import('@opentelemetry/api');
19+
return SpanStatusCode;
20+
} catch {
21+
return null;
22+
}
23+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/** @import { Tracer, Span, SpanContext } from '@opentelemetry/api' */
2+
3+
/**
4+
* Tracer implementation that does nothing (null object).
5+
* @type {Tracer}
6+
*/
7+
export const noop_tracer = {
8+
/**
9+
* @returns {Span}
10+
*/
11+
startSpan() {
12+
return noop_span;
13+
},
14+
15+
/**
16+
* @param {unknown} _name
17+
* @param {unknown} arg_1
18+
* @param {unknown} [arg_2]
19+
* @param {Function} [arg_3]
20+
* @returns {unknown}
21+
*/
22+
startActiveSpan(_name, arg_1, arg_2, arg_3) {
23+
if (typeof arg_1 === 'function') {
24+
return arg_1(noop_span);
25+
}
26+
if (typeof arg_2 === 'function') {
27+
return arg_2(noop_span);
28+
}
29+
if (typeof arg_3 === 'function') {
30+
return arg_3(noop_span);
31+
}
32+
}
33+
};
34+
35+
/**
36+
* @type {Span}
37+
*/
38+
export const noop_span = {
39+
spanContext() {
40+
return noop_span_context;
41+
},
42+
setAttribute() {
43+
return this;
44+
},
45+
setAttributes() {
46+
return this;
47+
},
48+
addEvent() {
49+
return this;
50+
},
51+
setStatus() {
52+
return this;
53+
},
54+
updateName() {
55+
return this;
56+
},
57+
end() {
58+
return this;
59+
},
60+
isRecording() {
61+
return false;
62+
},
63+
recordException() {
64+
return this;
65+
},
66+
addLink() {
67+
return this;
68+
},
69+
addLinks() {
70+
return this;
71+
}
72+
};
73+
74+
/**
75+
* @type {SpanContext}
76+
*/
77+
const noop_span_context = {
78+
traceId: '',
79+
spanId: '',
80+
traceFlags: 0
81+
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/** @import { Attributes, Span, Tracer } from '@opentelemetry/api' */
2+
import { HttpError, Redirect } from '../control.js';
3+
import { load_status_code } from './load_otel.js';
4+
import { noop_span } from './noop.js';
5+
6+
/**
7+
* @template T
8+
* @param {Object} options
9+
* @param {string} options.name
10+
* @param {Tracer} options.tracer
11+
* @param {Attributes} options.attributes
12+
* @param {function(Span): Promise<T>} options.fn
13+
* @returns {Promise<T>}
14+
*/
15+
export async function record_span({ name, tracer, attributes, fn }) {
16+
const SpanStatusCode = await load_status_code();
17+
if (SpanStatusCode === null) {
18+
return fn(noop_span);
19+
}
20+
21+
return tracer.startActiveSpan(name, { attributes }, async (span) => {
22+
try {
23+
const result = await fn(span);
24+
span.end();
25+
return result;
26+
} catch (error) {
27+
if (error instanceof HttpError) {
28+
span.setAttributes({
29+
[`${name}.result.type`]: 'known_error',
30+
[`${name}.result.status`]: error.status,
31+
[`${name}.result.message`]: error.body.message
32+
});
33+
span.recordException({
34+
name: 'HttpError',
35+
message: error.body.message
36+
});
37+
span.setStatus({
38+
code: SpanStatusCode.ERROR,
39+
message: error.body.message
40+
});
41+
} else if (error instanceof Redirect) {
42+
span.setAttributes({
43+
[`${name}.result.type`]: 'redirect',
44+
[`${name}.result.status`]: error.status,
45+
[`${name}.result.location`]: error.location
46+
});
47+
} else if (error instanceof Error) {
48+
span.setAttributes({
49+
[`${name}.result.type`]: 'unknown_error'
50+
});
51+
span.recordException({
52+
name: error.name,
53+
message: error.message,
54+
stack: error.stack
55+
});
56+
span.setStatus({
57+
code: SpanStatusCode.ERROR,
58+
message: error.message
59+
});
60+
} else {
61+
span.setAttributes({
62+
[`${name}.result.type`]: 'unknown_error'
63+
});
64+
span.setStatus({ code: SpanStatusCode.ERROR });
65+
}
66+
span.end();
67+
68+
throw error;
69+
}
70+
});
71+
}

0 commit comments

Comments
 (0)