UNPKG

autotel

Version:
1 lines 20.2 kB
{"version":3,"file":"semantic-helpers.cjs","names":["withTracing","SpanKind"],"sources":["../src/semantic-helpers.ts"],"sourcesContent":["/**\n * Semantic convention helpers for OpenTelemetry\n *\n * Pre-configured trace helpers that follow OpenTelemetry semantic conventions\n * for common operation types. Reduces boilerplate and ensures consistency.\n *\n * Based on: https://opentelemetry.io/docs/specs/semconv/\n */\n\nimport { withTracing } from './functional';\nimport { assertTraceFactory } from './trace-factory-validation';\nimport type { TraceContext } from './trace-context';\nimport { SpanKind, type Attributes } from '@opentelemetry/api';\n\ntype SemanticHandler<TArgs extends unknown[], TReturn> = (\n ...args: TArgs\n) => TReturn | Promise<TReturn>;\n\ntype SemanticFactory<TArgs extends unknown[], TReturn> = (\n ctx: TraceContext,\n) => SemanticHandler<TArgs, TReturn>;\n\nfunction setConfiguredAttributes(\n ctx: TraceContext,\n attributes?: Attributes,\n): void {\n if (!attributes) return;\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined || value === null) continue;\n const attributeValue =\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ? value\n : JSON.stringify(value);\n ctx.setAttribute(key, attributeValue);\n }\n}\n\n/**\n * Shared tail of `traceDB`/`traceHTTP`/`traceMessaging`: wrap `fn` directly\n * when given, otherwise return the curried factory-acceptor form.\n */\nfunction wrapSemantic<TArgs extends unknown[], TReturn>(\n helperName: string,\n name: string,\n spanKind: SpanKind,\n configure: (ctx: TraceContext) => void,\n fn?: SemanticHandler<TArgs, TReturn>,\n):\n | SemanticHandler<TArgs, TReturn>\n | (<TFactoryArgs extends unknown[], TFactoryReturn>(\n factory: SemanticFactory<TFactoryArgs, TFactoryReturn>,\n ) => SemanticHandler<TFactoryArgs, TFactoryReturn>) {\n if (fn) {\n assertTraceFactory(helperName, fn);\n return withTracing<TArgs, TReturn>({ name, spanKind })(\n (ctx: TraceContext) => {\n configure(ctx);\n return fn;\n },\n );\n }\n return <TFactoryArgs extends unknown[], TFactoryReturn>(\n factory: SemanticFactory<TFactoryArgs, TFactoryReturn>,\n ) => {\n assertTraceFactory(helperName, factory);\n return withTracing<TFactoryArgs, TFactoryReturn>({ name, spanKind })(\n (ctx: TraceContext) => {\n configure(ctx);\n const handler = factory(ctx);\n assertTraceFactory(helperName, handler, 'result');\n return handler;\n },\n );\n };\n}\n\n/**\n * Configuration for database operations\n *\n * Follows DB semantic conventions:\n * https://opentelemetry.io/docs/specs/semconv/database/\n */\nexport interface DBConfig {\n /** Database system (e.g., 'postgresql', 'mongodb', 'redis') */\n system: string;\n /** Operation type (e.g., 'SELECT', 'INSERT', 'find', 'get') */\n operation?: string;\n /** Database name */\n database?: string;\n /** Collection/table name */\n collection?: string;\n /** Low-cardinality query summary used as the preferred span name */\n querySummary?: string;\n /** Additional attributes to add to the span */\n attributes?: Attributes;\n}\n\n/**\n * Configuration for HTTP client operations\n *\n * Follows HTTP semantic conventions:\n * https://opentelemetry.io/docs/specs/semconv/http/\n */\nexport interface HTTPConfig {\n /** HTTP method (e.g., 'GET', 'POST') */\n method?: string;\n /** Target URL or URL template */\n url?: string;\n /** Low-cardinality server route used in the span name */\n route?: string;\n /** Low-cardinality client URL template used in the span name */\n urlTemplate?: string;\n /** Additional attributes to add to the span */\n attributes?: Attributes;\n}\n\n/**\n * Configuration for messaging operations\n *\n * Follows Messaging semantic conventions:\n * https://opentelemetry.io/docs/specs/semconv/messaging/\n */\nexport interface MessagingConfig {\n /** Messaging system (e.g., 'kafka', 'rabbitmq', 'sqs') */\n system: string;\n /** Operation type */\n operation?: 'publish' | 'receive' | 'process';\n /** Destination name (queue/topic) */\n destination?: string;\n /** Additional attributes to add to the span */\n attributes?: Attributes;\n}\n\n/**\n * Trace database operations with DB semantic conventions\n *\n * Automatically adds standard attributes for database operations:\n * - db.system.name\n * - db.operation.name\n * - db.namespace\n * - db.collection.name (for NoSQL)\n *\n * **Use Cases:**\n * - SQL queries (PostgreSQL, MySQL, SQLite)\n * - NoSQL operations (MongoDB, DynamoDB, Redis)\n * - ORM queries (Prisma, TypeORM, Drizzle)\n *\n * @param config - Database operation configuration\n * @returns Traced function factory with DB attributes\n *\n * @example PostgreSQL query\n * ```typescript\n * import { traceDB } from 'autotel/semantic-helpers'\n * import { pool } from './db'\n *\n * export const getUser = traceDB({\n * system: 'postgresql',\n * operation: 'SELECT',\n * database: 'app_db',\n * collection: 'users',\n * querySummary: 'SELECT users'\n * })(ctx => async (userId: string) => {\n * const query = 'SELECT * FROM users WHERE id = $1'\n * const result = await pool.query(query, [userId])\n * ctx.setAttribute('db.rows_affected', result.rowCount)\n *\n * return result.rows[0]\n * })\n * ```\n *\n * @example MongoDB with Mongoose\n * ```typescript\n * import { traceDB } from 'autotel/semantic-helpers'\n * import { User } from './models/User'\n *\n * export const findUsers = traceDB({\n * system: 'mongodb',\n * operation: 'find',\n * database: 'app_db',\n * collection: 'users'\n * })(ctx => async (filter: object) => {\n * ctx.setAttribute('db.mongodb.filter', JSON.stringify(filter))\n *\n * const users = await User.find(filter).limit(100)\n * ctx.setAttribute('db.response.count', users.length)\n *\n * return users\n * })\n * ```\n *\n * @example Redis operations\n * ```typescript\n * import { traceDB } from 'autotel/semantic-helpers'\n * import { redis } from './redis'\n *\n * export const cacheGet = traceDB({\n * system: 'redis',\n * operation: 'GET'\n * })(ctx => async (key: string) => {\n * ctx.setAttribute('db.redis.key', key)\n *\n * const value = await redis.get(key)\n * ctx.setAttribute('db.response.cache_hit', value !== null)\n *\n * return value\n * })\n * ```\n *\n * @example Prisma with detailed query info\n * ```typescript\n * import { traceDB } from 'autotel/semantic-helpers'\n * import { prisma } from './prisma'\n *\n * export const createPost = traceDB({\n * system: 'postgresql',\n * operation: 'INSERT',\n * database: 'app_db',\n * collection: 'posts'\n * })(ctx => async (data: { title: string; content: string; authorId: string }) => {\n * ctx.setAttribute('db.prisma.model', 'Post')\n * ctx.setAttribute('db.prisma.action', 'create')\n *\n * const post = await prisma.post.create({ data })\n *\n * ctx.setAttribute('db.response.id', post.id)\n * return post\n * })\n * ```\n *\n * @public\n */\nexport function traceDB(\n config: DBConfig,\n): <TArgs extends unknown[], TReturn>(\n factory: SemanticFactory<TArgs, TReturn>,\n) => SemanticHandler<TArgs, TReturn>;\nexport function traceDB<TArgs extends unknown[], TReturn>(\n config: DBConfig,\n fn: SemanticHandler<TArgs, TReturn>,\n): SemanticHandler<TArgs, TReturn>;\nexport function traceDB<TArgs extends unknown[], TReturn>(\n config: DBConfig,\n fn?: SemanticHandler<TArgs, TReturn>,\n):\n | SemanticHandler<TArgs, TReturn>\n | (<TFactoryArgs extends unknown[], TFactoryReturn>(\n factory: SemanticFactory<TFactoryArgs, TFactoryReturn>,\n ) => SemanticHandler<TFactoryArgs, TFactoryReturn>) {\n if (!config || typeof config !== 'object') {\n throw new TypeError('traceDB: config must be an object');\n }\n if (typeof config.system !== 'string' || config.system.trim() === '') {\n throw new TypeError('traceDB: config.system must be a non-empty string');\n }\n const target = config.collection ?? config.database;\n const name =\n config.querySummary ?? [config.operation, target].filter(Boolean).join(' ');\n const spanName = name || config.system;\n const configure = (ctx: TraceContext) => {\n // Emit current stable names plus legacy aliases for dashboard compatibility.\n ctx.setAttribute('db.system.name', config.system);\n ctx.setAttribute('db.system', config.system);\n if (config.operation) {\n ctx.setAttribute('db.operation.name', config.operation);\n ctx.setAttribute('db.operation', config.operation);\n }\n if (config.database) {\n ctx.setAttribute('db.namespace', config.database);\n ctx.setAttribute('db.name', config.database);\n }\n if (config.collection) {\n ctx.setAttribute('db.collection.name', config.collection);\n }\n if (config.querySummary) {\n ctx.setAttribute('db.query.summary', config.querySummary);\n }\n setConfiguredAttributes(ctx, config.attributes);\n };\n return wrapSemantic('traceDB', spanName, SpanKind.CLIENT, configure, fn);\n}\n\n/**\n * Trace HTTP client operations with HTTP semantic conventions\n *\n * Automatically adds standard attributes for HTTP requests:\n * - http.request.method\n * - url.full\n *\n * **Use Cases:**\n * - External API calls\n * - Microservice communication\n * - Third-party integrations\n *\n * @param config - HTTP operation configuration\n * @returns Traced function factory with HTTP attributes\n *\n * @example Fetch API\n * ```typescript\n * import { traceHTTP } from 'autotel/semantic-helpers'\n *\n * export const fetchUser = traceHTTP({\n * method: 'GET',\n * urlTemplate: '/users/{id}'\n * })(ctx => async (userId: string) => {\n * const url = `https://api.example.com/users/${userId}`\n * ctx.setAttribute('url.full', url)\n *\n * const response = await fetch(url)\n * ctx.setAttribute('http.response.status_code', response.status)\n *\n * if (!response.ok) {\n * ctx.setAttribute('error', true)\n * throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n * }\n *\n * return response.json()\n * })\n * ```\n *\n * @example Axios with retry logic\n * ```typescript\n * import { traceHTTP } from 'autotel/semantic-helpers'\n * import axios from 'axios'\n *\n * export const sendWebhook = traceHTTP({\n * method: 'POST',\n * url: 'https://webhook.example.com/events'\n * })(ctx => async (payload: object) => {\n * let attempts = 0\n * const maxAttempts = 3\n *\n * while (attempts < maxAttempts) {\n * try {\n * attempts++\n * ctx.setAttribute('http.request.resend_count', attempts - 1)\n *\n * const response = await axios.post('https://webhook.example.com/events', payload)\n * ctx.setAttribute('http.response.status_code', response.status)\n * return response.data\n * } catch (error) {\n * if (attempts >= maxAttempts) throw error\n * await new Promise(resolve => setTimeout(resolve, 1000 * attempts))\n * }\n * }\n * })\n * ```\n *\n * @public\n */\nexport function traceHTTP(\n config: HTTPConfig,\n): <TArgs extends unknown[], TReturn>(\n factory: SemanticFactory<TArgs, TReturn>,\n) => SemanticHandler<TArgs, TReturn>;\nexport function traceHTTP<TArgs extends unknown[], TReturn>(\n config: HTTPConfig,\n fn: SemanticHandler<TArgs, TReturn>,\n): SemanticHandler<TArgs, TReturn>;\nexport function traceHTTP<TArgs extends unknown[], TReturn>(\n config: HTTPConfig,\n fn?: SemanticHandler<TArgs, TReturn>,\n):\n | SemanticHandler<TArgs, TReturn>\n | (<TFactoryArgs extends unknown[], TFactoryReturn>(\n factory: SemanticFactory<TFactoryArgs, TFactoryReturn>,\n ) => SemanticHandler<TFactoryArgs, TFactoryReturn>) {\n if (!config || typeof config !== 'object') {\n throw new TypeError('traceHTTP: config must be an object');\n }\n const method = config.method?.toUpperCase() || 'HTTP';\n const target = config.route ?? config.urlTemplate;\n const spanName = target ? `${method} ${target}` : method;\n const configure = (ctx: TraceContext) => {\n if (config.method) ctx.setAttribute('http.request.method', method);\n if (config.url) ctx.setAttribute('url.full', config.url);\n if (config.route) ctx.setAttribute('http.route', config.route);\n if (config.urlTemplate) {\n ctx.setAttribute('url.template', config.urlTemplate);\n }\n setConfiguredAttributes(ctx, config.attributes);\n };\n return wrapSemantic('traceHTTP', spanName, SpanKind.CLIENT, configure, fn);\n}\n\n/**\n * Trace messaging operations with Messaging semantic conventions\n *\n * Automatically adds standard attributes for messaging:\n * - messaging.system\n * - messaging.operation\n * - messaging.destination.name\n *\n * **Use Cases:**\n * - Publishing messages to queues/topics\n * - Consuming messages from queues/topics\n * - Event-driven architectures\n *\n * @param config - Messaging operation configuration\n * @returns Traced function factory with Messaging attributes\n *\n * @example Publishing to Kafka\n * ```typescript\n * import { traceMessaging } from 'autotel/semantic-helpers'\n * import { kafka } from './kafka'\n *\n * const producer = kafka.producer()\n *\n * export const publishEvent = traceMessaging({\n * system: 'kafka',\n * operation: 'publish',\n * destination: 'user-events'\n * })(ctx => async (event: { type: string; userId: string; data: object }) => {\n * ctx.setAttribute('messaging.message.type', event.type)\n * ctx.setAttribute('messaging.kafka.partition', 0)\n *\n * await producer.send({\n * topic: 'user-events',\n * messages: [\n * {\n * key: event.userId,\n * value: JSON.stringify(event.data)\n * }\n * ]\n * })\n *\n * ctx.setAttribute('messaging.message.id', event.userId)\n * })\n * ```\n *\n * @example Consuming from RabbitMQ\n * ```typescript\n * import { traceMessaging } from 'autotel/semantic-helpers'\n * import { channel } from './rabbitmq'\n *\n * export const processOrder = traceMessaging({\n * system: 'rabbitmq',\n * operation: 'process',\n * destination: 'orders'\n * })(ctx => async (message: { orderId: string; items: object[] }) => {\n * ctx.setAttribute('messaging.message.id', message.orderId)\n * ctx.setAttribute('messaging.message.body.size', JSON.stringify(message).length)\n *\n * // Process order logic\n * const result = await processOrderInternal(message)\n *\n * ctx.setAttribute('messaging.operation.result', 'success')\n * return result\n * })\n * ```\n *\n * @example AWS SQS with batch processing\n * ```typescript\n * import { traceMessaging } from 'autotel/semantic-helpers'\n * import { SQS } from '@aws-sdk/client-sqs'\n *\n * const sqs = new SQS()\n *\n * export const sendBatch = traceMessaging({\n * system: 'aws_sqs',\n * operation: 'publish',\n * destination: 'notifications-queue'\n * })(ctx => async (messages: Array<{ id: string; body: object }>) => {\n * ctx.setAttribute('messaging.batch.message_count', messages.length)\n *\n * const result = await sqs.sendMessageBatch({\n * QueueUrl: process.env.QUEUE_URL,\n * Entries: messages.map(msg => ({\n * Id: msg.id,\n * MessageBody: JSON.stringify(msg.body)\n * }))\n * })\n *\n * ctx.setAttribute('messaging.operation.success_count', result.Successful?.length || 0)\n * ctx.setAttribute('messaging.operation.failed_count', result.Failed?.length || 0)\n *\n * return result\n * })\n * ```\n *\n * @public\n */\nexport function traceMessaging(\n config: MessagingConfig,\n): <TArgs extends unknown[], TReturn>(\n factory: SemanticFactory<TArgs, TReturn>,\n) => SemanticHandler<TArgs, TReturn>;\nexport function traceMessaging<TArgs extends unknown[], TReturn>(\n config: MessagingConfig,\n fn: SemanticHandler<TArgs, TReturn>,\n): SemanticHandler<TArgs, TReturn>;\nexport function traceMessaging<TArgs extends unknown[], TReturn>(\n config: MessagingConfig,\n fn?: SemanticHandler<TArgs, TReturn>,\n):\n | SemanticHandler<TArgs, TReturn>\n | (<TFactoryArgs extends unknown[], TFactoryReturn>(\n factory: SemanticFactory<TFactoryArgs, TFactoryReturn>,\n ) => SemanticHandler<TFactoryArgs, TFactoryReturn>) {\n if (!config || typeof config !== 'object') {\n throw new TypeError('traceMessaging: config must be an object');\n }\n if (typeof config.system !== 'string' || config.system.trim() === '') {\n throw new TypeError(\n 'traceMessaging: config.system must be a non-empty string',\n );\n }\n const operation = config.operation ?? 'messaging';\n const spanName = [operation, config.destination].filter(Boolean).join(' ');\n const operationType = operation === 'publish' ? 'send' : operation;\n const spanKind =\n operation === 'publish'\n ? SpanKind.PRODUCER\n : operation === 'process'\n ? SpanKind.CONSUMER\n : operation === 'receive'\n ? SpanKind.CLIENT\n : SpanKind.INTERNAL;\n const configure = (ctx: TraceContext) => {\n ctx.setAttribute('messaging.system', config.system);\n if (config.operation) {\n ctx.setAttribute('messaging.operation.name', config.operation);\n ctx.setAttribute('messaging.operation.type', operationType);\n ctx.setAttribute('messaging.operation', config.operation);\n }\n if (config.destination) {\n ctx.setAttribute('messaging.destination.name', config.destination);\n }\n setConfiguredAttributes(ctx, config.attributes);\n };\n return wrapSemantic('traceMessaging', spanName, spanKind, configure, fn);\n}\n"],"mappings":"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"}