@dexwox-labs/a2a-core
Version:
Core types, validation and telemetry for Google's Agent-to-Agent (A2A) protocol - shared foundation for client and server implementations
335 lines (301 loc) • 9.94 kB
text/typescript
/**
* @module Telemetry
* @description Decorators for instrumenting code with OpenTelemetry
*/
import { context, trace, type Context, type Span } from '@opentelemetry/api';
import { TelemetryService } from './service';
/** Type definition for any function */
type AnyFunction = (...args: any[]) => any;
/**
* Configuration options for the Trace decorator
*/
type TraceOptions = {
/** Custom name for the span (defaults to ClassName.methodName) */
name?: string;
/** Parent context for the span (for manual context propagation) */
parentContext?: Context;
};
/**
* Method decorator that automatically creates a span for the decorated method
*
* This decorator instruments methods with OpenTelemetry tracing, creating spans
* that track method execution, timing, and errors. It supports both synchronous
* and asynchronous methods.
*
* @param nameOrOptions - Optional span name or configuration object
*
* @example
* ```typescript
* class UserService {
* // Basic usage
* @Trace()
* async getUser(id: string) {
* // Method implementation
* }
*
* // With custom span name
* @Trace('FetchUserDetails')
* async getUserDetails(id: string) {
* // Method implementation
* }
*
* // With options object
* @Trace({ name: 'UserAuthentication' })
* async authenticateUser(username: string, password: string) {
* // Method implementation
* }
* }
* ```
*/
export function Trace(nameOrOptions?: string | TraceOptions): any {
// This function can be used as @Trace or @Trace(options)
return function(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
): PropertyDescriptor {
if (!descriptor || typeof descriptor.value !== 'function') {
throw new Error('@Trace can only be applied to method declarations');
}
const originalMethod = descriptor.value;
const className = target.constructor.name;
const methodName = String(propertyKey);
// Determine the span name and parent context
let spanName: string;
let parentContext: Context | undefined;
if (typeof nameOrOptions === 'string') {
spanName = nameOrOptions;
} else if (nameOrOptions?.name) {
spanName = nameOrOptions.name;
parentContext = nameOrOptions.parentContext;
} else {
spanName = `${className}.${methodName}`;
}
// Handle both synchronous and asynchronous methods
const isAsync = originalMethod.constructor.name === 'AsyncFunction';
if (isAsync) {
descriptor.value = async function(...args: any[]) {
const telemetry = TelemetryService.initialize();
if (!telemetry.isEnabled()) {
return originalMethod.apply(this, args);
}
const { span } = telemetry.startSpan(spanName, parentContext);
try {
// Record method arguments if detailed telemetry is enabled
if (telemetry['config'].collectionLevel === 'detailed') {
span.setAttribute('method', methodName);
span.setAttribute('class', className);
if (args && args.length > 0) {
span.setAttribute('args', JSON.stringify(args, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
));
}
}
const result = await originalMethod.apply(this, args);
span.setStatus({ code: 1 }); // OK
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: 2,
message: error instanceof Error ? error.message : String(error)
});
throw error;
} finally {
telemetry.endSpan(span);
}
};
} else {
descriptor.value = function(...args: any[]) {
const telemetry = TelemetryService.initialize();
if (!telemetry.isEnabled()) {
return originalMethod.apply(this, args);
}
const { span } = telemetry.startSpan(spanName, parentContext);
try {
// Record method arguments if detailed telemetry is enabled
if (telemetry['config'].collectionLevel === 'detailed') {
span.setAttribute('method', methodName);
span.setAttribute('class', className);
if (args && args.length > 0) {
span.setAttribute('args', JSON.stringify(args, (_, v) =>
typeof v === 'bigint' ? v.toString() : v
));
}
}
const result = originalMethod.apply(this, args);
span.setStatus({ code: 1 }); // OK
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({
code: 2,
message: error instanceof Error ? error.message : String(error)
});
throw error;
} finally {
telemetry.endSpan(span);
}
};
}
return descriptor;
};
}
/**
* Method decorator for recording metrics when a method is called
*
* This decorator records metrics each time the decorated method is called,
* allowing for monitoring method usage, performance, and other custom metrics.
*
* @param name - The name of the metric to record
* @param value - Optional value to record (default: 1)
* @param attributes - Optional attributes to include with the metric
* @returns A method decorator
*
* @example
* ```typescript
* class MessageService {
* // Count message sends
* @Metric('messages.sent')
* async sendMessage(message: Message) {
* // Implementation
* }
*
* // Record message size with custom value
* @Metric('message.size', message.content.length)
* async processMessage(message: Message) {
* // Implementation
* }
*
* // Include additional attributes
* @Metric('api.call', 1, { endpoint: '/messages' })
* async fetchMessages() {
* // Implementation
* }
* }
* ```
*/
export function Metric(name: string, value: number = 1, attributes?: Record<string, unknown>): any {
// This is a decorator factory, it returns the actual decorator function
return function(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
): PropertyDescriptor {
// Ensure we're decorating a method
if (!descriptor || typeof descriptor.value !== 'function') {
throw new Error('@Metric can only be applied to method declarations');
}
// Store the original method
const originalMethod = descriptor.value;
const className = target.constructor.name;
const methodName = String(propertyKey);
// Replace the original method with our instrumented version
descriptor.value = function(...args: any[]) {
const telemetry = TelemetryService.initialize();
if (telemetry.isEnabled()) {
telemetry.recordMetric(name, value, {
...attributes,
class: className,
method: methodName
});
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
/**
* Helper function to apply the metric decorator logic
*
* This is an internal utility function used by the Metric decorator to
* apply consistent metric recording logic.
*
* @internal
*/
function applyMetricDecorator(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
name: string,
value: number = 1,
attributes?: Record<string, unknown>
): PropertyDescriptor {
if (!descriptor || typeof descriptor.value !== 'function') {
throw new Error('@Metric can only be applied to method declarations');
}
const originalMethod = descriptor.value;
const className = target.constructor.name;
const methodName = String(propertyKey);
descriptor.value = function(...args: any[]) {
const telemetry = TelemetryService.initialize();
if (telemetry.isEnabled()) {
telemetry.recordMetric(name, value, {
...attributes,
class: className,
method: methodName
});
}
return originalMethod.apply(this, args);
};
return descriptor;
}
/**
* Class decorator to automatically trace all methods in a class
*
* This decorator applies the Trace decorator to all methods in a class,
* making it easy to instrument an entire class without decorating each
* method individually.
*
* @param name - Optional custom name prefix for the spans (defaults to class name)
* @returns A class decorator
*
* @example
* ```typescript
* // Basic usage - traces all methods with ClassName.methodName
* @TraceClass()
* class UserService {
* async getUser(id: string) {
* // Method implementation
* }
* async updateUser(id: string, data: any) {
* // Method implementation
* }
* }
*
* // With custom name prefix
* @TraceClass('Users')
* class UserService {
* // Will be traced as 'Users.getUser'
* async getUser(id: string) {
* // Method implementation
* }
* }
* ```
*/
export function TraceClass(name?: string): ClassDecorator {
return function(constructor: Function) {
const className = name || constructor.name;
for (const propertyName of Object.getOwnPropertyNames(constructor.prototype)) {
if (propertyName === 'constructor') continue;
const descriptor = Object.getOwnPropertyDescriptor(
constructor.prototype,
propertyName
);
if (descriptor && typeof descriptor.value === 'function') {
const traceDescriptor = Trace(`${className}.${propertyName}`)(
constructor.prototype,
propertyName,
descriptor
);
if (traceDescriptor) {
Object.defineProperty(
constructor.prototype,
propertyName,
traceDescriptor
);
}
}
}
};
}