autotel
Version:
Write Once, Observe Anywhere
446 lines (443 loc) • 14.2 kB
JavaScript
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