@kabran-tecnologia/kabran-config 1.6.0 → 1.8.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.
- package/README.md +283 -0
- package/package.json +63 -8
- package/src/schemas/ci-result.v2.schema.json +125 -0
- package/src/scripts/ci/ci-core.sh +131 -1
- package/src/scripts/ci/ci-runner.sh +88 -0
- package/src/scripts/ci-result-history.mjs +245 -0
- package/src/scripts/ci-result-trends.mjs +296 -0
- package/src/scripts/ci-result-utils.mjs +104 -0
- package/src/scripts/generate-ci-result.mjs +92 -11
- package/src/scripts/pr-quality-comment.mjs +36 -0
- package/src/scripts/setup.mjs +91 -4
- package/src/telemetry/README.md +407 -0
- package/src/telemetry/config/defaults.mjs +421 -0
- package/src/telemetry/config/index.mjs +132 -0
- package/src/telemetry/edge/index.mjs +446 -0
- package/src/telemetry/frontend/index.mjs +366 -0
- package/src/telemetry/logger/index.mjs +236 -0
- package/src/telemetry/node/index.mjs +386 -0
- package/src/telemetry/shared/helpers.mjs +133 -0
- package/src/telemetry/shared/index.mjs +15 -0
- package/src/telemetry/shared/types.d.ts +123 -0
- package/templates/telemetry/.env.telemetry.example +118 -0
|
@@ -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 }
|