@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,366 @@
1
+ /**
2
+ * Frontend Telemetry Module
3
+ *
4
+ * OpenTelemetry integration for browser/frontend applications.
5
+ * Provides distributed tracing, error tracking, and performance monitoring.
6
+ *
7
+ * @module telemetry/frontend
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { initTelemetry, createSpan } from '@kabran-tecnologia/kabran-config/telemetry/frontend'
12
+ *
13
+ * // Initialize at app startup (main.tsx)
14
+ * initTelemetry({ serviceName: 'my-app' })
15
+ *
16
+ * // Create custom spans
17
+ * const result = createSpan('calculate.total', (span) => {
18
+ * span.setAttribute('item.count', items.length)
19
+ * return calculateTotal(items)
20
+ * })
21
+ * ```
22
+ */
23
+
24
+ import { resolveConfig, detectEnabled } from '../config/index.mjs'
25
+ import {
26
+ getTracesPath,
27
+ getBspConfigFrontend,
28
+ getUserInteractionEvents,
29
+ getCorsUrls,
30
+ DEFAULT_SERVICE_VERSION,
31
+ DEFAULT_TRACER_NAME_FRONTEND,
32
+ } from '../config/defaults.mjs'
33
+ import { recordError, setAttributes, safeWarn, safeLog } from '../shared/helpers.mjs'
34
+
35
+ // State
36
+ let provider = null
37
+ let initialized = false
38
+ let resolvedConfig = null
39
+
40
+ /**
41
+ * Initialize OpenTelemetry for frontend
42
+ *
43
+ * @param {import('../shared/types').TelemetryConfig} [config] - Configuration options
44
+ * @returns {Promise<void>}
45
+ */
46
+ export async function initTelemetry(config = {}) {
47
+ if (initialized) return
48
+
49
+ // Skip in test environment
50
+ if (typeof window === 'undefined') return
51
+ if (import.meta?.env?.MODE === 'test') return
52
+
53
+ // Resolve configuration
54
+ const env = typeof import.meta !== 'undefined' ? import.meta.env || {} : {}
55
+ const mode = env.MODE || 'development'
56
+
57
+ // Require serviceName
58
+ if (!config.serviceName && !env.VITE_SERVICE_NAME) {
59
+ safeWarn('[Telemetry] Skipped: serviceName is required')
60
+ return
61
+ }
62
+
63
+ resolvedConfig = resolveConfig(
64
+ { serviceName: config.serviceName || env.VITE_SERVICE_NAME, ...config },
65
+ env,
66
+ mode
67
+ )
68
+
69
+ // Skip if disabled
70
+ if (!resolvedConfig.enabled) {
71
+ safeWarn('[Telemetry] Skipped: disabled')
72
+ return
73
+ }
74
+
75
+ // Skip if no endpoint
76
+ if (!resolvedConfig.endpoint) {
77
+ safeWarn('[Telemetry] Skipped: no endpoint configured')
78
+ return
79
+ }
80
+
81
+ try {
82
+ // Dynamic imports to avoid bundling when not used
83
+ const [
84
+ { trace, context, SpanStatusCode },
85
+ { OTLPTraceExporter },
86
+ { registerInstrumentations },
87
+ { Resource },
88
+ { BatchSpanProcessor, TraceIdRatioBasedSampler },
89
+ { WebTracerProvider },
90
+ {
91
+ SEMRESATTRS_SERVICE_NAME,
92
+ SEMRESATTRS_SERVICE_VERSION,
93
+ SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
94
+ },
95
+ ] = await Promise.all([
96
+ import('@opentelemetry/api'),
97
+ import('@opentelemetry/exporter-trace-otlp-http'),
98
+ import('@opentelemetry/instrumentation'),
99
+ import('@opentelemetry/resources'),
100
+ import('@opentelemetry/sdk-trace-base'),
101
+ import('@opentelemetry/sdk-trace-web'),
102
+ import('@opentelemetry/semantic-conventions'),
103
+ ])
104
+
105
+ provider = new WebTracerProvider({
106
+ resource: new Resource({
107
+ [SEMRESATTRS_SERVICE_NAME]: resolvedConfig.serviceName,
108
+ [SEMRESATTRS_SERVICE_VERSION]: resolvedConfig.serviceVersion,
109
+ [SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: resolvedConfig.environment,
110
+ 'service.namespace': resolvedConfig.namespace,
111
+ ...resolvedConfig.resourceAttributes,
112
+ }),
113
+ sampler: new TraceIdRatioBasedSampler(resolvedConfig.sampleRate),
114
+ })
115
+
116
+ const exporter = new OTLPTraceExporter({
117
+ url: `${resolvedConfig.endpoint}${getTracesPath()}`,
118
+ })
119
+
120
+ provider.addSpanProcessor(
121
+ new BatchSpanProcessor(exporter, getBspConfigFrontend())
122
+ )
123
+
124
+ provider.register()
125
+
126
+ // Setup auto-instrumentation
127
+ const instrumentations = []
128
+
129
+ if (resolvedConfig.instrumentation.fetch) {
130
+ const { FetchInstrumentation } = await import('@opentelemetry/instrumentation-fetch')
131
+ instrumentations.push(
132
+ new FetchInstrumentation({
133
+ propagateTraceHeaderCorsUrls: resolvedConfig.propagateTraceHeaderCorsUrls || getCorsUrls(),
134
+ clearTimingResources: true,
135
+ })
136
+ )
137
+ }
138
+
139
+ if (resolvedConfig.instrumentation.documentLoad) {
140
+ const { DocumentLoadInstrumentation } = await import('@opentelemetry/instrumentation-document-load')
141
+ instrumentations.push(new DocumentLoadInstrumentation())
142
+ }
143
+
144
+ if (resolvedConfig.instrumentation.userInteraction) {
145
+ const { UserInteractionInstrumentation } = await import('@opentelemetry/instrumentation-user-interaction')
146
+ instrumentations.push(
147
+ new UserInteractionInstrumentation({
148
+ eventNames: getUserInteractionEvents(),
149
+ })
150
+ )
151
+ }
152
+
153
+ if (instrumentations.length > 0) {
154
+ registerInstrumentations({ instrumentations })
155
+ }
156
+
157
+ // Setup error handlers
158
+ setupErrorHandlers()
159
+
160
+ initialized = true
161
+ safeLog(
162
+ `[Telemetry] Initialized: ${resolvedConfig.serviceName}@${resolvedConfig.serviceVersion} (${resolvedConfig.environment})`
163
+ )
164
+ } catch (error) {
165
+ safeWarn('[Telemetry] Failed to initialize:', error)
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Setup global error handlers
171
+ */
172
+ function setupErrorHandlers() {
173
+ if (typeof window === 'undefined') return
174
+
175
+ window.addEventListener('error', (event) => {
176
+ const tracer = getTracer()
177
+ const span = tracer.startSpan('error.uncaught')
178
+ span.setAttribute('error.type', 'uncaught')
179
+ span.setAttribute('error.message', event.message)
180
+ span.setAttribute('error.filename', event.filename || 'unknown')
181
+ span.setAttribute('error.lineno', event.lineno || 0)
182
+ import('@opentelemetry/api').then(({ SpanStatusCode }) => {
183
+ span.setStatus({ code: SpanStatusCode.ERROR, message: event.message })
184
+ span.end()
185
+ })
186
+ })
187
+
188
+ window.addEventListener('unhandledrejection', (event) => {
189
+ const tracer = getTracer()
190
+ const span = tracer.startSpan('error.unhandled_rejection')
191
+ span.setAttribute('error.type', 'unhandled_rejection')
192
+ const reason = event.reason instanceof Error ? event.reason.message : String(event.reason)
193
+ span.setAttribute('error.reason', reason)
194
+ import('@opentelemetry/api').then(({ SpanStatusCode }) => {
195
+ span.setStatus({ code: SpanStatusCode.ERROR, message: reason })
196
+ span.end()
197
+ })
198
+ })
199
+ }
200
+
201
+ /**
202
+ * Get a tracer instance
203
+ *
204
+ * @param {string} [name] - Tracer name (default: service name)
205
+ * @returns {import('@opentelemetry/api').Tracer}
206
+ */
207
+ export function getTracer(name) {
208
+ const { trace } = require('@opentelemetry/api')
209
+ const tracerName = name || resolvedConfig?.serviceName || DEFAULT_TRACER_NAME_FRONTEND
210
+ const version = resolvedConfig?.serviceVersion || DEFAULT_SERVICE_VERSION
211
+ return trace.getTracer(tracerName, version)
212
+ }
213
+
214
+ /**
215
+ * Get the currently active span
216
+ *
217
+ * @returns {import('@opentelemetry/api').Span|undefined}
218
+ */
219
+ export function getCurrentSpan() {
220
+ const { trace } = require('@opentelemetry/api')
221
+ return trace.getActiveSpan()
222
+ }
223
+
224
+ /**
225
+ * Get trace ID from current span
226
+ *
227
+ * @returns {string|undefined}
228
+ */
229
+ export function getTraceId() {
230
+ const span = getCurrentSpan()
231
+ return span?.spanContext().traceId
232
+ }
233
+
234
+ /**
235
+ * Create a span for tracking an operation
236
+ *
237
+ * @template T
238
+ * @param {string} name - Span name
239
+ * @param {(span: import('@opentelemetry/api').Span) => T} fn - Function to execute within span
240
+ * @param {Record<string, string|number|boolean>} [attributes] - Initial attributes
241
+ * @returns {T}
242
+ */
243
+ export function createSpan(name, fn, attributes) {
244
+ const tracer = getTracer()
245
+ const { SpanStatusCode } = require('@opentelemetry/api')
246
+
247
+ return tracer.startActiveSpan(name, (span) => {
248
+ if (attributes) {
249
+ setAttributes(span, attributes)
250
+ }
251
+
252
+ try {
253
+ const result = fn(span)
254
+
255
+ if (result instanceof Promise) {
256
+ return result
257
+ .then((value) => {
258
+ span.setStatus({ code: SpanStatusCode.OK })
259
+ span.end()
260
+ return value
261
+ })
262
+ .catch((error) => {
263
+ recordError(span, error, SpanStatusCode)
264
+ span.end()
265
+ throw error
266
+ })
267
+ }
268
+
269
+ span.setStatus({ code: SpanStatusCode.OK })
270
+ span.end()
271
+ return result
272
+ } catch (error) {
273
+ recordError(span, error, SpanStatusCode)
274
+ span.end()
275
+ throw error
276
+ }
277
+ })
278
+ }
279
+
280
+ /**
281
+ * Create an async span for tracking asynchronous operations
282
+ *
283
+ * @template T
284
+ * @param {string} name - Span name
285
+ * @param {(span: import('@opentelemetry/api').Span) => Promise<T>} fn - Async function
286
+ * @param {Record<string, string|number|boolean>} [attributes] - Initial attributes
287
+ * @returns {Promise<T>}
288
+ */
289
+ export async function createAsyncSpan(name, fn, attributes) {
290
+ const tracer = getTracer()
291
+ const { trace, context, SpanStatusCode } = require('@opentelemetry/api')
292
+
293
+ const span = tracer.startSpan(name)
294
+
295
+ if (attributes) {
296
+ setAttributes(span, attributes)
297
+ }
298
+
299
+ try {
300
+ const result = await context.with(trace.setSpan(context.active(), span), () => fn(span))
301
+ span.setStatus({ code: SpanStatusCode.OK })
302
+ return result
303
+ } catch (error) {
304
+ recordError(span, error, SpanStatusCode)
305
+ throw error
306
+ } finally {
307
+ span.end()
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Add an event to the current span
313
+ *
314
+ * @param {string} name - Event name
315
+ * @param {Record<string, string|number|boolean>} [attributes] - Event attributes
316
+ */
317
+ export function addSpanEvent(name, attributes) {
318
+ const span = getCurrentSpan()
319
+ span?.addEvent(name, attributes)
320
+ }
321
+
322
+ /**
323
+ * Set attributes on the current span
324
+ *
325
+ * @param {Record<string, string|number|boolean>} attributes - Attributes to set
326
+ */
327
+ export function setSpanAttributes(attributes) {
328
+ const span = getCurrentSpan()
329
+ if (span) {
330
+ setAttributes(span, attributes)
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Shutdown telemetry provider gracefully
336
+ *
337
+ * @returns {Promise<void>}
338
+ */
339
+ export async function shutdownTelemetry() {
340
+ if (provider) {
341
+ await provider.shutdown()
342
+ initialized = false
343
+ provider = null
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Check if telemetry is initialized
349
+ *
350
+ * @returns {boolean}
351
+ */
352
+ export function isInitialized() {
353
+ return initialized
354
+ }
355
+
356
+ /**
357
+ * Get current configuration
358
+ *
359
+ * @returns {import('../shared/types').ResolvedTelemetryConfig|null}
360
+ */
361
+ export function getConfig() {
362
+ return resolvedConfig
363
+ }
364
+
365
+ // Re-export useful types and utilities
366
+ export { resolveConfig } from '../config/index.mjs'
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Telemetry Logger Module
3
+ *
4
+ * Structured logger with trace correlation.
5
+ * Automatically includes trace_id and span_id in log output.
6
+ *
7
+ * @module telemetry/logger
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { createLogger } from '@kabran-tecnologia/kabran-config/telemetry/logger'
12
+ *
13
+ * const log = createLogger()
14
+ * log.info('User logged in', { userId: '123' })
15
+ * // Output: {"level":"info","message":"User logged in","userId":"123","trace_id":"abc...","span_id":"def...","timestamp":"..."}
16
+ * ```
17
+ */
18
+
19
+ import { shouldDisableColors, getLogTraceIdLength } from '../config/defaults.mjs'
20
+
21
+ /**
22
+ * Log levels
23
+ */
24
+ const LOG_LEVELS = {
25
+ debug: 0,
26
+ info: 1,
27
+ warn: 2,
28
+ error: 3,
29
+ }
30
+
31
+ /**
32
+ * Get trace context from current span
33
+ *
34
+ * @returns {{ trace_id?: string, span_id?: string }}
35
+ */
36
+ function getTraceContext() {
37
+ try {
38
+ const { trace } = require('@opentelemetry/api')
39
+ const span = trace.getActiveSpan()
40
+ if (span) {
41
+ const ctx = span.spanContext()
42
+ return {
43
+ trace_id: ctx.traceId,
44
+ span_id: ctx.spanId,
45
+ }
46
+ }
47
+ } catch {
48
+ // OTel not available
49
+ }
50
+ return {}
51
+ }
52
+
53
+ /**
54
+ * Format log entry for JSON output
55
+ *
56
+ * @param {string} level - Log level
57
+ * @param {string} message - Log message
58
+ * @param {Record<string, unknown>} [data] - Additional data
59
+ * @param {boolean} includeTrace - Whether to include trace context
60
+ * @returns {string} JSON formatted log entry
61
+ */
62
+ function formatJson(level, message, data = {}, includeTrace = true) {
63
+ const entry = {
64
+ level,
65
+ message,
66
+ timestamp: new Date().toISOString(),
67
+ ...data,
68
+ }
69
+
70
+ if (includeTrace) {
71
+ const traceContext = getTraceContext()
72
+ if (traceContext.trace_id) {
73
+ entry.trace_id = traceContext.trace_id
74
+ entry.span_id = traceContext.span_id
75
+ }
76
+ }
77
+
78
+ return JSON.stringify(entry)
79
+ }
80
+
81
+ /**
82
+ * ANSI color codes
83
+ */
84
+ const COLORS = {
85
+ gray: '\x1b[90m',
86
+ cyan: '\x1b[36m',
87
+ yellow: '\x1b[33m',
88
+ red: '\x1b[31m',
89
+ reset: '\x1b[0m',
90
+ }
91
+
92
+ /**
93
+ * Get color code (returns empty string if colors are disabled)
94
+ *
95
+ * @param {string} colorName - Color name
96
+ * @returns {string} ANSI code or empty string
97
+ */
98
+ function getColor(colorName) {
99
+ if (shouldDisableColors()) return ''
100
+ return COLORS[colorName] || ''
101
+ }
102
+
103
+ /**
104
+ * Format log entry for pretty output
105
+ *
106
+ * @param {string} level - Log level
107
+ * @param {string} message - Log message
108
+ * @param {Record<string, unknown>} [data] - Additional data
109
+ * @param {boolean} includeTrace - Whether to include trace context
110
+ * @returns {string} Pretty formatted log entry
111
+ */
112
+ function formatPretty(level, message, data = {}, includeTrace = true) {
113
+ const timestamp = new Date().toISOString()
114
+ const levelColors = {
115
+ debug: getColor('gray'),
116
+ info: getColor('cyan'),
117
+ warn: getColor('yellow'),
118
+ error: getColor('red'),
119
+ }
120
+ const reset = getColor('reset')
121
+ const color = levelColors[level] || ''
122
+
123
+ let output = `${timestamp} ${color}[${level.toUpperCase()}]${reset} ${message}`
124
+
125
+ if (includeTrace) {
126
+ const traceContext = getTraceContext()
127
+ if (traceContext.trace_id) {
128
+ const traceIdLength = getLogTraceIdLength()
129
+ output += ` ${getColor('gray')}[trace:${traceContext.trace_id.substring(0, traceIdLength)}]${reset}`
130
+ }
131
+ }
132
+
133
+ if (Object.keys(data).length > 0) {
134
+ output += ` ${JSON.stringify(data)}`
135
+ }
136
+
137
+ return output
138
+ }
139
+
140
+ /**
141
+ * Create a logger instance
142
+ *
143
+ * @param {import('../shared/types').LoggerOptions} [options] - Logger options
144
+ * @returns {import('../shared/types').TelemetryLogger}
145
+ */
146
+ export function createLogger(options = {}) {
147
+ const {
148
+ level = 'info',
149
+ format = process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
150
+ includeTrace = true,
151
+ } = options
152
+
153
+ const minLevel = LOG_LEVELS[level] ?? LOG_LEVELS.info
154
+ const formatter = format === 'json' ? formatJson : formatPretty
155
+
156
+ const shouldLog = (logLevel) => LOG_LEVELS[logLevel] >= minLevel
157
+
158
+ return {
159
+ debug(message, data) {
160
+ if (shouldLog('debug')) {
161
+ console.debug(formatter('debug', message, data, includeTrace))
162
+ }
163
+ },
164
+
165
+ info(message, data) {
166
+ if (shouldLog('info')) {
167
+ console.info(formatter('info', message, data, includeTrace))
168
+ }
169
+ },
170
+
171
+ warn(message, data) {
172
+ if (shouldLog('warn')) {
173
+ console.warn(formatter('warn', message, data, includeTrace))
174
+ }
175
+ },
176
+
177
+ error(message, data) {
178
+ if (shouldLog('error')) {
179
+ console.error(formatter('error', message, data, includeTrace))
180
+ }
181
+ },
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Create a logger bound to a specific span
187
+ *
188
+ * @param {import('@opentelemetry/api').Span} [span] - Span to bind to
189
+ * @param {import('../shared/types').LoggerOptions} [options] - Logger options
190
+ * @returns {import('../shared/types').TelemetryLogger}
191
+ */
192
+ export function createSpanLogger(span, options = {}) {
193
+ const logger = createLogger(options)
194
+
195
+ const withSpanContext = (data = {}) => {
196
+ if (span) {
197
+ const ctx = span.spanContext()
198
+ return {
199
+ ...data,
200
+ trace_id: ctx.traceId,
201
+ span_id: ctx.spanId,
202
+ }
203
+ }
204
+ return data
205
+ }
206
+
207
+ return {
208
+ debug(message, data) {
209
+ logger.debug(message, withSpanContext(data))
210
+ span?.addEvent(`log.debug: ${message}`, data)
211
+ },
212
+
213
+ info(message, data) {
214
+ logger.info(message, withSpanContext(data))
215
+ span?.addEvent(`log.info: ${message}`, data)
216
+ },
217
+
218
+ warn(message, data) {
219
+ logger.warn(message, withSpanContext(data))
220
+ span?.addEvent(`log.warn: ${message}`, data)
221
+ },
222
+
223
+ error(message, data) {
224
+ logger.error(message, withSpanContext(data))
225
+ span?.addEvent(`log.error: ${message}`, data)
226
+ },
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Default logger instance
232
+ */
233
+ export const log = createLogger()
234
+
235
+ // Re-export types
236
+ export { getTraceContext }