@fiberplane/hono-otel
Version:
Hono middleware to forward OpenTelemetry traces to a local instance of @fiberplane/studio
388 lines (387 loc) • 13 kB
JavaScript
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);
}