UNPKG

autotel

Version:
446 lines (443 loc) 14.2 kB
import { getConfig } from "./config.js"; import { SpanStatusCode, context, createContextKey, trace } from "@opentelemetry/api"; //#region src/autotel-logger.ts /** * Zero-dependency structured logger for autotel * * This logger provides: * - Structured JSON logging (production) or pretty print (development) * - Auto trace context injection (traceId, spanId, correlationId) * - Dynamic log level control (per-request via OTel context) * - Level support (debug, info, warn, error, none) * - Zero additional dependencies (uses @opentelemetry/api, already a dep) * * Uses Pino-compatible signature supporting both patterns: * - `log.info('simple message')` - string-only * - `log.info({ userId: '123' }, 'message')` - object first with optional message * * Used as the default fallback when users don't provide Pino/Bunyan. * Can also be used directly: import { createBuiltinLogger } from 'autotel/logger' * * @example * ```typescript * import { createBuiltinLogger, runWithLogLevel } from 'autotel/logger'; * * const log = createBuiltinLogger('my-service'); * * // Simple message (no metadata) * log.info('Server started'); * * // With metadata (Pino-style: object first, message second) * log.info({ userId: '123' }, 'User created'); * // Output: {"level":"info","service":"my-service","msg":"User created","userId":"123","traceId":"..."} * * // Dynamic log level per-request * runWithLogLevel('debug', () => { * log.debug('This will log even if default level is "info"'); * }); * ``` */ /** * Context key for storing active log level (enables per-request log levels) */ const LOG_LEVEL_KEY = createContextKey("autotel-log-level"); /** * Get the active log level from context (if set) * Falls back to undefined if no log level is set in context */ function getActiveLogLevel() { return context.active().getValue(LOG_LEVEL_KEY); } /** * Run a function with a specific log level * The log level is stored in OpenTelemetry context and applies to all logger calls within the callback * * @example * ```typescript * // Enable debug logging for a specific request * runWithLogLevel('debug', () => { * log.debug('This will be logged'); * processRequest(); * }); * * // Disable logging temporarily * runWithLogLevel('none', () => { * log.info('This will NOT be logged'); * }); * ``` */ function runWithLogLevel(level, callback) { const ctx = context.active().setValue(LOG_LEVEL_KEY, level); return context.with(ctx, callback); } /** * Get current trace context from active span * Returns null if no active span exists */ function getTraceContextInternal() { const span = trace.getActiveSpan(); if (!span) return null; const ctx = span.spanContext(); return { traceId: ctx.traceId, spanId: ctx.spanId, correlationId: ctx.traceId.slice(0, 16) }; } /** * Helper to get trace context (useful for BYOL - Bring Your Own Logger) * * @example * ```typescript * import bunyan from 'bunyan'; * import { getTraceContext } from 'autotel/logger'; * * const bunyanLogger = bunyan.createLogger({ name: 'myapp' }); * const ctx = getTraceContext(); * bunyanLogger.info({ ...ctx, email: 'test@example.com' }, 'Creating user'); * ``` */ function getTraceContext() { return getTraceContextInternal(); } /** * Create a lightweight structured logger * * @param service - Service name for logging * @param options - Optional configuration * * @example * ```typescript * const log = createBuiltinLogger('user-service'); * * log.info('Creating user', { email: 'test@example.com' }); * // Output: {"level":"info","service":"user-service","msg":"Creating user", * // "email":"test@example.com","traceId":"...","spanId":"..."} * * // Dynamic log level control per-request * runWithLogLevel('debug', () => { * log.debug('This will be logged even if logger was created with level: "info"'); * }); * ``` */ function createBuiltinLogger(service, options) { const defaultLevel = options?.level || "info"; const pretty = options?.pretty || false; const levelPriority = { none: -1, debug: 0, info: 1, warn: 2, error: 3 }; const shouldLog = (level) => { const activeLevel = getActiveLogLevel() ?? defaultLevel; if (activeLevel === "none") return false; return levelPriority[level] >= levelPriority[activeLevel]; }; const log = (level, msg, attrs) => { if (!shouldLog(level)) return; const ctx = getTraceContextInternal(); const logEntry = { level, service, msg, ...attrs, ...ctx, timestamp: (/* @__PURE__ */ new Date()).toISOString() }; if (pretty) { const traceInfo = ctx ? ` [${ctx.traceId.slice(0, 8)}.../${ctx.spanId.slice(0, 8)}...]` : ""; console.log(`[${level.toUpperCase()}]${traceInfo} ${service}: ${msg}`, attrs || ""); } else console.log(JSON.stringify(logEntry)); }; const createLogMethod = (level) => { return (extraOrMessage, message) => { if (typeof extraOrMessage === "string") if (message !== void 0 && typeof message === "object") { if (process.env.NODE_ENV !== "production") console.warn(`[autotel] Legacy logger pattern detected: logger.${level}('message', metadata). Autotel recommends Pino signature: logger.${level}({ ...metadata }, 'message'). Auto-swapping arguments for compatibility.`); log(level, extraOrMessage, message); } else log(level, extraOrMessage); else log(level, message || "", extraOrMessage); }; }; return { info: createLogMethod("info"), warn: createLogMethod("warn"), debug: createLogMethod("debug"), error: (extraOrMessage, message) => { if (typeof extraOrMessage === "string") { if (message instanceof Error) { if (process.env.NODE_ENV !== "production") console.warn("[autotel] Legacy logger pattern detected: logger.error('message', error). Autotel recommends Pino signature: logger.error({ err: error }, 'message'). Auto-swapping arguments for compatibility."); log("error", extraOrMessage, { error: message.message, stack: message.stack, name: message.name }); return; } if (message !== void 0 && typeof message === "object") { if (process.env.NODE_ENV !== "production") console.warn("[autotel] Legacy logger pattern detected: logger.error('message', metadata). Autotel recommends Pino signature: logger.error({ ...metadata }, 'message'). Auto-swapping arguments for compatibility."); const { err, ...rest } = message; let errorAttrs = rest; if (err instanceof Error) errorAttrs = { error: err.message, stack: err.stack, name: err.name, ...rest }; else if (err !== void 0) errorAttrs = { err, ...rest }; log("error", extraOrMessage, errorAttrs); return; } log("error", extraOrMessage); return; } const { err, ...rest } = extraOrMessage; let errorAttrs = rest; if (err instanceof Error) errorAttrs = { error: err.message, stack: err.stack, name: err.name, ...rest }; else if (err !== void 0) errorAttrs = { err, ...rest }; log("error", message || "", errorAttrs); } }; } /** * Pino-like factory function for creating an autotel logger * * @example * ```typescript * import { autotelLogger } from 'autotel/logger'; * * const log = autotelLogger(); * * // Simple message * log.info('Server started'); * * // With metadata (Pino-style: object first, message second) * log.info({ userId: '123' }, 'User created'); * * // With options * const devLog = autotelLogger({ service: 'my-app', level: 'debug', pretty: true }); * ``` */ function autotelLogger(options) { return createBuiltinLogger(options?.service || "app", { level: options?.level, pretty: options?.pretty }); } //#endregion //#region src/logger.ts /** * Logger types and utilities for autotel * * **Zero-Config Option:** Don't provide a logger to `init()` and autotel uses * a built-in structured JSON logger with automatic trace context injection. * * **BYOL (Bring Your Own Logger):** Pass Pino or Bunyan to `init()` for * automatic instrumentation with trace context and OTLP log export. * * ## Logger Signature * * Autotel v2.10+ uses **Pino's signature**: `logger.info({ metadata }, 'message')`. * * ### Backward Compatibility * * The built-in logger auto-detects legacy Winston-style calls and swaps arguments: * ```typescript * // Legacy (auto-detected and handled) * logger.info('User created', { userId: '123' }); * // → Internally treated as: logger.info({ userId: '123' }, 'User created') * // → Logs warning in development, works silently in production * ``` * * ### Recommended Usage * * ```typescript * // ✅ Pino-style (preferred) * logger.info({ userId: '123' }, 'User created'); * * // ✅ Simple message (no metadata) * logger.info('Server started'); * ``` * * **Note:** If you BYOL (bring your own logger), it must use Pino signature. * Winston and other `(message, meta)` loggers are NOT compatible. * For Winston, use `@opentelemetry/instrumentation-winston` instead. * * @example Zero-config (uses built-in logger) * ```typescript * import { init } from 'autotel'; * * init({ service: 'my-app' }); * // Internal logs: {"level":"info","service":"my-app","msg":"...","traceId":"..."} * ``` * * @example Using built-in logger directly * ```typescript * import { createBuiltinLogger, runWithLogLevel } from 'autotel/logger'; * * const log = createBuiltinLogger('my-service'); * * // Simple message (no metadata) * log.info('Server started'); * * // With metadata (Pino-style: object first, message second) * log.info({ userId: '123' }, 'User created'); * // Output: {"level":"info","service":"my-service","msg":"User created","userId":"123","traceId":"..."} * * // Dynamic log level per-request * runWithLogLevel('debug', () => { * log.debug('Debug info for this request only'); * }); * ``` * * @example Using Pino (recommended for production, auto-instrumented) * ```typescript * import pino from 'pino'; // npm install pino * import { init } from 'autotel'; * * const logger = pino({ level: 'info' }); * init({ service: 'my-app', logger }); * * // Logs automatically include traceId/spanId and export via OTLP! * logger.info({ userId: '123' }, 'User created'); * ``` * * @example Using Bunyan (auto-instrumented, same signature as Pino) * ```typescript * import bunyan from 'bunyan'; // npm install bunyan @opentelemetry/instrumentation-bunyan * import { init } from 'autotel'; * import { BunyanInstrumentation } from '@opentelemetry/instrumentation-bunyan'; * * const logger = bunyan.createLogger({ name: 'my-app' }); * init({ * service: 'my-app', * logger, * instrumentations: [new BunyanInstrumentation()] * }); * ``` * * @example Custom logger (MUST use Pino-compatible signature) * ```typescript * // ⚠️ Your custom logger MUST accept (object, message?) signature * const logger = { * info: (extra, msg) => console.log(msg || '', extra), * warn: (extra, msg) => console.warn(msg || '', extra), * error: (extra, msg) => console.error(msg || '', extra), * debug: (extra, msg) => console.debug(msg || '', extra), * }; * init({ service: 'my-app', logger }); * ``` * * @example BYOL helper: inject trace context into any logger * ```typescript * import bunyan from 'bunyan'; * import { getTraceContext } from 'autotel/logger'; * * const bunyanLogger = bunyan.createLogger({ name: 'myapp' }); * const ctx = getTraceContext(); * bunyanLogger.info({ ...ctx, userId: '123' }, 'Creating user'); * ``` */ /** * Log level constants */ const LOG_LEVEL = { DEBUG: "debug", INFO: "info", WARN: "warn", ERROR: "error" }; /** * TS5+ Standard Decorator for logging and tracing operations * Uses TC39 Stage 3 decorator syntax * * This is the traditional per-method decorator approach. * For zero-boilerplate solution, see @Instrumented class decorator. * * @example * // Simple usage (Pino-style: object first, message second) * class OrderService { * constructor(private readonly deps: { log: Logger }) {} * * @LoggedOperation('order.create') * async createOrder(data: CreateOrderData) { * // ✅ Correct Pino-style logging * this.deps.log.info({ orderId: data.id }, 'Creating order'); * } * } * * // Advanced usage (future-proof for options) * @LoggedOperation({ operationName: 'order.create' }) * async createOrder(data: CreateOrderData) { } */ function LoggedOperation(operationNameOrOptions) { const operationName = typeof operationNameOrOptions === "string" ? operationNameOrOptions : operationNameOrOptions.operationName; return function(originalMethod, context) { const methodName = String(context.name); return async function(...args) { const log = this.deps?.log; const startTime = performance.now(); return getConfig().tracer.startActiveSpan(operationName, async (span) => { try { log?.info({ operation: operationName, method: methodName, args }, "Operation started"); const result = await originalMethod.apply(this, args); const duration = performance.now() - startTime; log?.info({ operation: operationName, method: methodName, duration }, "Operation completed"); span.setStatus({ code: SpanStatusCode.OK }); span.setAttributes({ "operation.name": operationName, "operation.method": methodName, "operation.duration": duration, "operation.success": true }); return result; } catch (error) { const duration = performance.now() - startTime; log?.error({ err: error instanceof Error ? error : void 0, operation: operationName, method: methodName, duration }, "Operation failed"); span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : "Unknown error" }); span.setAttributes({ "operation.name": operationName, "operation.method": methodName, "operation.duration": duration, "operation.success": false, "error.type": error instanceof Error ? error.constructor.name : "Unknown" }); throw error; } finally { span.end(); } }); }; }; } //#endregion export { getActiveLogLevel as a, createBuiltinLogger as i, LoggedOperation as n, getTraceContext as o, autotelLogger as r, runWithLogLevel as s, LOG_LEVEL as t }; //# sourceMappingURL=logger-D3Ej3DII.js.map