So I wanted to setup tracing for an LLM app with Phoenix and while doing so I had some learnings that I wanted to document:

Here is an example implementation of such a Trace decorator.

export function Trace<A, R>(
   traceName: string,
   traceFunction: (span: Span, args: A, result: R) => void,
 ) {
   return function (
     target: unknown,
     propertyKey: string,
     descriptor: PropertyDescriptor,
   ) {
     const originalMethod = descriptor.value;

     descriptor.value = async function (args: A) {
       return await tracer.startActiveSpan(traceName, async (span: Span) => {
         const result = await originalMethod.call(this, args);
         traceFunction(span, args, result);
         span.end();
         return result;
       });
     };
     return descriptor;
   };
 }

And here is an example on how you can use it. As you can see the business logic in doingSomeWork() is completely free of any tracing logic. Removing the @Trace() decorator completely removes any tracing from it.

type DoingSomeWorkParams = {
    input: string;
}

@Trace("Doing Some Work", DoingSomeWorkParams, string)
async function doingSomeWork(args: DoingSomeWorkParams): Promise<string> {
    // ... doing some work with args.input
    return result
}

function traceDoingSomeWork(span: Span, args: DoingSomeWorkParams, result: string) {
    span.setAttributes({
     [SemanticConventions.OPENINFERENCE_SPAN_KIND]: OpenInferenceSpanKind.AGENT,
     [SemanticConventions.INPUT_VALUE]: args.input,
     [SemanticConventions.OUTPUT_VALUE]: result,
    });
}

Going on with the list of things I learned: