@kabran-tecnologia/kabran-config 1.5.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.
- package/README.md +283 -0
- package/package.json +67 -9
- package/src/schemas/ci-result.v2.schema.json +125 -0
- package/src/scripts/ci/ci-core.sh +61 -0
- package/src/scripts/ci/ci-runner.sh +89 -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 +223 -0
- package/src/scripts/generate-ci-result.mjs +79 -11
- package/src/scripts/pr-quality-comment.mjs +326 -0
- package/src/scripts/setup.mjs +91 -4
- 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/.github/workflows/ci-quality.yml +111 -0
- package/templates/telemetry/.env.telemetry.example +118 -0
|
@@ -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,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
|
+
}
|