- ✅ Ticket 1.1: Estructura Clean Architecture en backend - ✅ Ticket 1.2: Schemas Zod compartidos - ✅ Ticket 1.3: Refactorización drugs.ts (1362 → 8 archivos modulares) - ✅ Ticket 1.4: Refactorización procedures.ts (3583 → 6 archivos modulares) - ✅ Ticket 1.5: Eliminación de duplicidades (~50 líneas) Cambios principales: - Creada estructura Clean Architecture en backend/src/ - Schemas Zod compartidos en backend/src/shared/schemas/ - Refactorización modular de drugs y procedures - Utilidades genéricas en src/utils/ (filter, validation) - Eliminados scripts obsoletos y documentación antigua - Corregidos errores: QueryClient, import test-error-handling - Build verificado y funcionando correctamente
218 lines
5.9 KiB
JavaScript
218 lines
5.9 KiB
JavaScript
class Traces {
|
|
/**
|
|
* otel stands for OpenTelemetry
|
|
*/
|
|
#otel = null;
|
|
#sdk = null;
|
|
#init = null;
|
|
#noopSpan = createNoopSpan();
|
|
#noopContext = createNoopContext();
|
|
#initStartTime = performance.now();
|
|
#initEndTime = 0;
|
|
#initRecorded = false;
|
|
constructor(options) {
|
|
if (options.enabled) {
|
|
const apiInit = import('@opentelemetry/api').then((api) => {
|
|
this.#otel = {
|
|
tracer: api.trace.getTracer(options.tracerName || "vitest"),
|
|
context: api.context,
|
|
propagation: api.propagation,
|
|
trace: api.trace,
|
|
SpanKind: api.SpanKind,
|
|
SpanStatusCode: api.SpanStatusCode
|
|
};
|
|
}).catch(() => {
|
|
throw new Error(`"@opentelemetry/api" is not installed locally. Make sure you have setup OpenTelemetry instrumentation: https://vitest.dev/guide/open-telemetry`);
|
|
});
|
|
const sdkInit = (options.sdkPath ? import(
|
|
/* @vite-ignore */
|
|
options.sdkPath
|
|
) : Promise.resolve()).catch((cause) => {
|
|
throw new Error(`Failed to import custom OpenTelemetry SDK script (${options.sdkPath}): ${cause.message}`);
|
|
});
|
|
this.#init = Promise.all([sdkInit, apiInit]).then(([sdk]) => {
|
|
if (sdk != null) {
|
|
if (sdk.default != null && typeof sdk.default === "object" && typeof sdk.default.shutdown === "function") this.#sdk = sdk.default;
|
|
else if (options.watchMode !== true && process.env.VITEST_MODE !== "watch") console.warn(`OpenTelemetry instrumentation module (${options.sdkPath}) does not have a default export with a "shutdown" method. Vitest won't be able to ensure that all traces are processed in time. Try running Vitest in watch mode instead.`);
|
|
}
|
|
}).finally(() => {
|
|
this.#initEndTime = performance.now();
|
|
this.#init = null;
|
|
});
|
|
}
|
|
}
|
|
isEnabled() {
|
|
return !!this.#otel;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async waitInit() {
|
|
if (this.#init) await this.#init;
|
|
return this;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
recordInitSpan(context) {
|
|
if (this.#initRecorded) return;
|
|
this.#initRecorded = true;
|
|
this.startSpan("vitest.runtime.traces", { startTime: this.#initStartTime }, context).end(this.#initEndTime);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
startContextSpan(name, currentContext) {
|
|
if (!this.#otel) return {
|
|
span: this.#noopSpan,
|
|
context: this.#noopContext
|
|
};
|
|
const activeContext = currentContext || this.#otel.context.active();
|
|
const span = this.#otel.tracer.startSpan(name, {}, activeContext);
|
|
return {
|
|
span,
|
|
context: this.#otel.trace.setSpan(activeContext, span)
|
|
};
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getContextFromCarrier(carrier) {
|
|
if (!this.#otel) return this.#noopContext;
|
|
const activeContext = this.#otel.context.active();
|
|
if (!carrier) return activeContext;
|
|
return this.#otel.propagation.extract(activeContext, carrier);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getContextFromEnv(env) {
|
|
if (!this.#otel) return this.#noopContext;
|
|
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md
|
|
// some tools sets only `TRACEPARENT` but not `TRACESTATE`
|
|
const carrier = {};
|
|
if (typeof env.TRACEPARENT === "string") carrier.traceparent = env.TRACEPARENT;
|
|
if (typeof env.TRACESTATE === "string") carrier.tracestate = env.TRACESTATE;
|
|
return this.getContextFromCarrier(carrier);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
getContextCarrier(context) {
|
|
if (!this.#otel) return;
|
|
const carrier = {};
|
|
this.#otel.propagation.inject(context || this.#otel.context.active(), carrier);
|
|
return carrier;
|
|
}
|
|
#callActiveSpan(span, callback) {
|
|
const otel = this.#otel;
|
|
let result;
|
|
try {
|
|
result = callback(span);
|
|
if (result instanceof Promise) return result.catch((error) => {
|
|
span.recordException({
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
span.setStatus({ code: otel.SpanStatusCode.ERROR });
|
|
throw error;
|
|
}).finally(() => span.end());
|
|
return result;
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
span.recordException({
|
|
name: error.name,
|
|
message: error.message,
|
|
stack: error.stack
|
|
});
|
|
span.setStatus({ code: otel.SpanStatusCode.ERROR });
|
|
}
|
|
throw error;
|
|
} finally {
|
|
// end sync callbcak
|
|
if (!(result instanceof Promise)) span.end();
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
$(name, optionsOrFn, fn) {
|
|
const callback = typeof optionsOrFn === "function" ? optionsOrFn : fn;
|
|
if (!this.#otel) return callback(this.#noopSpan);
|
|
const otel = this.#otel;
|
|
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
const context = options.context;
|
|
if (context) return otel.tracer.startActiveSpan(name, options, context, (span) => this.#callActiveSpan(span, callback));
|
|
return otel.tracer.startActiveSpan(name, options, (span) => this.#callActiveSpan(span, callback));
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
startSpan(name, options, context) {
|
|
if (!this.#otel) return this.#noopSpan;
|
|
const { tracer } = this.#otel;
|
|
return tracer.startSpan(name, options, context);
|
|
}
|
|
// On browser mode, async context is not automatically propagated,
|
|
// so we manually bind the `$` calls to the provided context.
|
|
// TODO: this doesn't bind to user land's `@optelemetry/api` calls
|
|
/**
|
|
* @internal
|
|
*/
|
|
bind(context) {
|
|
if (!this.#otel) return;
|
|
const original = this.$.__original ?? this.$;
|
|
this.$ = this.#otel.context.bind(context, original);
|
|
this.$.__original = original;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async finish() {
|
|
await this.#sdk?.shutdown();
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
async flush() {
|
|
await this.#sdk?.forceFlush?.();
|
|
}
|
|
}
|
|
function noopSpan() {
|
|
return this;
|
|
}
|
|
function createNoopSpan() {
|
|
return {
|
|
setAttribute: noopSpan,
|
|
setStatus: noopSpan,
|
|
addEvent: noopSpan,
|
|
addLink: noopSpan,
|
|
addLinks: noopSpan,
|
|
setAttributes: noopSpan,
|
|
updateName: noopSpan,
|
|
end: () => {},
|
|
isRecording: () => false,
|
|
recordException: noopSpan,
|
|
spanContext() {
|
|
return {
|
|
spanId: "",
|
|
traceFlags: 0,
|
|
traceId: ""
|
|
};
|
|
}
|
|
};
|
|
}
|
|
function noopContext() {
|
|
return this;
|
|
}
|
|
function createNoopContext() {
|
|
return {
|
|
getValue: noopContext,
|
|
setValue: noopContext,
|
|
deleteValue: noopContext
|
|
};
|
|
}
|
|
|
|
export { Traces as T };
|