@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,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge Functions Telemetry Module
|
|
3
|
+
*
|
|
4
|
+
* OpenTelemetry integration for serverless/edge functions (Supabase, Deno, Cloudflare).
|
|
5
|
+
* Uses SimpleSpanProcessor for immediate export before function termination.
|
|
6
|
+
*
|
|
7
|
+
* @module telemetry/edge
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { serve } from 'https://deno.land/std/http/server.ts'
|
|
12
|
+
* import { withTelemetry, traceSupabaseQuery } from '@kabran-tecnologia/kabran-config/telemetry/edge'
|
|
13
|
+
*
|
|
14
|
+
* serve(withTelemetry('my-function', async (req, span) => {
|
|
15
|
+
* const result = await traceSupabaseQuery('select', 'users', () =>
|
|
16
|
+
* supabase.from('users').select()
|
|
17
|
+
* )
|
|
18
|
+
* return new Response(JSON.stringify(result.data))
|
|
19
|
+
* }))
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { resolveConfig } from '../config/index.mjs'
|
|
24
|
+
import {
|
|
25
|
+
getTracesPath,
|
|
26
|
+
getExportTimeoutEdge,
|
|
27
|
+
getErrorResponseConfig,
|
|
28
|
+
DEFAULT_SERVICE_VERSION,
|
|
29
|
+
DEFAULT_TRACER_NAME_EDGE,
|
|
30
|
+
} from '../config/defaults.mjs'
|
|
31
|
+
import { recordError, setAttributes, generateInvocationId, safeWarn, safeLog } from '../shared/helpers.mjs'
|
|
32
|
+
|
|
33
|
+
// State
|
|
34
|
+
let provider = null
|
|
35
|
+
let initialized = false
|
|
36
|
+
let resolvedConfig = null
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize provider for edge function
|
|
40
|
+
*
|
|
41
|
+
* @param {string} serviceName - Service/function name
|
|
42
|
+
* @param {import('../shared/types').TelemetryConfig} [config] - Additional config
|
|
43
|
+
*/
|
|
44
|
+
async function initProvider(serviceName, config = {}) {
|
|
45
|
+
if (initialized) return
|
|
46
|
+
|
|
47
|
+
// Get environment (Deno-style)
|
|
48
|
+
const env = typeof Deno !== 'undefined' ? {
|
|
49
|
+
OTEL_ENDPOINT: Deno.env.get('OTEL_ENDPOINT'),
|
|
50
|
+
SERVICE_VERSION: Deno.env.get('SERVICE_VERSION'),
|
|
51
|
+
ENVIRONMENT: Deno.env.get('ENVIRONMENT'),
|
|
52
|
+
OTEL_ENABLED: Deno.env.get('OTEL_ENABLED'),
|
|
53
|
+
} : typeof process !== 'undefined' ? process.env : {}
|
|
54
|
+
|
|
55
|
+
resolvedConfig = resolveConfig(
|
|
56
|
+
{ serviceName, ...config },
|
|
57
|
+
env,
|
|
58
|
+
env.ENVIRONMENT || 'production'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// Default enabled for edge (production-like)
|
|
62
|
+
if (resolvedConfig.enabled === false && env.OTEL_ENABLED !== 'false') {
|
|
63
|
+
resolvedConfig.enabled = true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!resolvedConfig.enabled) return
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// Dynamic imports for Deno compatibility
|
|
70
|
+
const [
|
|
71
|
+
{ trace, context, propagation, SpanStatusCode },
|
|
72
|
+
{ NodeTracerProvider },
|
|
73
|
+
{ SimpleSpanProcessor },
|
|
74
|
+
{ OTLPTraceExporter },
|
|
75
|
+
{ Resource },
|
|
76
|
+
{
|
|
77
|
+
SEMRESATTRS_SERVICE_NAME,
|
|
78
|
+
SEMRESATTRS_SERVICE_VERSION,
|
|
79
|
+
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT,
|
|
80
|
+
},
|
|
81
|
+
{ W3CTraceContextPropagator },
|
|
82
|
+
] = await Promise.all([
|
|
83
|
+
import('@opentelemetry/api'),
|
|
84
|
+
import('@opentelemetry/sdk-trace-node'),
|
|
85
|
+
import('@opentelemetry/sdk-trace-base'),
|
|
86
|
+
import('@opentelemetry/exporter-trace-otlp-http'),
|
|
87
|
+
import('@opentelemetry/resources'),
|
|
88
|
+
import('@opentelemetry/semantic-conventions'),
|
|
89
|
+
import('@opentelemetry/core'),
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
provider = new NodeTracerProvider({
|
|
93
|
+
resource: new Resource({
|
|
94
|
+
[SEMRESATTRS_SERVICE_NAME]: resolvedConfig.serviceName,
|
|
95
|
+
[SEMRESATTRS_SERVICE_VERSION]: resolvedConfig.serviceVersion,
|
|
96
|
+
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: resolvedConfig.environment,
|
|
97
|
+
'service.namespace': resolvedConfig.namespace,
|
|
98
|
+
'faas.name': serviceName,
|
|
99
|
+
'faas.runtime': typeof Deno !== 'undefined' ? 'deno' : 'node',
|
|
100
|
+
...resolvedConfig.resourceAttributes,
|
|
101
|
+
}),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const exporter = new OTLPTraceExporter({
|
|
105
|
+
url: `${resolvedConfig.endpoint}${getTracesPath()}`,
|
|
106
|
+
timeoutMillis: getExportTimeoutEdge(),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Use SimpleSpanProcessor for serverless (immediate export)
|
|
110
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
|
|
111
|
+
|
|
112
|
+
// Configure W3C Trace Context propagation
|
|
113
|
+
propagation.setGlobalPropagator(new W3CTraceContextPropagator())
|
|
114
|
+
|
|
115
|
+
provider.register()
|
|
116
|
+
initialized = true
|
|
117
|
+
|
|
118
|
+
safeLog(`[Telemetry] Initialized: ${resolvedConfig.serviceName}@${resolvedConfig.serviceVersion}`)
|
|
119
|
+
} catch (error) {
|
|
120
|
+
safeWarn('[Telemetry] Failed to initialize:', error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get a tracer instance
|
|
126
|
+
*
|
|
127
|
+
* @param {string} [name] - Tracer name
|
|
128
|
+
* @returns {import('@opentelemetry/api').Tracer}
|
|
129
|
+
*/
|
|
130
|
+
export function getTracer(name) {
|
|
131
|
+
const { trace } = require('@opentelemetry/api')
|
|
132
|
+
return trace.getTracer(
|
|
133
|
+
name || resolvedConfig?.serviceName || DEFAULT_TRACER_NAME_EDGE,
|
|
134
|
+
resolvedConfig?.serviceVersion || DEFAULT_SERVICE_VERSION
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the currently active span
|
|
140
|
+
*
|
|
141
|
+
* @returns {import('@opentelemetry/api').Span|undefined}
|
|
142
|
+
*/
|
|
143
|
+
export function getCurrentSpan() {
|
|
144
|
+
const { trace } = require('@opentelemetry/api')
|
|
145
|
+
return trace.getActiveSpan()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get trace ID from current span
|
|
150
|
+
*
|
|
151
|
+
* @returns {string|undefined}
|
|
152
|
+
*/
|
|
153
|
+
export function getTraceId() {
|
|
154
|
+
return getCurrentSpan()?.spanContext().traceId
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract trace context from incoming request headers
|
|
159
|
+
*
|
|
160
|
+
* @param {Headers} headers - Request headers
|
|
161
|
+
* @returns {import('@opentelemetry/api').Context}
|
|
162
|
+
*/
|
|
163
|
+
export function extractContext(headers) {
|
|
164
|
+
const { context, propagation } = require('@opentelemetry/api')
|
|
165
|
+
const carrier = {}
|
|
166
|
+
headers.forEach((value, key) => {
|
|
167
|
+
carrier[key.toLowerCase()] = value
|
|
168
|
+
})
|
|
169
|
+
return propagation.extract(context.active(), carrier)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Inject trace context into outgoing request headers
|
|
174
|
+
*
|
|
175
|
+
* @param {Headers} headers - Headers to inject into
|
|
176
|
+
*/
|
|
177
|
+
export function injectContext(headers) {
|
|
178
|
+
const { context, propagation } = require('@opentelemetry/api')
|
|
179
|
+
const carrier = {}
|
|
180
|
+
propagation.inject(context.active(), carrier)
|
|
181
|
+
Object.entries(carrier).forEach(([key, value]) => {
|
|
182
|
+
headers.set(key, value)
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a span for tracking an operation
|
|
188
|
+
*
|
|
189
|
+
* @template T
|
|
190
|
+
* @param {string} name - Span name
|
|
191
|
+
* @param {(span: import('@opentelemetry/api').Span) => T} fn - Function to execute
|
|
192
|
+
* @param {Record<string, string|number|boolean>} [attributes] - Initial attributes
|
|
193
|
+
* @returns {T}
|
|
194
|
+
*/
|
|
195
|
+
export function createSpan(name, fn, attributes) {
|
|
196
|
+
const tracer = getTracer()
|
|
197
|
+
const { SpanStatusCode } = require('@opentelemetry/api')
|
|
198
|
+
|
|
199
|
+
return tracer.startActiveSpan(name, (span) => {
|
|
200
|
+
if (attributes) {
|
|
201
|
+
setAttributes(span, attributes)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const result = fn(span)
|
|
206
|
+
|
|
207
|
+
if (result instanceof Promise) {
|
|
208
|
+
return result
|
|
209
|
+
.then((value) => {
|
|
210
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
211
|
+
span.end()
|
|
212
|
+
return value
|
|
213
|
+
})
|
|
214
|
+
.catch((error) => {
|
|
215
|
+
recordError(span, error, SpanStatusCode)
|
|
216
|
+
span.end()
|
|
217
|
+
throw error
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
222
|
+
span.end()
|
|
223
|
+
return result
|
|
224
|
+
} catch (error) {
|
|
225
|
+
recordError(span, error, SpanStatusCode)
|
|
226
|
+
span.end()
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create an async span
|
|
234
|
+
*
|
|
235
|
+
* @template T
|
|
236
|
+
* @param {string} name - Span name
|
|
237
|
+
* @param {(span: import('@opentelemetry/api').Span) => Promise<T>} fn - Async function
|
|
238
|
+
* @param {Record<string, string|number|boolean>} [attributes] - Initial attributes
|
|
239
|
+
* @returns {Promise<T>}
|
|
240
|
+
*/
|
|
241
|
+
export async function createAsyncSpan(name, fn, attributes) {
|
|
242
|
+
const tracer = getTracer()
|
|
243
|
+
const { trace, context, SpanStatusCode } = require('@opentelemetry/api')
|
|
244
|
+
|
|
245
|
+
const span = tracer.startSpan(name)
|
|
246
|
+
|
|
247
|
+
if (attributes) {
|
|
248
|
+
setAttributes(span, attributes)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const result = await context.with(trace.setSpan(context.active(), span), () => fn(span))
|
|
253
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
254
|
+
return result
|
|
255
|
+
} catch (error) {
|
|
256
|
+
recordError(span, error, SpanStatusCode)
|
|
257
|
+
throw error
|
|
258
|
+
} finally {
|
|
259
|
+
span.end()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Wrap a request handler with automatic telemetry instrumentation
|
|
265
|
+
*
|
|
266
|
+
* @param {string} functionName - Function name (used as service name)
|
|
267
|
+
* @param {import('../shared/types').EdgeHandler} handler - Request handler
|
|
268
|
+
* @param {import('../shared/types').TelemetryConfig} [config] - Additional config
|
|
269
|
+
* @returns {(req: Request) => Promise<Response>}
|
|
270
|
+
*/
|
|
271
|
+
export function withTelemetry(functionName, handler, config = {}) {
|
|
272
|
+
return async (req) => {
|
|
273
|
+
// Initialize provider
|
|
274
|
+
await initProvider(functionName, config)
|
|
275
|
+
|
|
276
|
+
const { trace, context, SpanStatusCode } = await import('@opentelemetry/api')
|
|
277
|
+
const {
|
|
278
|
+
SEMATTRS_HTTP_METHOD,
|
|
279
|
+
SEMATTRS_HTTP_URL,
|
|
280
|
+
SEMATTRS_HTTP_STATUS_CODE,
|
|
281
|
+
SEMATTRS_HTTP_USER_AGENT,
|
|
282
|
+
} = await import('@opentelemetry/semantic-conventions')
|
|
283
|
+
|
|
284
|
+
// Skip telemetry for OPTIONS (CORS preflight)
|
|
285
|
+
if (req.method === 'OPTIONS') {
|
|
286
|
+
const noopSpan = trace.getTracer(functionName).startSpan('cors-preflight')
|
|
287
|
+
try {
|
|
288
|
+
return await handler(req, noopSpan)
|
|
289
|
+
} finally {
|
|
290
|
+
noopSpan.end()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Skip if disabled
|
|
295
|
+
if (!resolvedConfig?.enabled) {
|
|
296
|
+
const noopSpan = trace.getTracer(functionName).startSpan('noop')
|
|
297
|
+
try {
|
|
298
|
+
return await handler(req, noopSpan)
|
|
299
|
+
} finally {
|
|
300
|
+
noopSpan.end()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const tracer = getTracer(functionName)
|
|
305
|
+
const parentContext = extractContext(req.headers)
|
|
306
|
+
|
|
307
|
+
const span = tracer.startSpan(
|
|
308
|
+
`${functionName}.handler`,
|
|
309
|
+
{
|
|
310
|
+
attributes: {
|
|
311
|
+
[SEMATTRS_HTTP_METHOD]: req.method,
|
|
312
|
+
[SEMATTRS_HTTP_URL]: req.url,
|
|
313
|
+
[SEMATTRS_HTTP_USER_AGENT]: req.headers.get('user-agent') || 'unknown',
|
|
314
|
+
'faas.trigger': 'http',
|
|
315
|
+
'faas.invocation_id': generateInvocationId(),
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
parentContext
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
const startTime = Date.now()
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const response = await context.with(
|
|
325
|
+
trace.setSpan(parentContext, span),
|
|
326
|
+
() => handler(req, span)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status)
|
|
330
|
+
span.setAttribute('http.response_content_length', response.headers.get('content-length') || '0')
|
|
331
|
+
span.setAttribute('faas.duration_ms', Date.now() - startTime)
|
|
332
|
+
|
|
333
|
+
if (response.status >= 400) {
|
|
334
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${response.status}` })
|
|
335
|
+
} else {
|
|
336
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return response
|
|
340
|
+
} catch (error) {
|
|
341
|
+
recordError(span, error, SpanStatusCode)
|
|
342
|
+
span.setAttribute('faas.duration_ms', Date.now() - startTime)
|
|
343
|
+
|
|
344
|
+
const errorConfig = getErrorResponseConfig()
|
|
345
|
+
return new Response(
|
|
346
|
+
JSON.stringify({
|
|
347
|
+
error: errorConfig.message,
|
|
348
|
+
code: errorConfig.code,
|
|
349
|
+
trace_id: span.spanContext().traceId,
|
|
350
|
+
}),
|
|
351
|
+
{
|
|
352
|
+
status: 500,
|
|
353
|
+
headers: { 'Content-Type': 'application/json' },
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
} finally {
|
|
357
|
+
span.end()
|
|
358
|
+
|
|
359
|
+
// Force flush for serverless
|
|
360
|
+
if (provider) {
|
|
361
|
+
try {
|
|
362
|
+
await provider.forceFlush()
|
|
363
|
+
} catch {
|
|
364
|
+
// Ignore flush errors
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Wrap a Supabase query with telemetry
|
|
373
|
+
*
|
|
374
|
+
* @template T
|
|
375
|
+
* @param {string} operation - Operation type (select, insert, update, delete)
|
|
376
|
+
* @param {string} table - Table name
|
|
377
|
+
* @param {() => Promise<{data: T, error: Error|null}>} fn - Query function
|
|
378
|
+
* @returns {Promise<{data: T, error: Error|null}>}
|
|
379
|
+
*/
|
|
380
|
+
export async function traceSupabaseQuery(operation, table, fn) {
|
|
381
|
+
return createAsyncSpan(
|
|
382
|
+
`supabase.${operation}`,
|
|
383
|
+
async (span) => {
|
|
384
|
+
span.setAttribute('db.system', 'postgresql')
|
|
385
|
+
span.setAttribute('db.name', 'supabase')
|
|
386
|
+
span.setAttribute('db.operation', operation)
|
|
387
|
+
span.setAttribute('db.sql.table', table)
|
|
388
|
+
|
|
389
|
+
const result = await fn()
|
|
390
|
+
|
|
391
|
+
const { SpanStatusCode } = await import('@opentelemetry/api')
|
|
392
|
+
|
|
393
|
+
if (result.error) {
|
|
394
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.message })
|
|
395
|
+
span.setAttribute('db.error', result.error.message)
|
|
396
|
+
} else {
|
|
397
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
398
|
+
if (Array.isArray(result.data)) {
|
|
399
|
+
span.setAttribute('db.result_count', result.data.length)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return result
|
|
404
|
+
},
|
|
405
|
+
{ 'db.system': 'postgresql' }
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Add an event to the current span
|
|
411
|
+
*
|
|
412
|
+
* @param {string} name - Event name
|
|
413
|
+
* @param {Record<string, string|number|boolean>} [attributes] - Event attributes
|
|
414
|
+
*/
|
|
415
|
+
export function addSpanEvent(name, attributes) {
|
|
416
|
+
const span = getCurrentSpan()
|
|
417
|
+
span?.addEvent(name, attributes)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Set attributes on the current span
|
|
422
|
+
*
|
|
423
|
+
* @param {Record<string, string|number|boolean>} attributes - Attributes to set
|
|
424
|
+
*/
|
|
425
|
+
export function setSpanAttributes(attributes) {
|
|
426
|
+
const span = getCurrentSpan()
|
|
427
|
+
if (span) {
|
|
428
|
+
setAttributes(span, attributes)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Shutdown telemetry provider
|
|
434
|
+
*
|
|
435
|
+
* @returns {Promise<void>}
|
|
436
|
+
*/
|
|
437
|
+
export async function shutdownTelemetry() {
|
|
438
|
+
if (provider) {
|
|
439
|
+
await provider.shutdown()
|
|
440
|
+
initialized = false
|
|
441
|
+
provider = null
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Re-exports
|
|
446
|
+
export { resolveConfig } from '../config/index.mjs'
|