UNPKG

@fiberplane/hono-otel

Version:

Hono middleware to forward OpenTelemetry traces to a local instance of @fiberplane/studio

388 lines (387 loc) 13 kB
import { SpanStatusCode, context, trace, } from "@opentelemetry/api"; import { isPromiseLike } from "./utils/index.js"; export function measure(nameOrOptions, fn) { const isOptions = typeof nameOrOptions === "object"; const name = isOptions ? nameOrOptions.name : nameOrOptions; const { onStart, onSuccess, onError, checkResult, logger, endSpanManually, attributes, spanKind, } = isOptions ? nameOrOptions : {}; return (...args) => { function handleActiveSpan(span) { let shouldEndSpan = true; if (onStart) { try { onStart(span, args); } catch (error) { if (logger) { const errorMessage = formatException(convertToException(error)); logger.warn(`Error in onStart while measuring ${name}:`, errorMessage); } } } try { const returnValue = fn(...args); if (isGeneratorValue(returnValue)) { shouldEndSpan = false; const handlerOptions = { endSpanManually, onSuccess, checkResult, onError, }; return handleGenerator(span, returnValue, handlerOptions); } if (isAsyncGeneratorValue(returnValue)) { shouldEndSpan = false; const handlerOptions = { endSpanManually, onSuccess, checkResult, onError, }; return handleAsyncGenerator(span, returnValue, handlerOptions); } if (isPromiseLike(returnValue)) { shouldEndSpan = false; return handlePromise(span, returnValue, { onSuccess, onError, checkResult, endSpanManually, }); } span.setStatus({ code: SpanStatusCode.OK }); if (onSuccess) { try { const result = onSuccess(span, returnValue); if (isPromiseLike(result)) { shouldEndSpan = false; result.finally(() => { if (!endSpanManually) { span.end(); } }); } } catch (error) { if (logger) { const errorMessage = formatException(convertToException(error)); logger.warn(`Error in onSuccess while measuring ${name}:`, errorMessage); } } } return returnValue; } catch (error) { const exception = convertToException(error); span.recordException(exception); if (onError) { try { onError(span, error); } catch { // swallow error } } throw error; } finally { if (shouldEndSpan) { span.end(); } } } const tracer = trace.getTracer("fpx-tracer"); return tracer.startActiveSpan(name, { kind: spanKind, attributes }, handleActiveSpan); }; } /** * Handle complete flow of a promise (including ending the span) * * @returns the promise */ async function handlePromise(span, resultPromise, options) { const { onSuccess, onError, checkResult, endSpanManually = false } = options; try { const result = await resultPromise; if (checkResult) { try { await checkResult(result); } catch (error) { // recordException only accepts Error objects or strings const exception = convertToException(error); span.recordException(exception); if (onError) { try { await onError(span, exception); } catch { // swallow error } } if (onSuccess) { try { await onSuccess(span, result); } catch { // swallow error } finally { if (!endSpanManually) { span.end(); } } } return result; } } span.setStatus({ code: SpanStatusCode.OK }); if (onSuccess) { try { await onSuccess(span, result); } catch { // swallow error } } return result; } catch (error) { try { const exception = convertToException(error); span.recordException(exception); const message = formatException(exception); span.setStatus({ code: SpanStatusCode.ERROR, message, }); if (onError) { try { onError(span, error); } catch { // swallow error } } } catch { // swallow error } // Rethrow the error throw error; } finally { if (!endSpanManually || !onSuccess) { span.end(); } } } /** * Handles synchronous iterators (generators). * Measures the time until the generator is fully consumed. */ function handleGenerator(span, iterable, options) { const { checkResult, endSpanManually, onError, onSuccess } = options; function handleError(error) { const exception = convertToException(error); span.recordException(exception); const message = formatException(exception); span.setStatus({ code: SpanStatusCode.ERROR, message, }); if (onError) { try { onError(span, error); } catch { // swallow error } } span.end(); } const active = context.active(); return { ...iterable, next: context.bind(active, measure("iterator.next", function nextFunction(...args) { try { const result = iterable.next(...args); if (result.done) { try { if (checkResult) { checkResult(result.value); } if (!endSpanManually) { span.setStatus({ code: SpanStatusCode.OK }); span.end(); } if (onSuccess) { onSuccess(span, result.value); } } catch (error) { handleError(error); } } return result; } catch (error) { handleError(error); throw error; } })), return: context.bind(active, function returnFunction(value) { try { const result = iterable.return(value); if (result.done) { try { if (checkResult) { checkResult(result.value); } if (!endSpanManually) { span.setStatus({ code: SpanStatusCode.OK }); span.end(); } if (onSuccess) { onSuccess(span, result.value); } } catch (error) { handleError(error); } } return result; } catch (error) { handleError(error); throw error; } }), throw: context.bind(active, function throwFunction(error) { try { return iterable.throw(error); } finally { handleError(error); } }), [Symbol.iterator]() { return this; }, }; } /** * Handles asynchronous iterators (async generators). * Measures the time until the async generator is fully consumed. */ function handleAsyncGenerator(span, iterable, options) { const { checkResult, endSpanManually, onError, onSuccess } = options; const active = context.active(); function handleError(error) { const exception = convertToException(error); span.recordException(exception); const message = formatException(exception); span.setStatus({ code: SpanStatusCode.ERROR, message, }); if (onError) { try { onError(span, error); } catch { // swallow error } } span.end(); } return { ...iterable, next: context.bind(active, measure("iterator.next", async function nextFunction(...args) { try { const result = await iterable.next(...args); if (result.done) { try { if (checkResult) { await checkResult(result.value); } if (!endSpanManually) { span.setStatus({ code: SpanStatusCode.OK }); span.end(); } if (onSuccess) { await onSuccess(span, result.value); } } catch (error) { handleError(error); } } return result; } catch (error) { handleError(error); throw error; } })), return: context.bind(active, async function returnFunction(value) { try { const result = await iterable.return(value); if (result.done) { try { if (checkResult) { checkResult(result.value); } if (!endSpanManually) { span.setStatus({ code: SpanStatusCode.OK }); span.end(); } if (onSuccess) { onSuccess(span, result.value); } } catch (error) { handleError(error); } } return result; } catch (error) { handleError(error); throw error; } }), throw: context.bind(active, async function throwFunction(error) { try { return await iterable.throw(error); } finally { handleError(error); } }), [Symbol.asyncIterator]() { return this; }, }; } function convertToException(error) { return error instanceof Error ? error : "Unknown error occurred"; } function formatException(exception) { return typeof exception === "string" ? exception : exception.message || "Unknown error occurred"; } // const GeneratorFunction = Object.getPrototypeOf(function* () {}).constructor; export function isGeneratorValue(value) { return (value !== null && typeof value === "object" && Symbol.iterator in value); } /** * Type guard to check if a function is an async generator. * * @param fn - The function to be checked * @returns true if the function is an async generator, otherwise false */ export function isAsyncGeneratorValue(value) { return (value !== null && typeof value === "object" && Symbol.asyncIterator in value); }