UNPKG

@delta-base/observability

Version:

Observability framework for delta-base applications

1,462 lines (1,455 loc) 65.9 kB
#!/usr/bin/env node "use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // package.json var package_default; var init_package = __esm({ "package.json"() { package_default = { name: "@delta-base/observability", version: "0.0.2", description: "Observability framework for delta-base applications", main: "dist/index.js", module: "dist/index.mjs", types: "dist/index.d.ts", exports: { ".": { types: "./dist/index.d.ts", import: "./dist/index.mjs", require: "./dist/index.js" }, "./scripts/*": { require: "./dist/scripts/*.js" } }, bin: { "collect-build-info": "./dist/scripts/collect-build-info.js" }, files: [ "dist", "LICENSE", "README.md" ], scripts: { typecheck: "tsc --noEmit", build: "tsup", dev: "tsup --watch", test: "vitest run --passWithNoTests", "test:watch": "vitest", prepublishOnly: "pnpm run build" }, keywords: [ "delta-base", "observability", "opentelemetry", "tracing", "monitoring", "logging" ], author: "Nibbio LLC", license: "LicenseRef-LICENSE", repository: { type: "git", url: "https://github.com/nibbio-co/delta-base.git", directory: "packages/libs/deltabase/observability" }, dependencies: { "@delta-base/toolkit": "workspace:*", "@opentelemetry/api": "^1.9.0", "@opentelemetry/sdk-trace-base": "^1.30.1", hono: "catalog:", zod: "catalog:" }, devDependencies: { "@types/node": "^20.0.0", "@hono/zod-openapi": "catalog:", tsup: "^8.3.6", typescript: "^5.0.0", vitest: "^3.1.1" }, peerDependencies: { "@hono/zod-openapi": "^0.17.1" }, peerDependenciesMeta: { "@hono/zod-openapi": { optional: true } }, engines: { node: ">=16.0.0" }, publishConfig: { access: "public" }, sideEffects: false }; } }); // src/config.ts var config_exports = {}; __export(config_exports, { DEFAULT_CONTEXT_SCHEMA: () => DEFAULT_CONTEXT_SCHEMA, DEFAULT_ERROR_PATTERNS: () => DEFAULT_ERROR_PATTERNS, DEFAULT_OBSERVABILITY_CONFIG: () => DEFAULT_OBSERVABILITY_CONFIG, ObservabilityConfigManager: () => ObservabilityConfigManager, configureObservability: () => configureObservability, configureObservabilityAdvanced: () => configureObservabilityAdvanced, createObservabilityConfig: () => createObservabilityConfig, observabilityConfig: () => observabilityConfig }); function detectEnvironment() { if (process.env.NODE_ENV) { return process.env.NODE_ENV; } if (process.env.CF_ENVIRONMENT) { return process.env.CF_ENVIRONMENT; } if (process.env.ENVIRONMENT) { return process.env.ENVIRONMENT; } if (process.env.NODE_ENV === void 0 && (process.env.DEVELOPMENT || process.env.DEV)) { return "development"; } return "development"; } function createObservabilityConfig(options = {}) { const config = { ...DEFAULT_OBSERVABILITY_CONFIG }; if (options.serviceName) { config.tracing.serviceName = options.serviceName; } if (options.autoTrackHttp !== void 0) { config.http.trackRequests = options.autoTrackHttp; config.http.trackResponses = options.autoTrackHttp; } if (options.autoTrackErrorResponses !== void 0) { config.errors.autoTrackErrorResponses = options.autoTrackErrorResponses; } if (options.debug !== void 0) { config.debug = options.debug; } return config; } function configureObservability(options) { const config = createObservabilityConfig(options); observabilityConfig.updateConfig(config); } function configureObservabilityAdvanced(config) { observabilityConfig.updateConfig(config); } var DEFAULT_CONTEXT_SCHEMA, DEFAULT_ERROR_PATTERNS, DEFAULT_OBSERVABILITY_CONFIG, ObservabilityConfigManager, observabilityConfig; var init_config = __esm({ "src/config.ts"() { "use strict"; init_package(); DEFAULT_CONTEXT_SCHEMA = { user: ["id", "email", "first_name", "last_name", "actor_type"], entity: ["type", "id", "status"], operation: ["type", "name", "domain", "layer"], infrastructure: [ "event_store.stream_id", "event_store.expected_version", "event_store.next_version", "event_store.created_new_stream", "database.operation", "database.table", "database.duration_ms", "external_service.name", "external_service.operation", "external_service.duration_ms" ], error: [ "type", "message", "unexpected", "validation.issue_count", "validation.fields", "business_rule.rule", "business_rule.violation" ], http: [ "method", "path", "status_code", "user_agent", "content_type", "content_length" ], performance: [ "duration_ms", "category", "timing.total_ms", "timing.domain_ms", "timing.validation_ms", "timing.event_store_ms", "timing.database_ms" ], service: ["name", "version", "environment", "build_info"] }; DEFAULT_ERROR_PATTERNS = [ { name: "OpenAPIHono_ZodValidation", matcher: (body) => { var _a, _b; return (body == null ? void 0 : body.success) === false && ((_a = body == null ? void 0 : body.error) == null ? void 0 : _a.name) === "ZodError" && Array.isArray((_b = body == null ? void 0 : body.error) == null ? void 0 : _b.issues); }, tracker: (wideEvent, body) => { wideEvent.trackError({ type: "ValidationError", message: "Request validation failed", validation: { issueCount: body.error.issues.length, fields: body.error.issues.map( (issue) => Array.isArray(issue.path) ? issue.path.join(".") : String(issue.path) ) }, unexpected: false // Expected - user submitted invalid data }); } }, { name: "BusinessValidation", matcher: (body) => (body == null ? void 0 : body.success) === false && typeof (body == null ? void 0 : body.message) === "string" && !(body == null ? void 0 : body.error), // No nested error object tracker: (wideEvent, body) => { wideEvent.trackError({ type: "BusinessValidationError", message: body.message, businessRule: { rule: "business_validation", violation: body.message }, unexpected: false // Expected - business rule violation }); } }, { name: "GenericError", matcher: (body) => (body == null ? void 0 : body.success) === false || (body == null ? void 0 : body.error) || typeof (body == null ? void 0 : body.message) === "string", tracker: (wideEvent, body) => { var _a; wideEvent.trackError({ type: "GenericError", message: (body == null ? void 0 : body.message) || ((_a = body == null ? void 0 : body.error) == null ? void 0 : _a.message) || "Unknown error", unexpected: true // Unexpected - could be system issue }); } } ]; DEFAULT_OBSERVABILITY_CONFIG = { contextSchema: DEFAULT_CONTEXT_SCHEMA, tracing: { enabled: true, serviceName: process.env.SERVICE_NAME || package_default.name, serviceVersion: process.env.SERVICE_VERSION || package_default.version, environment: detectEnvironment(), defaultAttributes: {} }, http: { trackRequests: true, trackResponses: true, trackBodies: false, maxBodySize: 1024 * 1024, // 1MB excludeHeaders: ["authorization", "cookie", "x-api-key"], includeHeaders: void 0 }, errors: { autoTrackErrorResponses: true, autoCategorizeErrors: true, errorPatterns: DEFAULT_ERROR_PATTERNS, defaultUnexpected: true }, performance: { trackTiming: true, categorizePerformance: true, performanceThresholds: { fast: 100, slow: 1e3 }, autoCreateSpans: true }, debug: false }; ObservabilityConfigManager = class { config; constructor(config = DEFAULT_OBSERVABILITY_CONFIG) { this.config = { ...config }; } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Update configuration */ updateConfig(updates) { this.config = { ...this.config, ...updates }; } /** * Add custom context schema fields */ addContextFields(category, fields) { this.config.contextSchema[category] = [ ...this.config.contextSchema[category], ...fields ]; } /** * Add custom error pattern */ addErrorPattern(pattern) { this.config.errors.errorPatterns.push(pattern); } /** * Remove error pattern by name */ removeErrorPattern(name) { this.config.errors.errorPatterns = this.config.errors.errorPatterns.filter( (pattern) => pattern.name !== name ); } /** * Get context schema for a category */ getContextSchema(category) { return [...this.config.contextSchema[category]]; } /** * Get all context schema fields (flattened) */ getAllContextFields() { return Object.values(this.config.contextSchema).flat(); } /** * Get error patterns */ getErrorPatterns() { return [...this.config.errors.errorPatterns]; } }; observabilityConfig = new ObservabilityConfigManager(); } }); // src/wide-events.ts var wide_events_exports = {}; __export(wide_events_exports, { DEFAULT_BUILD_INFO: () => DEFAULT_BUILD_INFO, WideEventsWrapper: () => WideEventsWrapper, createOperationTracker: () => createOperationTracker, createUserOperationTracker: () => createUserOperationTracker, getBuildInfo: () => getBuildInfo, initializeBuildInfo: () => initializeBuildInfo, setBuildInfo: () => setBuildInfo }); function setBuildInfo(buildInfo) { customBuildInfo = buildInfo; } function getBuildInfo() { return { ...DEFAULT_BUILD_INFO, ...customBuildInfo }; } function initializeBuildInfo(buildInfo) { setBuildInfo(buildInfo); } function createUserOperationTracker(operationName) { return new WideEventsWrapper( `User ${operationName}`, `user.${operationName.toLowerCase()}` ); } function createOperationTracker(entity, operation) { return new WideEventsWrapper( `${entity} ${operation}`, `${entity.toLowerCase()}.${operation.toLowerCase()}` ); } var import_api, DEFAULT_BUILD_INFO, customBuildInfo, WideEventsWrapper; var init_wide_events = __esm({ "src/wide-events.ts"() { "use strict"; import_api = require("@opentelemetry/api"); DEFAULT_BUILD_INFO = { "service.name": "unknown-service", "service.version": "0.0.0", "service.environment": "development" }; customBuildInfo = {}; WideEventsWrapper = class { requestId; rootSpan; trackedData = {}; hasError = false; /** * Creates a new wide events wrapper for an operation. * * @param operationName - Human-readable name for the operation (e.g., "Create User") * @param operationType - Machine-readable operation type (e.g., "user.create") * @param serviceName - Name of the service (defaults to environment variable) */ constructor(operationName, operationType, serviceName = process.env.SERVICE_NAME || "unknown-service") { this.requestId = crypto.randomUUID(); const buildInfo = getBuildInfo(); const tracer = import_api.trace.getTracer( serviceName, buildInfo["service.version"] ); this.rootSpan = tracer.startSpan(operationName, { kind: import_api.SpanKind.SERVER, attributes: { "request.id": this.requestId, "operation.type": operationType, "operation.name": operationName, ...buildInfo } }); this.trackedData = { request_id: this.requestId, operation: operationType }; } /** * Set the operation type and context for this operation. * * This is the primary method for identifying what operation is being performed. * It sets both the operation type identifier and any contextual metadata. * * @param operationType - The operation type identifier (e.g., "user.create", "user.change-password") * @param context - Optional contextual information about the operation * @returns This instance for method chaining * * @example * ```typescript * // Simple operation * tracker.setOperation('user.create'); * * // With context * tracker.setOperation('user.create', { * domain: 'user-management', * layer: 'api' * }); * * // With custom properties * tracker.setOperation('user.change-password', { * domain: 'user-management', * layer: 'business-logic', * trigger: 'user-initiated' * }); * ``` */ setOperation(operationType, context3) { const attributes = { "operation.type": operationType }; if (context3) { if (context3.domain) attributes["operation.domain"] = context3.domain; if (context3.layer) attributes["operation.layer"] = context3.layer; for (const [key, value] of Object.entries(context3)) { if (value !== void 0 && !["domain", "layer"].includes(key)) { attributes[`operation.${key}`] = value; } } } this.rootSpan.setAttributes(attributes); Object.assign(this.trackedData, { operation: operationType, ...attributes }); return this; } /** * Track user/actor information for this operation. * * Use this to record who is performing the operation, including * user details, authentication context, and actor type. * * @param userData - User and actor information * @returns This instance for method chaining * * @example * ```typescript * tracker.trackUser({ * id: '123', * email: 'john@example.com', * actorType: 'authenticated_user' * }); * ``` */ trackUser(userData) { const attributes = {}; if (userData.id) attributes["user.id"] = userData.id; if (userData.email) attributes["user.email"] = userData.email; if (userData.firstName) attributes["user.first_name"] = userData.firstName; if (userData.lastName) attributes["user.last_name"] = userData.lastName; if (userData.actorType) attributes["actor.type"] = userData.actorType; for (const [key, value] of Object.entries(userData)) { if (value !== void 0 && !["id", "email", "firstName", "lastName", "actorType"].includes(key)) { attributes[`user.${key}`] = value; } } this.rootSpan.setAttributes(attributes); Object.assign(this.trackedData, attributes); return this; } /** * Track the primary entity/resource being operated on. * * Use this to record what thing your operation is working with, * such as a user being created, an order being processed, etc. * * @param entityData - Entity/resource information * @returns This instance for method chaining * * @example * ```typescript * tracker.trackEntity({ * type: 'user', * id: 'user-123', * status: 'active' * }); * ``` */ trackEntity(entityData) { const attributes = { "entity.type": entityData.type, "entity.id": entityData.id }; for (const [key, value] of Object.entries(entityData)) { if (value !== void 0 && !["type", "id"].includes(key)) { attributes[`entity.${key}`] = value; } } this.rootSpan.setAttributes(attributes); Object.assign(this.trackedData, attributes); return this; } /** * Track infrastructure and technical details. * * Use this to record technical aspects of your operation, * such as database operations, external service calls, etc. * * @param infrastructureData - Infrastructure operation details * @returns This instance for method chaining * * @example * ```typescript * tracker.trackInfrastructure({ * database: { * operation: 'INSERT', * table: 'users', * duration: 45 * }, * eventStore: { * streamId: 'user-123', * expectedVersion: 0 * } * }); * ``` */ trackInfrastructure(infrastructureData) { const attributes = {}; if (infrastructureData.database) { const db = infrastructureData.database; if (db.operation) attributes["db.operation"] = db.operation; if (db.table) attributes["db.table"] = db.table; if (db.query) attributes["db.query"] = db.query; if (db.duration) attributes["db.duration_ms"] = db.duration; } if (infrastructureData.eventStore) { const es = infrastructureData.eventStore; if (es.streamId) attributes["event_store.stream_id"] = es.streamId; if (es.expectedVersion !== void 0) attributes["event_store.expected_version"] = es.expectedVersion; if (es.nextVersion !== void 0) attributes["event_store.next_version"] = es.nextVersion; if (es.createdNewStream !== void 0) attributes["event_store.created_new_stream"] = es.createdNewStream; } if (infrastructureData.externalService) { const ext = infrastructureData.externalService; if (ext.name) attributes["external_service.name"] = ext.name; if (ext.operation) attributes["external_service.operation"] = ext.operation; if (ext.duration) attributes["external_service.duration_ms"] = ext.duration; } for (const [key, value] of Object.entries(infrastructureData)) { if (value !== void 0 && !["database", "eventStore", "externalService"].includes(key)) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { attributes[`infrastructure.${key}`] = value; } } } this.rootSpan.setAttributes(attributes); Object.assign(this.trackedData, attributes); return this; } /** * Track error information. * * Use this to record error details, including error type, * validation failures, business rule violations, etc. * * @param errorData - Error information * @returns This instance for method chaining * * @example * ```typescript * tracker.trackError({ * type: 'ValidationError', * message: 'Email is required', * validation: { * issueCount: 2, * fields: ['email', 'firstName'] * } * }); * ``` */ trackError(errorData) { const attributes = {}; if (errorData.type) attributes["error.type"] = errorData.type; if (errorData.message) attributes["error.message"] = errorData.message; if (errorData.unexpected !== void 0) attributes["error.unexpected"] = errorData.unexpected; if (errorData.validation) { const val = errorData.validation; if (val.issueCount) attributes["error.validation.issue_count"] = val.issueCount; if (val.fields) attributes["error.validation.fields"] = val.fields.join(","); } if (errorData.businessRule) { const rule = errorData.businessRule; if (rule.rule) attributes["error.business_rule.rule"] = rule.rule; if (rule.violation) attributes["error.business_rule.violation"] = rule.violation; } for (const [key, value] of Object.entries(errorData)) { if (value !== void 0 && ![ "type", "message", "unexpected", "validation", "businessRule" ].includes(key)) { if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { attributes[`error.${key}`] = value; } } } this.rootSpan.setAttributes(attributes); Object.assign(this.trackedData, attributes); this.rootSpan.setStatus({ code: import_api.SpanStatusCode.ERROR, message: errorData.message || "Operation failed" }); this.hasError = true; return this; } /** * Record a business event during the operation. * * Use this to record significant business events that occur * during your operation, such as "validation.completed" or * "user.created". * * @param eventName - Name of the business event * @param eventData - Additional event data * @returns This instance for method chaining * * @example * ```typescript * tracker.recordEvent('user.created', { * userId: '123', * source: 'api' * }); * ``` */ recordEvent(eventName, eventData = {}) { this.rootSpan.addEvent(eventName, eventData); return this; } /** * Create a child span for a sub-operation. * * Use this to create detailed spans for specific parts of your * operation, such as validation, database operations, etc. * * @param name - Name of the sub-operation * @param attributes - Additional span attributes * @returns OpenTelemetry span for the sub-operation * * @example * ```typescript * const validationSpan = tracker.createOperationSpan('validation', { * 'operation.layer': 'validation' * }); * * try { * // validation logic * validationSpan.setStatus({ code: SpanStatusCode.OK }); * } catch (error) { * validationSpan.recordException(error); * } finally { * validationSpan.end(); * } * ``` */ createOperationSpan(name, attributes = {}) { const buildInfo = getBuildInfo(); const tracer = import_api.trace.getTracer( process.env.SERVICE_NAME || "unknown-service", buildInfo["service.version"] ); return tracer.startSpan( name, { kind: import_api.SpanKind.INTERNAL, attributes }, import_api.trace.setSpan(import_api.context.active(), this.rootSpan) ); } /** * Automatically track HTTP request details. * * This method extracts and tracks HTTP-specific information * from the request, including method, URL, headers, etc. * * @param request - HTTP request object * @returns This instance for method chaining * * @internal This method is typically called by middleware */ trackHttpRequest(request) { const url = new URL(request.url); this.rootSpan.setAttributes({ "http.method": request.method, "http.url": request.url, "http.scheme": url.protocol.slice(0, -1), "http.host": url.host, "http.target": url.pathname + url.search, "http.user_agent": request.headers.get("user-agent") || "", "http.request_content_length": request.headers.get("content-length") || "" }); this.rootSpan.addEvent("http.request_received", { "http.method": request.method, "http.path": url.pathname }); return this; } /** * Automatically track HTTP response details. * * This method extracts and tracks HTTP response information, * including status code, headers, and sets appropriate span status. * * @param response - HTTP response object * @returns This instance for method chaining * * @internal This method is typically called by middleware */ trackHttpResponse(response) { this.rootSpan.setAttributes({ "http.status_code": response.status, "http.status_text": response.statusText, "http.response_content_length": response.headers.get("content-length") || "" }); this.trackedData["http.status_code"] = response.status; if (!this.hasError) { if (response.status >= 400) { this.rootSpan.setStatus({ code: import_api.SpanStatusCode.ERROR, message: `HTTP ${response.status}` }); } else { this.rootSpan.setStatus({ code: import_api.SpanStatusCode.OK }); } } else { if (response.status >= 200 && response.status < 300) { this.rootSpan.setStatus({ code: import_api.SpanStatusCode.OK }); this.hasError = false; } } this.rootSpan.addEvent("http.response_sent", { "http.status_code": response.status, success: response.status >= 200 && response.status < 300 }); return this; } /** * Execute the operation with proper OpenTelemetry context. * * This method wraps your operation in the appropriate OpenTelemetry * context and ensures spans are properly managed and exported. * * @param fn - Operation function to execute * @returns Promise with the operation result * * @example * ```typescript * const result = await tracker.execute(async (t) => { * t.trackUser({ id: '123' }); * // Your operation logic here * return { success: true }; * }); * ``` */ async execute(fn) { return import_api.context.with( import_api.trace.setSpan(import_api.context.active(), this.rootSpan), async () => { try { const result = await fn(this); if (!this.trackedData["http.status_code"]) { this.rootSpan.setStatus({ code: import_api.SpanStatusCode.OK }); } return result; } catch (error) { if (error instanceof Error) { this.rootSpan.recordException(error); this.rootSpan.setStatus({ code: import_api.SpanStatusCode.ERROR, message: error.message }); } throw error; } finally { this.rootSpan.end(); } } ); } /** * Get all tracked data for this operation. * * @returns Copy of all tracked data * * @internal Primarily used for testing and debugging */ getTrackedData() { return { ...this.trackedData }; } }; } }); // src/index.ts var index_exports = {}; __export(index_exports, { DEFAULT_BUILD_INFO: () => DEFAULT_BUILD_INFO, DEFAULT_CONTEXT_SCHEMA: () => DEFAULT_CONTEXT_SCHEMA, DEFAULT_ERROR_PATTERNS: () => DEFAULT_ERROR_PATTERNS, DEFAULT_EXPECTED_ERROR_GUARDS: () => DEFAULT_EXPECTED_ERROR_GUARDS, DEFAULT_OBSERVABILITY_CONFIG: () => DEFAULT_OBSERVABILITY_CONFIG, ObservabilityConfigManager: () => ObservabilityConfigManager, WideEventsWrapper: () => WideEventsWrapper, addContextFields: () => addContextFields, addErrorPattern: () => addErrorPattern, categorizeError: () => categorizeError, configureObservability: () => configureObservability, configureObservabilityAdvanced: () => configureObservabilityAdvanced, createErrorGuard: () => createErrorGuard, createObservabilityConfig: () => createObservabilityConfig, createObservabilityErrorHandler: () => createObservabilityErrorHandler, createObservabilityMiddleware: () => createObservabilityMiddleware, createOperationTracker: () => createOperationTracker, createSimpleTracker: () => createSimpleTracker, createSpan: () => createSpan, createUserOperationTracker: () => createUserOperationTracker, default: () => index_default, extractUserContext: () => extractUserContext, getBuildInfo: () => getBuildInfo, handleAndTrackError: () => handleAndTrackError, initializeBuildInfo: () => initializeBuildInfo, initializeTracing: () => initializeTracing, isExpectedError: () => isExpectedError, observabilityConfig: () => observabilityConfig, observabilityMiddleware: () => observabilityMiddleware, quickStart: () => quickStart, setBuildInfo: () => setBuildInfo, trackOperationTiming: () => trackOperationTiming, trackUserFromSources: () => trackUserFromSources, withDatabaseTracking: () => withDatabaseTracking, withDomainTracking: () => withDomainTracking, withEventStoreTracking: () => withEventStoreTracking, withExternalServiceTracking: () => withExternalServiceTracking, withSpan: () => withSpan, withValidationTracking: () => withValidationTracking }); module.exports = __toCommonJS(index_exports); init_config(); // src/middleware.ts var import_toolkit = require("@delta-base/toolkit"); var import_factory = require("hono/factory"); var import_http_exception = require("hono/http-exception"); var import_zod = require("zod"); init_config(); // src/tracing.ts var import_api2 = require("@opentelemetry/api"); var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base"); init_wide_events(); var WideEventExporter = class { // Store child spans until their parent is exported childSpanCache = /* @__PURE__ */ new Map(); // biome-ignore lint/suspicious/noExplicitAny: do not know the type of the result export(spans, resultCallback) { spans.forEach((span) => { var _a; if (span.parentSpanId) { const parentId = span.parentSpanId; if (!this.childSpanCache.has(parentId)) { this.childSpanCache.set(parentId, []); } (_a = this.childSpanCache.get(parentId)) == null ? void 0 : _a.push(span); } else { const spanId = span.spanContext().spanId; const childSpans = this.childSpanCache.get(spanId) || []; const allSpans = [span, ...childSpans]; const wideEvent = this.spanToWideEvent(span, allSpans); const success = this.isSuccessfulSpan(span); const logEntry = { level: success ? "info" : "error", summary: this.generateSummary(span, wideEvent), performance_category: this.categorizePerformance( wideEvent["timing.total_ms"] ), error_category: success ? null : this.categorizeError(span, wideEvent), ...wideEvent }; if (success) { console.log(logEntry); } else { console.error(logEntry); } this.childSpanCache.delete(spanId); } }); resultCallback({ code: 0 /* SUCCESS */ }); } shutdown() { return Promise.resolve(); } isSuccessfulSpan(span) { const httpStatus = span.attributes["http.status_code"]; if (httpStatus) { return httpStatus >= 200 && httpStatus < 400; } return span.status.code !== import_api2.SpanStatusCode.ERROR; } spanToWideEvent(rootSpan, allSpans) { const attributes = rootSpan.attributes || {}; const childSpans = allSpans.filter( (span) => span.parentSpanId === rootSpan.spanContext().spanId ); const timing = this.extractLegacyTiming(childSpans); const totalDurationMs = this.calculateDuration(rootSpan); const httpContext = this.extractHttpContext(attributes); const businessContext = this.extractBusinessContext(attributes); const traceContext = this.extractTraceContext(rootSpan); return { // Request and operation context request_id: attributes["request.id"], timestamp: new Date( rootSpan.startTime[0] * 1e3 + rootSpan.startTime[1] / 1e6 ).toISOString(), operation: attributes["operation.type"], success: this.isSuccessfulSpan(rootSpan), // HTTP context ...httpContext, // Trace context ...traceContext, // Business context (user, resource, domain-specific data) ...businessContext, // Legacy timing for backward compatibility ...timing, "timing.total_ms": totalDurationMs, // Comprehensive span information spans: this.formatSpansHierarchy(rootSpan, allSpans), // Build information ...getBuildInfo(), // Error context if applicable ...rootSpan.status.code === import_api2.SpanStatusCode.ERROR && { error: this.extractDetailedError(attributes, rootSpan) } }; } // biome-ignore lint/suspicious/noExplicitAny: do not know the type of the result extractHttpContext(attributes) { const httpKeys = [ "http.method", "http.url", "http.scheme", "http.host", "http.target", "http.status_code", "http.status_text", "http.user_agent", "http.request_content_length", "http.response_content_length" ]; const httpContext = {}; httpKeys.forEach((key) => { if (attributes[key] !== void 0) { httpContext[key] = attributes[key]; } }); if (attributes["http.target"]) { httpContext["http.path"] = new URL( attributes["http.target"], "http://localhost" ).pathname; } return httpContext; } // biome-ignore lint/suspicious/noExplicitAny: do not know the type of the result extractBusinessContext(attributes) { const { observabilityConfig: observabilityConfig2 } = (init_config(), __toCommonJS(config_exports)); const businessKeys = observabilityConfig2.getAllContextFields(); const businessContext = {}; businessKeys.forEach((key) => { if (attributes[key] !== void 0) { businessContext[key] = attributes[key]; } }); return businessContext; } extractTraceContext(span) { return { "trace.trace_id": span.spanContext().traceId, "trace.span_id": span.spanContext().spanId, ...span.parentSpanId && { "trace.parent_span_id": span.parentSpanId } }; } extractLegacyTiming(childSpans) { const timing = {}; childSpans.forEach((span) => { const spanType = span.attributes["operation.layer"]; if (spanType) { const duration = this.calculateDuration(span); timing[`timing.${spanType}_ms`] = duration; } }); return timing; } calculateDuration(span) { if (!span.endTime) return 0; const startMs = span.startTime[0] * 1e3 + span.startTime[1] / 1e6; const endMs = span.endTime[0] * 1e3 + span.endTime[1] / 1e6; return Math.round((endMs - startMs) * 100) / 100; } formatSpansHierarchy(_rootSpan, allSpans) { return allSpans.map((span) => ({ name: span.name, span_id: span.spanContext().spanId, parent_span_id: span.parentSpanId || void 0, start_time: new Date( span.startTime[0] * 1e3 + span.startTime[1] / 1e6 ).toISOString(), end_time: span.endTime ? new Date( span.endTime[0] * 1e3 + span.endTime[1] / 1e6 ).toISOString() : void 0, duration_ms: this.calculateDuration(span), status: span.status.code === import_api2.SpanStatusCode.ERROR ? "error" : "ok", attributes: { ...span.attributes }, events: span.events.map((event) => ({ timestamp: new Date( event.time[0] * 1e3 + event.time[1] / 1e6 ).toISOString(), name: event.name, attributes: event.attributes || {} })) })); } // biome-ignore lint/suspicious/noExplicitAny: do not know the type of the result generateSummary(span, wideEvent) { const summaryParts = []; if (wideEvent.request_id) { summaryParts.push(`Request ID: ${wideEvent.request_id}`); } if (wideEvent.operation) { summaryParts.push(`Operation: ${wideEvent.operation}`); } if (wideEvent["timing.total_ms"]) { summaryParts.push(`Duration: ${wideEvent["timing.total_ms"]}ms`); } if (span.status.code === import_api2.SpanStatusCode.ERROR) { summaryParts.push(`Error: ${span.status.message || "Unknown error"}`); } return summaryParts.join(" - "); } categorizePerformance(durationMs) { if (durationMs < 100) { return "fast"; } if (durationMs < 1e3) { return "moderate"; } return "slow"; } extractDetailedError(attributes, span) { if (attributes["error.type"]) { const error = { type: attributes["error.type"], message: attributes["error.message"] || span.status.message || "Unknown error" }; if (attributes["error.validation.issue_count"]) { error.validation = { issueCount: attributes["error.validation.issue_count"], fields: attributes["error.validation.fields"] ? attributes["error.validation.fields"].split(",") : [] }; } if (attributes["error.business_rule.rule"]) { error.businessRule = { rule: attributes["error.business_rule.rule"], violation: attributes["error.business_rule.violation"] }; } if (attributes["error.unexpected"] !== void 0) { error.unexpected = attributes["error.unexpected"]; } return error; } return { type: "SpanError", message: span.status.message || "Unknown error" }; } // biome-ignore lint/suspicious/noExplicitAny: do not know the type of the result categorizeError(span, wideEvent) { if (span.status.code === import_api2.SpanStatusCode.ERROR) { if (wideEvent["error.validation.issue_count"]) { return "validation_error"; } if (wideEvent["error.business_rule.violation"] === "already_exists") { return "already_exists"; } if (wideEvent["error.unexpected"]) { return "unexpected_error"; } return "application_error"; } return null; } }; var tracerProvider = null; function initializeTracing() { if (tracerProvider) { return; } console.log( "OpenTelemetry tracing initialized with build info:", getBuildInfo()["service.name"], getBuildInfo()["git.commit_hash_short"] ); const wideEventExporter = new WideEventExporter(); const spanProcessor = new import_sdk_trace_base.SimpleSpanProcessor(wideEventExporter); tracerProvider = new import_sdk_trace_base.BasicTracerProvider({ spanProcessors: [spanProcessor] }); tracerProvider.register(); console.log("Wide Events exporter configured and ready"); } function createSpan(options) { const tracer = import_api2.trace.getTracer( "delta-base-auth", String(getBuildInfo()["service.version"]) ); const span = options.parentSpan ? tracer.startSpan( options.name, { kind: options.kind || import_api2.SpanKind.INTERNAL, attributes: options.attributes }, import_api2.trace.setSpan(import_api2.context.active(), options.parentSpan) ) : tracer.startSpan(options.name, { kind: options.kind || import_api2.SpanKind.INTERNAL, attributes: options.attributes }); return span; } function withSpan(span, fn) { return import_api2.context.with(import_api2.trace.setSpan(import_api2.context.active(), span), async () => { try { const result = await fn(span); span.setStatus({ code: import_api2.SpanStatusCode.OK }); return result; } catch (error) { span.setStatus({ code: import_api2.SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) }); span.recordException( error instanceof Error ? error : new Error(String(error)) ); throw error; } finally { span.end(); } }); } // src/middleware.ts init_wide_events(); function observabilityMiddleware(options = {}) { if (options.buildInfo) { initializeBuildInfo(options.buildInfo); } if (Object.keys(options).length > 0) { const config2 = createObservabilityConfig(options); observabilityConfig.updateConfig(config2); } const config = observabilityConfig.getConfig(); const serviceName = config.tracing.serviceName || "unknown-service"; const autoTrackHttp = config.http.trackRequests && config.http.trackResponses; const autoTrackErrorResponses = config.errors.autoTrackErrorResponses; return (0, import_factory.createMiddleware)(async (c, next) => { initializeTracing(); const method = c.req.method; const path = c.req.path; const operationName = `${method} ${path}`; const operationType = simpleOperationTypeDerivation(method, path); const wideEvent = new WideEventsWrapper( operationName, operationType, serviceName ); if (autoTrackHttp) { wideEvent.trackHttpRequest(c.req.raw); } c.set("wideEvent", wideEvent); await wideEvent.execute(async () => { await next(); if (autoTrackHttp && c.res) { wideEvent.trackHttpResponse(c.res); } if (autoTrackErrorResponses && c.res && isErrorResponse(c.res.status)) { await detectAndTrackErrorResponse(wideEvent, c.res); } }); }); } function simpleOperationTypeDerivation(method, path) { const resourceMatch = path.match(/^\/([^/?]+)/); let resource = (resourceMatch == null ? void 0 : resourceMatch[1]) || "unknown"; if (resource.endsWith("s") && resource.length > 1) { resource = resource.slice(0, -1); } const methodMap = { GET: "read", POST: "create", PUT: "update", PATCH: "update", DELETE: "delete" }; const action = methodMap[method.toUpperCase()] || "operation"; return `${resource}.${action}`; } function isErrorResponse(status) { return status >= 400; } async function detectAndTrackErrorResponse(wideEvent, response) { try { const contentType = response.headers.get("content-type"); if (!(contentType == null ? void 0 : contentType.includes("application/json"))) { return; } const clonedResponse = response.clone(); const responseBody = await clonedResponse.json(); const config = observabilityConfig.getConfig(); for (const pattern of config.errors.errorPatterns) { if (pattern.matcher(responseBody)) { console.log(`Detected ${pattern.name} error response, tracking...`); pattern.tracker(wideEvent, responseBody); return; } } const isClientError = response.status >= 400 && response.status < 500; wideEvent.trackError({ type: "UnknownError", message: `HTTP ${response.status}`, httpStatus: response.status, unexpected: !isClientError // 4xx = expected (client error), 5xx = unexpected (server error) }); } catch (error) { console.log("Failed to parse response body for error detection:", error); } } function createObservabilityErrorHandler(options = {}) { if (Object.keys(options).length > 0) { const config2 = createObservabilityConfig(options); observabilityConfig.updateConfig(config2); } const config = observabilityConfig.getConfig(); const serviceName = config.tracing.serviceName || "unknown-service"; return (error, c) => { const wideEvent = c.get("wideEvent"); if (wideEvent) { trackErrorWithContext(wideEvent, error); } else { const method = c.req.method; const path = c.req.path; const operationName = `${method} ${path}`; const operationType = simpleOperationTypeDerivation(method, path); const errorTracker = new WideEventsWrapper( operationName, operationType, serviceName ); trackErrorWithContext(errorTracker, error); errorTracker.execute(async () => { }).catch(console.error); } return createErrorResponse(error, c); }; } function trackErrorWithContext(wideEvent, error) { var _a, _b, _c, _d, _e; if (error instanceof import_zod.ZodError) { wideEvent.trackError({ type: "ValidationError", message: "Request validation failed", validation: { issueCount: error.issues.length, fields: error.issues.map((issue) => issue.path.join(".")) }, unexpected: false // Expected - user submitted invalid data }); } else if (error instanceof import_toolkit.ValidationError) { wideEvent.trackError({ type: "BusinessValidationError", message: error.message, businessRule: { rule: "business_validation", violation: error.message }, unexpected: false // Expected - business rule violation }); } else if (error instanceof import_toolkit.IllegalStateError) { const alreadyExists = error.message.includes("already exists"); wideEvent.trackError({ type: "BusinessRuleViolation", message: error.message, businessRule: { rule: "illegal_state", violation: alreadyExists ? "already_exists" : "illegal_state" }, unexpected: false // Expected - business constraint violation }); } else if (error instanceof import_http_exception.HTTPException) { const isClientError = error.status >= 400 && error.status < 500; wideEvent.trackError({ type: "HTTPException", message: error.message, httpStatus: error.status, unexpected: !isClientError // 4xx = expected (client error), 5xx = unexpected (server error) }); } else if ((0, import_toolkit.isDeltaBaseError)(error)) { const isServerError = error.status >= 500; const unexpected = isServerError || ["INTERNAL_SERVER_ERROR", "STORAGE_ERROR", "TIMEOUT_ERROR"].includes( error.errorType ); const errorData = { type: error.errorType, message: error.message, httpStatus: error.status, unexpected }; if (error.details && Object.keys(error.details).length > 0) { errorData.details = error.details; } if (error.errorType === "Version conflict" || error.errorType === "STREAM_VERSION_CONFLICT") { errorData.concurrency = { type: "version_conflict", details: error.details }; } else if (error.errorType === "AUTHENTICATION_FAILED" || error.errorType === "AUTHORIZATION_FAILED") { errorData.security = { type: error.errorType.toLowerCase(), details: error.details }; } else if (error.errorType === "RATE_LIMIT_EXCEEDED") { errorData.rateLimit = { retryAfter: (_a = error.details) == null ? void 0 : _a.retryAfter }; } else if (error.errorType === "NOT_FOUND" || error.errorType.endsWith("_NOT_FOUND")) { errorData.resource = { type: ((_b = error.details) == null ? void 0 : _b.resourceType) || "unknown", id: ((_c = error.details) == null ? void 0 : _c.resourceId) || ((_d = error.details) == null ? void 0 : _d.eventStoreId) || ((_e = error.details) == null ? void 0 : _e.subscriptionId) }; } wideEvent.trackError(errorData); return; } else { wideEvent.trackError({ type: "UnknownError", message: error.message, unexpected: true }); } } function createErrorResponse(error, c) { if (error instanceof import_zod.ZodError) { return c.json( { success: false, error: { issues: error.issues, name: error.name } }, 400 ); } if (error instanceof import_toolkit.ValidationError) { return c.json({ success: false, message: error.message }, 400); } if (error instanceof import_toolkit.IllegalStateError) { const alreadyExists = error.message.includes("already exists"); const status = alreadyExists ? 409 : 500; return c.json({ success: false, message: error.message }, status); } if (error instanceof import_http_exception.HTTPException) { return c.json( { success: false, message: error.message }, error.status ); } if ((0, import_toolkit.isDeltaBaseError)(error)) { const responseBody = { success: false, error: { type: error.errorType, message: