-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Initial tracing setup (peer deps + utils) #13899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/** @import { Tracer } from '@opentelemetry/api' */ | ||
import { DEV } from 'esm-env'; | ||
import { noop_tracer } from './noop.js'; | ||
import { load_tracer } from './load_otel.js'; | ||
|
||
/** | ||
* @param {Object} [options={}] - Configuration options | ||
* @param {boolean} [options.is_enabled=false] - Whether tracing is enabled | ||
* @returns {Promise<Tracer>} The tracer instance | ||
*/ | ||
export async function get_tracer({ is_enabled = false } = {}) { | ||
if (!is_enabled) { | ||
return noop_tracer; | ||
} | ||
|
||
const otel_tracer = await load_tracer(); | ||
if (otel_tracer === null) { | ||
if (DEV) { | ||
console.warn( | ||
'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?' | ||
); | ||
} | ||
return noop_tracer; | ||
} | ||
|
||
return otel_tracer; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { describe, test, expect, beforeEach, vi } from 'vitest'; | ||
import { get_tracer } from './get_tracer.js'; | ||
import { noop_tracer } from './noop.js'; | ||
import * as load_otel from './load_otel.js'; | ||
|
||
describe('validateHeaders', () => { | ||
beforeEach(() => { | ||
vi.resetAllMocks(); | ||
}); | ||
|
||
test('returns noop tracer if tracing is disabled', async () => { | ||
const tracer = await get_tracer({ is_enabled: false }); | ||
expect(tracer).toBe(noop_tracer); | ||
}); | ||
|
||
test('returns noop tracer if @opentelemetry/api is not installed, warning', async () => { | ||
vi.spyOn(load_otel, 'load_tracer').mockResolvedValue(null); | ||
const console_warn_spy = vi.spyOn(console, 'warn'); | ||
|
||
const tracer = await get_tracer({ is_enabled: true }); | ||
expect(tracer).toBe(noop_tracer); | ||
expect(console_warn_spy).toHaveBeenCalledWith( | ||
'Tracing is enabled, but `@opentelemetry/api` is not available. Have you installed it?' | ||
); | ||
}); | ||
|
||
test('returns otel tracer if @opentelemetry/api is installed', async () => { | ||
const tracer = await get_tracer({ is_enabled: true }); | ||
expect(tracer).not.toBe(noop_tracer); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** | ||
* @returns {Promise<import('@opentelemetry/api').Tracer | null>} | ||
*/ | ||
export async function load_tracer() { | ||
try { | ||
const { trace } = await import('@opentelemetry/api'); | ||
return trace.getTracer('sveltekit'); | ||
} catch { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* @returns {Promise<typeof import('@opentelemetry/api').SpanStatusCode | null>} | ||
*/ | ||
export async function load_status_code() { | ||
try { | ||
const { SpanStatusCode } = await import('@opentelemetry/api'); | ||
return SpanStatusCode; | ||
} catch { | ||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/** @import { Tracer, Span, SpanContext } from '@opentelemetry/api' */ | ||
|
||
/** | ||
* Tracer implementation that does nothing (null object). | ||
* @type {Tracer} | ||
*/ | ||
export const noop_tracer = { | ||
/** | ||
* @returns {Span} | ||
*/ | ||
startSpan() { | ||
return noop_span; | ||
}, | ||
|
||
/** | ||
* @param {unknown} _name | ||
* @param {unknown} arg_1 | ||
* @param {unknown} [arg_2] | ||
* @param {Function} [arg_3] | ||
* @returns {unknown} | ||
*/ | ||
startActiveSpan(_name, arg_1, arg_2, arg_3) { | ||
if (typeof arg_1 === 'function') { | ||
return arg_1(noop_span); | ||
} | ||
if (typeof arg_2 === 'function') { | ||
return arg_2(noop_span); | ||
} | ||
if (typeof arg_3 === 'function') { | ||
return arg_3(noop_span); | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* @type {Span} | ||
*/ | ||
export const noop_span = { | ||
spanContext() { | ||
return noop_span_context; | ||
}, | ||
setAttribute() { | ||
return this; | ||
}, | ||
setAttributes() { | ||
return this; | ||
}, | ||
addEvent() { | ||
return this; | ||
}, | ||
setStatus() { | ||
return this; | ||
}, | ||
updateName() { | ||
return this; | ||
}, | ||
end() { | ||
return this; | ||
}, | ||
isRecording() { | ||
return false; | ||
}, | ||
recordException() { | ||
return this; | ||
}, | ||
addLink() { | ||
return this; | ||
}, | ||
addLinks() { | ||
return this; | ||
} | ||
}; | ||
|
||
/** | ||
* @type {SpanContext} | ||
*/ | ||
const noop_span_context = { | ||
traceId: '', | ||
spanId: '', | ||
traceFlags: 0 | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** @import { Attributes, Span, Tracer } from '@opentelemetry/api' */ | ||
import { HttpError, Redirect } from '../control.js'; | ||
import { load_status_code } from './load_otel.js'; | ||
import { noop_span } from './noop.js'; | ||
|
||
/** | ||
* @template T | ||
* @param {Object} options | ||
* @param {string} options.name | ||
* @param {Tracer} options.tracer | ||
* @param {Attributes} options.attributes | ||
* @param {function(Span): Promise<T>} options.fn | ||
* @returns {Promise<T>} | ||
*/ | ||
export async function record_span({ name, tracer, attributes, fn }) { | ||
const SpanStatusCode = await load_status_code(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since |
||
if (SpanStatusCode === null) { | ||
return fn(noop_span); | ||
} | ||
|
||
return tracer.startActiveSpan(name, { attributes }, async (span) => { | ||
try { | ||
const result = await fn(span); | ||
span.end(); | ||
return result; | ||
} catch (error) { | ||
if (error instanceof HttpError) { | ||
span.setAttributes({ | ||
[`${name}.result.type`]: 'known_error', | ||
[`${name}.result.status`]: error.status, | ||
[`${name}.result.message`]: error.body.message | ||
}); | ||
span.recordException({ | ||
name: 'HttpError', | ||
message: error.body.message | ||
}); | ||
Comment on lines
+33
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Furthermore, I'm not 100% sure if this applies here but in our SDK, when we capture |
||
span.setStatus({ | ||
code: SpanStatusCode.ERROR, | ||
message: error.body.message | ||
}); | ||
} else if (error instanceof Redirect) { | ||
span.setAttributes({ | ||
[`${name}.result.type`]: 'redirect', | ||
[`${name}.result.status`]: error.status, | ||
[`${name}.result.location`]: error.location | ||
}); | ||
} else if (error instanceof Error) { | ||
span.setAttributes({ | ||
[`${name}.result.type`]: 'unknown_error' | ||
}); | ||
span.recordException({ | ||
name: error.name, | ||
message: error.message, | ||
stack: error.stack | ||
}); | ||
span.setStatus({ | ||
code: SpanStatusCode.ERROR, | ||
message: error.message | ||
}); | ||
} else { | ||
span.setAttributes({ | ||
[`${name}.result.type`]: 'unknown_error' | ||
}); | ||
span.setStatus({ code: SpanStatusCode.ERROR }); | ||
} | ||
span.end(); | ||
|
||
throw error; | ||
} | ||
}); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.