@kabran-tecnologia/kabran-config 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Node.js Telemetry Module
3
+ *
4
+ * OpenTelemetry integration for Node.js backend applications.
5
+ * Uses BatchSpanProcessor for efficient span export.
6
+ *
7
+ * @module telemetry/node
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import express from 'express'
12
+ * import { initTelemetry, telemetryMiddleware } from '@kabran-tecnologia/kabran-config/telemetry/node'
13
+ *
14
+ * initTelemetry({ serviceName: 'api-server' })
15
+ *
16
+ * const app = express()
17
+ * app.use(telemetryMiddleware())
18
+ * ```
19
+ */
20
+
21
+ import { resolveConfig } from '../config/index.mjs'
22
+ import {
23
+ getTracesPath,
24
+ getExportTimeoutNode,
25
+ getBspConfigNode,
26
+ getIgnorePaths,
27
+ DEFAULT_SERVICE_VERSION,
28
+ DEFAULT_TRACER_NAME_NODE,
29
+ } from '../config/defaults.mjs'
30
+ import { recordError, setAttributes, generateInvocationId, safeWarn, safeLog } from '../shared/helpers.mjs'
31
+
32
+ // State
33
+ let provider = null
34
+ let initialized = false
35
+ let resolvedConfig = null
36
+
37
+ /**
38
+ * Initialize OpenTelemetry for Node.js
39
+ *
40
+ * @param {import('../shared/types').TelemetryConfig} config - Configuration
41
+ * @returns {Promise<void>}
42
+ */
43
+ export async function initTelemetry(config = {}) {
44
+ if (initialized) return
45
+
46
+ if (!config.serviceName && !process.env.SERVICE_NAME) {
47
+ safeWarn('[Telemetry] Skipped: serviceName is required')
48
+ return
49
+ }
50
+
51
+ resolvedConfig = resolveConfig(
52
+ { serviceName: config.serviceName || process.env.SERVICE_NAME, ...config },
53
+ process.env,
54
+ process.env.NODE_ENV || 'development'
55
+ )
56
+
57
+ if (!resolvedConfig.enabled) {
58
+ safeWarn('[Telemetry] Skipped: disabled')
59
+ return
60
+ }
61
+
62
+ if (!resolvedConfig.endpoint) {
63
+ safeWarn('[Telemetry] Skipped: no endpoint configured')
64
+ return
65
+ }
66
+
67
+ try {
68
+ const [
69
+ { trace, context, propagation, SpanStatusCode },
70
+ { NodeTracerProvider },
71
+ { BatchSpanProcessor, TraceIdRatioBasedSampler },
72
+ { OTLPTraceExporter },
73
+ { Resource },
74
+ {
75
+ SEMRESATTRS_SERVICE_NAME,
76
+ SEMRESATTRS_SERVICE_VERSION,
77
+ SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
78
+ },
79
+ { W3CTraceContextPropagator },
80
+ ] = await Promise.all([
81
+ import('@opentelemetry/api'),
82
+ import('@opentelemetry/sdk-trace-node'),
83
+ import('@opentelemetry/sdk-trace-base'),
84
+ import('@opentelemetry/exporter-trace-otlp-http'),
85
+ import('@opentelemetry/resources'),
86
+ import('@opentelemetry/semantic-conventions'),
87
+ import('@opentelemetry/core'),
88
+ ])
89
+
90
+ provider = new NodeTracerProvider({
91
+ resource: new Resource({
92
+ [SEMRESATTRS_SERVICE_NAME]: resolvedConfig.serviceName,
93
+ [SEMRESATTRS_SERVICE_VERSION]: resolvedConfig.serviceVersion,
94
+ [SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: resolvedConfig.environment,
95
+ 'service.namespace': resolvedConfig.namespace,
96
+ ...resolvedConfig.resourceAttributes,
97
+ }),
98
+ sampler: new TraceIdRatioBasedSampler(resolvedConfig.sampleRate),
99
+ })
100
+
101
+ const exporter = new OTLPTraceExporter({
102
+ url: `${resolvedConfig.endpoint}${getTracesPath()}`,
103
+ timeoutMillis: getExportTimeoutNode(),
104
+ })
105
+
106
+ // Use BatchSpanProcessor for efficiency in long-running processes
107
+ provider.addSpanProcessor(
108
+ new BatchSpanProcessor(exporter, getBspConfigNode())
109
+ )
110
+
111
+ // Configure W3C Trace Context propagation
112
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator())
113
+
114
+ provider.register()
115
+ initialized = true
116
+
117
+ // Graceful shutdown on process exit
118
+ process.on('SIGTERM', async () => {
119
+ await shutdownTelemetry()
120
+ })
121
+
122
+ process.on('SIGINT', async () => {
123
+ await shutdownTelemetry()
124
+ })
125
+
126
+ safeLog(
127
+ `[Telemetry] Initialized: ${resolvedConfig.serviceName}@${resolvedConfig.serviceVersion} (${resolvedConfig.environment})`
128
+ )
129
+ } catch (error) {
130
+ safeWarn('[Telemetry] Failed to initialize:', error)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get a tracer instance
136
+ *
137
+ * @param {string} [name] - Tracer name
138
+ * @returns {import('@opentelemetry/api').Tracer}
139
+ */
140
+ export function getTracer(name) {
141
+ const { trace } = require('@opentelemetry/api')
142
+ return trace.getTracer(
143
+ name || resolvedConfig?.serviceName || DEFAULT_TRACER_NAME_NODE,
144
+ resolvedConfig?.serviceVersion || DEFAULT_SERVICE_VERSION
145
+ )
146
+ }
147
+
148
+ /**
149
+ * Get the currently active span
150
+ *
151
+ * @returns {import('@opentelemetry/api').Span|undefined}
152
+ */
153
+ export function getCurrentSpan() {
154
+ const { trace } = require('@opentelemetry/api')
155
+ return trace.getActiveSpan()
156
+ }
157
+
158
+ /**
159
+ * Get trace ID from current span
160
+ *
161
+ * @returns {string|undefined}
162
+ */
163
+ export function getTraceId() {
164
+ return getCurrentSpan()?.spanContext().traceId
165
+ }
166
+
167
+ /**
168
+ * Create a span for tracking an operation
169
+ *
170
+ * @template T
171
+ * @param {string} name - Span name
172
+ * @param {(span: import('@opentelemetry/api').Span) => T} fn - Function to execute
173
+ * @param {Record<string, string|number|boolean>} [attributes] - Initial attributes
174
+ * @returns {T}
175
+ */
176
+ export function createSpan(name, fn, attributes) {
177
+ const tracer = getTracer()
178
+ const { SpanStatusCode } = require('@opentelemetry/api')
179
+
180
+ return tracer.startActiveSpan(name, (span) => {
181
+ if (attributes) {
182
+ setAttributes(span, attributes)
183
+ }
184
+
185
+ try {
186
+ const result = fn(span)
187
+
188
+ if (result instanceof Promise) {
189
+ return result
190
+ .then((value) => {
191
+ span.setStatus({ code: SpanStatusCode.OK })
192
+ span.end()
193
+ return value
194
+ })
195
+ .catch((error) => {
196
+ recordError(span, error, SpanStatusCode)
197
+ span.end()
198
+ throw error
199
+ })
200
+ }
201
+
202
+ span.setStatus({ code: SpanStatusCode.OK })
203
+ span.end()
204
+ return result
205
+ } catch (error) {
206
+ recordError(span, error, SpanStatusCode)
207
+ span.end()
208
+ throw error
209
+ }
210
+ })
211
+ }
212
+
213
+ /**
214
+ * Create an async span
215
+ *
216
+ * @template T
217
+ * @param {string} name - Span name
218
+ * @param {(span: import('@opentelemetry/api').Span) => Promise<T>} fn - Async function
219
+ * @param {Record<string, string|number|boolean>} [attributes] - Initial attributes
220
+ * @returns {Promise<T>}
221
+ */
222
+ export async function createAsyncSpan(name, fn, attributes) {
223
+ const tracer = getTracer()
224
+ const { trace, context, SpanStatusCode } = require('@opentelemetry/api')
225
+
226
+ const span = tracer.startSpan(name)
227
+
228
+ if (attributes) {
229
+ setAttributes(span, attributes)
230
+ }
231
+
232
+ try {
233
+ const result = await context.with(trace.setSpan(context.active(), span), () => fn(span))
234
+ span.setStatus({ code: SpanStatusCode.OK })
235
+ return result
236
+ } catch (error) {
237
+ recordError(span, error, SpanStatusCode)
238
+ throw error
239
+ } finally {
240
+ span.end()
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Express/Fastify compatible middleware for automatic request tracing
246
+ *
247
+ * @param {Object} [options] - Middleware options
248
+ * @param {string[]} [options.ignorePaths] - Paths to ignore (e.g., ['/health', '/ready'])
249
+ * @returns {Function} Middleware function
250
+ */
251
+ export function telemetryMiddleware(options = {}) {
252
+ const { ignorePaths = getIgnorePaths() } = options
253
+
254
+ return async (req, res, next) => {
255
+ // Skip ignored paths
256
+ const path = req.path || req.url
257
+ if (ignorePaths.some((p) => path.startsWith(p))) {
258
+ return next()
259
+ }
260
+
261
+ // Skip if not initialized
262
+ if (!initialized || !resolvedConfig?.enabled) {
263
+ return next()
264
+ }
265
+
266
+ const { trace, context, propagation, SpanStatusCode } = await import('@opentelemetry/api')
267
+ const {
268
+ SEMATTRS_HTTP_METHOD,
269
+ SEMATTRS_HTTP_URL,
270
+ SEMATTRS_HTTP_STATUS_CODE,
271
+ SEMATTRS_HTTP_ROUTE,
272
+ SEMATTRS_HTTP_USER_AGENT,
273
+ } = await import('@opentelemetry/semantic-conventions')
274
+
275
+ // Extract parent context from headers
276
+ const carrier = {}
277
+ Object.keys(req.headers).forEach((key) => {
278
+ carrier[key.toLowerCase()] = req.headers[key]
279
+ })
280
+ const parentContext = propagation.extract(context.active(), carrier)
281
+
282
+ const tracer = getTracer()
283
+ const span = tracer.startSpan(
284
+ `${req.method} ${path}`,
285
+ {
286
+ attributes: {
287
+ [SEMATTRS_HTTP_METHOD]: req.method,
288
+ [SEMATTRS_HTTP_URL]: req.originalUrl || req.url,
289
+ [SEMATTRS_HTTP_ROUTE]: req.route?.path || path,
290
+ [SEMATTRS_HTTP_USER_AGENT]: req.headers['user-agent'] || 'unknown',
291
+ 'http.request_id': generateInvocationId(),
292
+ },
293
+ },
294
+ parentContext
295
+ )
296
+
297
+ const startTime = Date.now()
298
+
299
+ // Inject trace context for downstream services
300
+ propagation.inject(trace.setSpan(context.active(), span), req.headers)
301
+
302
+ // Store span on request for access in handlers
303
+ req.span = span
304
+ req.traceId = span.spanContext().traceId
305
+
306
+ // Hook into response finish
307
+ const originalEnd = res.end
308
+ res.end = function (...args) {
309
+ span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, res.statusCode)
310
+ span.setAttribute('http.response_time_ms', Date.now() - startTime)
311
+
312
+ if (res.statusCode >= 400) {
313
+ span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${res.statusCode}` })
314
+ } else {
315
+ span.setStatus({ code: SpanStatusCode.OK })
316
+ }
317
+
318
+ span.end()
319
+ return originalEnd.apply(this, args)
320
+ }
321
+
322
+ // Execute within span context
323
+ context.with(trace.setSpan(parentContext, span), () => {
324
+ next()
325
+ })
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Add an event to the current span
331
+ *
332
+ * @param {string} name - Event name
333
+ * @param {Record<string, string|number|boolean>} [attributes] - Event attributes
334
+ */
335
+ export function addSpanEvent(name, attributes) {
336
+ const span = getCurrentSpan()
337
+ span?.addEvent(name, attributes)
338
+ }
339
+
340
+ /**
341
+ * Set attributes on the current span
342
+ *
343
+ * @param {Record<string, string|number|boolean>} attributes - Attributes to set
344
+ */
345
+ export function setSpanAttributes(attributes) {
346
+ const span = getCurrentSpan()
347
+ if (span) {
348
+ setAttributes(span, attributes)
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Shutdown telemetry provider gracefully
354
+ *
355
+ * @returns {Promise<void>}
356
+ */
357
+ export async function shutdownTelemetry() {
358
+ if (provider) {
359
+ safeLog('[Telemetry] Shutting down...')
360
+ await provider.shutdown()
361
+ initialized = false
362
+ provider = null
363
+ safeLog('[Telemetry] Shutdown complete')
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Check if telemetry is initialized
369
+ *
370
+ * @returns {boolean}
371
+ */
372
+ export function isInitialized() {
373
+ return initialized
374
+ }
375
+
376
+ /**
377
+ * Get current configuration
378
+ *
379
+ * @returns {import('../shared/types').ResolvedTelemetryConfig|null}
380
+ */
381
+ export function getConfig() {
382
+ return resolvedConfig
383
+ }
384
+
385
+ // Re-exports
386
+ export { resolveConfig } from '../config/index.mjs'
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Shared Telemetry Helpers
3
+ *
4
+ * Common utilities used across all telemetry modules.
5
+ *
6
+ * @module telemetry/shared/helpers
7
+ */
8
+
9
+ /**
10
+ * Record an error on a span with full details
11
+ *
12
+ * @param {import('@opentelemetry/api').Span} span - Span to record error on
13
+ * @param {Error|unknown} error - Error to record
14
+ * @param {import('@opentelemetry/api').SpanStatusCode} SpanStatusCode - Status code enum
15
+ */
16
+ export function recordError(span, error, SpanStatusCode) {
17
+ if (error instanceof Error) {
18
+ span.recordException(error)
19
+ span.setStatus({
20
+ code: SpanStatusCode.ERROR,
21
+ message: error.message,
22
+ })
23
+ span.setAttribute('error.type', error.name)
24
+ span.setAttribute('error.message', error.message)
25
+ if (error.stack) {
26
+ span.setAttribute('error.stack', error.stack)
27
+ }
28
+ } else {
29
+ const message = String(error)
30
+ span.setStatus({
31
+ code: SpanStatusCode.ERROR,
32
+ message,
33
+ })
34
+ span.setAttribute('error.message', message)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Set multiple attributes on a span
40
+ *
41
+ * @param {import('@opentelemetry/api').Span} span - Span to set attributes on
42
+ * @param {Record<string, string|number|boolean>} attributes - Attributes to set
43
+ */
44
+ export function setAttributes(span, attributes) {
45
+ if (span && attributes) {
46
+ Object.entries(attributes).forEach(([key, value]) => {
47
+ span.setAttribute(key, value)
48
+ })
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Create resource attributes from config
54
+ *
55
+ * @param {import('../shared/types').ResolvedTelemetryConfig} config - Configuration
56
+ * @param {Object} SEMRESATTRS - Semantic resource attributes constants
57
+ * @returns {Record<string, string>} Resource attributes
58
+ */
59
+ export function createResourceAttributes(config, SEMRESATTRS) {
60
+ return {
61
+ [SEMRESATTRS.SEMRESATTRS_SERVICE_NAME]: config.serviceName,
62
+ [SEMRESATTRS.SEMRESATTRS_SERVICE_VERSION]: config.serviceVersion,
63
+ [SEMRESATTRS.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: config.environment,
64
+ 'service.namespace': config.namespace,
65
+ ...config.resourceAttributes,
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Format duration for logging
71
+ *
72
+ * @param {number} ms - Duration in milliseconds
73
+ * @returns {string} Formatted duration
74
+ */
75
+ export function formatDuration(ms) {
76
+ if (ms < 1000) {
77
+ return `${ms}ms`
78
+ }
79
+ return `${(ms / 1000).toFixed(2)}s`
80
+ }
81
+
82
+ /**
83
+ * Generate a unique invocation ID
84
+ *
85
+ * @returns {string} UUID v4
86
+ */
87
+ export function generateInvocationId() {
88
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
89
+ return crypto.randomUUID()
90
+ }
91
+ // Fallback for older environments
92
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
93
+ const r = (Math.random() * 16) | 0
94
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
95
+ return v.toString(16)
96
+ })
97
+ }
98
+
99
+ /**
100
+ * Safe console warn that doesn't throw
101
+ *
102
+ * @param {string} message - Message to log
103
+ * @param {unknown} [data] - Additional data
104
+ */
105
+ export function safeWarn(message, data) {
106
+ try {
107
+ if (data !== undefined) {
108
+ console.warn(message, data)
109
+ } else {
110
+ console.warn(message)
111
+ }
112
+ } catch {
113
+ // Ignore console errors
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Safe console log that doesn't throw
119
+ *
120
+ * @param {string} message - Message to log
121
+ * @param {unknown} [data] - Additional data
122
+ */
123
+ export function safeLog(message, data) {
124
+ try {
125
+ if (data !== undefined) {
126
+ console.log(message, data)
127
+ } else {
128
+ console.log(message)
129
+ }
130
+ } catch {
131
+ // Ignore console errors
132
+ }
133
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared Telemetry Utilities
3
+ *
4
+ * @module telemetry/shared
5
+ */
6
+
7
+ export {
8
+ recordError,
9
+ setAttributes,
10
+ createResourceAttributes,
11
+ formatDuration,
12
+ generateInvocationId,
13
+ safeWarn,
14
+ safeLog,
15
+ } from './helpers.mjs'
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Telemetry Type Definitions
3
+ *
4
+ * @module telemetry/shared/types
5
+ */
6
+
7
+ import type { Span, Context, Tracer } from '@opentelemetry/api'
8
+
9
+ export type { Span, Context, Tracer }
10
+
11
+ /**
12
+ * Telemetry configuration options
13
+ */
14
+ export interface TelemetryConfig {
15
+ /** Service name (required) */
16
+ serviceName: string
17
+
18
+ /** Service version (default: from package.json or '1.0.0') */
19
+ serviceVersion?: string
20
+
21
+ /** Deployment environment (default: from NODE_ENV or 'development') */
22
+ environment?: string
23
+
24
+ /** OTLP endpoint URL (default: 'https://otel.kabran.com.br') */
25
+ endpoint?: string
26
+
27
+ /** Sampling rate 0.0-1.0 (default: 0.1) */
28
+ sampleRate?: number
29
+
30
+ /** Enable/disable telemetry (default: auto-detect based on environment) */
31
+ enabled?: boolean
32
+
33
+ /** Service namespace for grouping services */
34
+ namespace?: string
35
+
36
+ /** Additional resource attributes */
37
+ resourceAttributes?: Record<string, string | number | boolean>
38
+
39
+ /** Frontend-specific: CORS URLs for trace header propagation */
40
+ propagateTraceHeaderCorsUrls?: (string | RegExp)[]
41
+
42
+ /** Instrumentation options */
43
+ instrumentation?: InstrumentationOptions
44
+ }
45
+
46
+ /**
47
+ * Instrumentation feature flags
48
+ */
49
+ export interface InstrumentationOptions {
50
+ /** Auto-instrument fetch requests (default: true) */
51
+ fetch?: boolean
52
+
53
+ /** Auto-instrument document load (default: true, frontend only) */
54
+ documentLoad?: boolean
55
+
56
+ /** Auto-instrument user interactions (default: true, frontend only) */
57
+ userInteraction?: boolean
58
+
59
+ /** Auto-instrument database queries (default: true, edge/node only) */
60
+ database?: boolean
61
+ }
62
+
63
+ /**
64
+ * Resolved configuration with all defaults applied
65
+ */
66
+ export interface ResolvedTelemetryConfig {
67
+ serviceName: string
68
+ serviceVersion: string
69
+ environment: string
70
+ endpoint: string
71
+ sampleRate: number
72
+ enabled: boolean
73
+ namespace: string
74
+ resourceAttributes: Record<string, string | number | boolean>
75
+ propagateTraceHeaderCorsUrls: (string | RegExp)[]
76
+ instrumentation: Required<InstrumentationOptions>
77
+ }
78
+
79
+ /**
80
+ * Span attributes type
81
+ */
82
+ export type SpanAttributes = Record<string, string | number | boolean>
83
+
84
+ /**
85
+ * Edge function handler type
86
+ */
87
+ export type EdgeHandler = (
88
+ req: Request,
89
+ span: Span
90
+ ) => Promise<Response> | Response
91
+
92
+ /**
93
+ * Express-style middleware request handler
94
+ */
95
+ export type MiddlewareHandler = (
96
+ req: unknown,
97
+ res: unknown,
98
+ next: () => void
99
+ ) => void
100
+
101
+ /**
102
+ * Logger interface
103
+ */
104
+ export interface TelemetryLogger {
105
+ debug(message: string, data?: Record<string, unknown>): void
106
+ info(message: string, data?: Record<string, unknown>): void
107
+ warn(message: string, data?: Record<string, unknown>): void
108
+ error(message: string, data?: Record<string, unknown>): void
109
+ }
110
+
111
+ /**
112
+ * Logger options
113
+ */
114
+ export interface LoggerOptions {
115
+ /** Minimum log level (default: 'info') */
116
+ level?: 'debug' | 'info' | 'warn' | 'error'
117
+
118
+ /** Output format (default: 'json' in production, 'pretty' in development) */
119
+ format?: 'json' | 'pretty'
120
+
121
+ /** Include trace context in logs (default: true) */
122
+ includeTrace?: boolean
123
+ }