@geekmidas/telescope 1.1.1 → 10.0.0-alpha.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 +14 -4
- package/dist/{hono-Bz9PQQzj.mjs → hono-Bw-ey6CT.mjs} +2 -2
- package/dist/{hono-Bz9PQQzj.mjs.map → hono-Bw-ey6CT.mjs.map} +1 -1
- package/dist/{hono-Z_JIICmt.cjs → hono-CPHX7FAm.cjs} +2 -2
- package/dist/{hono-Z_JIICmt.cjs.map → hono-CPHX7FAm.cjs.map} +1 -1
- package/dist/{http-Crsriv32.mjs → http-BIvr0YdI.mjs} +1 -1
- package/dist/{http-Crsriv32.mjs.map → http-BIvr0YdI.mjs.map} +1 -1
- package/dist/{http-JycFRfSe.cjs → http-BsqmhurZ.cjs} +1 -1
- package/dist/{http-JycFRfSe.cjs.map → http-BsqmhurZ.cjs.map} +1 -1
- package/dist/{http-D5kn3_Mt.d.mts → http-DULM1u2L.d.cts} +3 -3
- package/dist/{http-D5kn3_Mt.d.mts.map → http-DULM1u2L.d.cts.map} +1 -1
- package/dist/{http-Dvca6fC3.d.cts → http-GB-Nlvwc.d.mts} +3 -3
- package/dist/{http-Dvca6fC3.d.cts.map → http-GB-Nlvwc.d.mts.map} +1 -1
- package/dist/instrumentation/hono.cjs +2 -2
- package/dist/instrumentation/hono.mjs +2 -2
- package/dist/instrumentation/http.cjs +1 -1
- package/dist/instrumentation/http.d.cts +1 -1
- package/dist/instrumentation/http.d.mts +1 -1
- package/dist/instrumentation/http.mjs +1 -1
- package/dist/instrumentation/index.cjs +5 -5
- package/dist/instrumentation/index.d.cts +3 -3
- package/dist/instrumentation/index.d.mts +3 -3
- package/dist/instrumentation/index.mjs +5 -5
- package/dist/instrumentation/middleware.cjs +2 -2
- package/dist/instrumentation/middleware.d.cts +1 -1
- package/dist/instrumentation/middleware.d.mts +1 -1
- package/dist/instrumentation/middleware.mjs +2 -2
- package/dist/instrumentation/otel.cjs +2 -2
- package/dist/instrumentation/otel.d.cts +1 -1
- package/dist/instrumentation/otel.d.mts +1 -1
- package/dist/instrumentation/otel.mjs +2 -2
- package/dist/instrumentation/setup.cjs +1 -1
- package/dist/instrumentation/setup.mjs +1 -1
- package/dist/logger/console.d.cts.map +1 -1
- package/dist/{middleware-D3v5SnSs.cjs → middleware-Bcq6XXGG.cjs} +3 -3
- package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
- package/dist/{middleware-Dy1ctr2N.mjs → middleware-D1XEHvyy.mjs} +3 -3
- package/dist/middleware-D1XEHvyy.mjs.map +1 -0
- package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
- package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
- package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
- package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
- package/dist/{otel-DPKNyJ0t.mjs → otel-B9bNoeZ2.mjs} +4 -4
- package/dist/otel-B9bNoeZ2.mjs.map +1 -0
- package/dist/{otel-1bJPb2Z7.d.mts → otel-Bk89b1lX.d.cts} +3 -3
- package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
- package/dist/{otel-DuiBeBPU.cjs → otel-BzjOtVxZ.cjs} +4 -4
- package/dist/otel-BzjOtVxZ.cjs.map +1 -0
- package/dist/{otel-SBVoVEHr.d.cts → otel-lPMsM26e.d.mts} +3 -3
- package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
- package/dist/otlp/hono.cjs +3 -3
- package/dist/otlp/hono.mjs +3 -3
- package/dist/otlp/index.cjs +3 -3
- package/dist/otlp/index.mjs +3 -3
- package/dist/otlp/receiver.cjs +3 -3
- package/dist/otlp/receiver.mjs +3 -3
- package/dist/otlp/transformer.cjs +2 -2
- package/dist/otlp/transformer.mjs +2 -2
- package/dist/otlp/types.cjs +1 -1
- package/dist/otlp/types.mjs +1 -1
- package/dist/{receiver-q4f7eirs.mjs → receiver-D46dfkYI.mjs} +2 -2
- package/dist/{receiver-q4f7eirs.mjs.map → receiver-D46dfkYI.mjs.map} +1 -1
- package/dist/{receiver-BW_4go_X.cjs → receiver-slCoAFat.cjs} +2 -2
- package/dist/{receiver-BW_4go_X.cjs.map → receiver-slCoAFat.cjs.map} +1 -1
- package/dist/{setup-BhCp2LUp.mjs → setup-DC9JMEYZ.mjs} +1 -1
- package/dist/{setup-BhCp2LUp.mjs.map → setup-DC9JMEYZ.mjs.map} +1 -1
- package/dist/{setup-_uT4L_x5.cjs → setup-DufxQpi9.cjs} +1 -1
- package/dist/{setup-_uT4L_x5.cjs.map → setup-DufxQpi9.cjs.map} +1 -1
- package/dist/storage/kysely.cjs +48 -38
- package/dist/storage/kysely.cjs.map +1 -1
- package/dist/storage/kysely.d.cts +29 -9
- package/dist/storage/kysely.d.cts.map +1 -1
- package/dist/storage/kysely.d.mts +29 -9
- package/dist/storage/kysely.d.mts.map +1 -1
- package/dist/storage/kysely.mjs +48 -38
- package/dist/storage/kysely.mjs.map +1 -1
- package/dist/tracing-BX8uY2nR.d.cts.map +1 -1
- package/dist/tracing-PnECEbEu.d.mts.map +1 -1
- package/dist/{transformer-CFrJQkUg.mjs → transformer-CFQ4WFk6.mjs} +2 -2
- package/dist/{transformer-CFrJQkUg.mjs.map → transformer-CFQ4WFk6.mjs.map} +1 -1
- package/dist/{transformer-DvDYyFRQ.cjs → transformer-Csl6fbgv.cjs} +2 -2
- package/dist/{transformer-DvDYyFRQ.cjs.map → transformer-Csl6fbgv.cjs.map} +1 -1
- package/dist/types-B1EIa_cQ.d.mts.map +1 -1
- package/dist/{types-BBeTDpHr.cjs → types-BMiHgsi4.cjs} +1 -1
- package/dist/{types-BBeTDpHr.cjs.map → types-BMiHgsi4.cjs.map} +1 -1
- package/dist/{types-CXYLhG_G.mjs → types-Cj59nqy7.mjs} +1 -1
- package/dist/{types-CXYLhG_G.mjs.map → types-Cj59nqy7.mjs.map} +1 -1
- package/dist/types-l7Rlb5Mg.d.cts.map +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -51
- package/dist/middleware-D3v5SnSs.cjs.map +0 -1
- package/dist/middleware-Dy1ctr2N.mjs.map +0 -1
- package/dist/otel-DPKNyJ0t.mjs.map +0 -1
- package/dist/otel-DuiBeBPU.cjs.map +0 -1
- package/src/Telescope.ts +0 -561
- package/src/__tests__/Telescope.spec.ts +0 -438
- package/src/__tests__/pino-transport.spec.ts +0 -115
- package/src/__tests__/redact.spec.ts +0 -336
- package/src/adapters/__tests__/lambda.spec.ts +0 -318
- package/src/adapters/__tests__/middy.spec.ts +0 -741
- package/src/adapters/index.ts +0 -22
- package/src/adapters/lambda.ts +0 -432
- package/src/adapters/middy.ts +0 -402
- package/src/adapters/types.ts +0 -181
- package/src/core.ts +0 -51
- package/src/index.ts +0 -34
- package/src/instrumentation/__tests__/core.spec.ts +0 -248
- package/src/instrumentation/__tests__/hono.spec.ts +0 -348
- package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
- package/src/instrumentation/__tests__/otel.spec.ts +0 -291
- package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
- package/src/instrumentation/core.ts +0 -228
- package/src/instrumentation/hono.ts +0 -270
- package/src/instrumentation/http.ts +0 -306
- package/src/instrumentation/index.ts +0 -90
- package/src/instrumentation/middleware.ts +0 -297
- package/src/instrumentation/otel.ts +0 -271
- package/src/instrumentation/setup.ts +0 -246
- package/src/instrumentation/tracing.ts +0 -145
- package/src/logger/__tests__/console.spec.ts +0 -366
- package/src/logger/__tests__/pino.spec.ts +0 -217
- package/src/logger/console.ts +0 -230
- package/src/logger/pino.ts +0 -201
- package/src/metrics/MetricsAggregator.ts +0 -555
- package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
- package/src/metrics/index.ts +0 -2
- package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
- package/src/metrics/storage/MetricsStorage.ts +0 -74
- package/src/metrics/storage/index.ts +0 -3
- package/src/metrics/storage/types.ts +0 -243
- package/src/otlp/__tests__/receiver.spec.ts +0 -367
- package/src/otlp/__tests__/transformer.spec.ts +0 -455
- package/src/otlp/hono.ts +0 -120
- package/src/otlp/index.ts +0 -22
- package/src/otlp/receiver.ts +0 -162
- package/src/otlp/transformer.ts +0 -367
- package/src/otlp/types.ts +0 -364
- package/src/redact.ts +0 -116
- package/src/server/__tests__/hono.spec.ts +0 -814
- package/src/server/hono.ts +0 -442
- package/src/storage/__tests__/kysely.spec.ts +0 -715
- package/src/storage/__tests__/memory.spec.ts +0 -694
- package/src/storage/kysely.ts +0 -599
- package/src/storage/memory.ts +0 -204
- package/src/types.ts +0 -447
- package/src/ui-assets.ts +0 -40
- package/tsconfig.json +0 -9
- package/ui/CHANGELOG.md +0 -7
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -24
- package/ui/src/App.tsx +0 -595
- package/ui/src/api.ts +0 -88
- package/ui/src/components/ExceptionDetail.tsx +0 -100
- package/ui/src/components/FilterBar.tsx +0 -162
- package/ui/src/components/LogDetail.tsx +0 -91
- package/ui/src/components/RequestDetail.tsx +0 -143
- package/ui/src/main.tsx +0 -10
- package/ui/src/styles.css +0 -11
- package/ui/src/types.ts +0 -63
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/src/vite-plugin-gkm-config.ts +0 -54
- package/ui/tsconfig.json +0 -20
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -13
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"middleware-Dy1ctr2N.mjs","names":["event: any","options: TelemetryMiddlewareOptions","lambdaContext: LambdaContext","route: string | undefined","host: string | undefined","scheme: 'http' | 'https' | undefined","userAgent: string | undefined","clientIp: string | undefined","requestId: string | undefined","attrs: HttpSpanAttributes","headers: Record<string, string | string[] | undefined>","normalized: Record<string, string | string[] | undefined>","fn: () => Promise<T>"],"sources":["../src/instrumentation/middleware.ts"],"sourcesContent":["/**\n * OpenTelemetry instrumentation middleware for @geekmidas/constructs\n *\n * This middleware automatically creates spans for HTTP requests,\n * extracts trace context from incoming headers, and records\n * request/response metadata as span attributes.\n */\nimport type { MiddlewareObj } from '@middy/core';\nimport { context, type Span, trace } from '@opentelemetry/api';\nimport type { Context as LambdaContext } from 'aws-lambda';\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\textractTraceContext,\n\ttype HttpSpanAttributes,\n} from './http';\n\n/**\n * Options for the telemetry middleware\n */\nexport interface TelemetryMiddlewareOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract endpoint name from event\n\t */\n\tgetEndpointName?: (event: any) => string | undefined;\n\n\t/**\n\t * Custom function to extract operation ID from event\n\t */\n\tgetOperationId?: (event: any) => string | undefined;\n\n\t/**\n\t * Custom function to extract user ID from event\n\t */\n\tgetUserId?: (event: any) => string | undefined;\n\n\t/**\n\t * Whether to skip tracing for this request\n\t */\n\tshouldSkip?: (event: any) => boolean;\n}\n\n// Symbol for storing span on event\nconst SPAN_SYMBOL = Symbol('telemetry.span');\nconst CONTEXT_SYMBOL = Symbol('telemetry.context');\n\n/**\n * Get the current span from the Lambda event\n */\nexport function getSpanFromEvent(event: any): Span | undefined {\n\treturn event?.[SPAN_SYMBOL];\n}\n\n/**\n * Get the current trace context from the Lambda event\n */\nexport function getContextFromEvent(event: any): any {\n\treturn event?.[CONTEXT_SYMBOL];\n}\n\n/**\n * Create a telemetry middleware for Lambda handlers\n *\n * @example\n * ```typescript\n * import { telemetryMiddleware } from '@geekmidas/telescope/instrumentation';\n *\n * const adaptor = new AmazonApiGatewayV2Endpoint(envParser, endpoint, {\n * telemetry: { middleware: telemetryMiddleware() },\n * });\n * ```\n */\nexport function telemetryMiddleware(\n\toptions: TelemetryMiddlewareOptions = {},\n): MiddlewareObj<any, any, Error, LambdaContext> {\n\treturn {\n\t\tbefore: async (request) => {\n\t\t\t// Skip if custom skip function returns true\n\t\t\tif (options.shouldSkip?.(request.event)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst event = request.event;\n\t\t\tconst lambdaContext = request.context;\n\n\t\t\t// Extract trace context from headers\n\t\t\tconst headers = normalizeHeaders(event.headers || {});\n\t\t\tconst parentContext = extractTraceContext(headers);\n\n\t\t\t// Build span attributes from event\n\t\t\tconst attrs = buildSpanAttributes(event, lambdaContext, options);\n\n\t\t\t// Create the span\n\t\t\tconst span = createHttpServerSpan(attrs, parentContext);\n\n\t\t\t// Store span and context on event for access in handler\n\t\t\tevent[SPAN_SYMBOL] = span;\n\t\t\tevent[CONTEXT_SYMBOL] = trace.setSpan(parentContext, span);\n\n\t\t\t// Add Lambda-specific attributes\n\t\t\tspan.setAttribute('faas.invocation_id', lambdaContext.awsRequestId);\n\t\t\tspan.setAttribute('faas.name', lambdaContext.functionName);\n\n\t\t\tif (options.recordBody && event.body) {\n\t\t\t\tconst bodyStr =\n\t\t\t\t\ttypeof event.body === 'string'\n\t\t\t\t\t\t? event.body\n\t\t\t\t\t\t: JSON.stringify(event.body);\n\t\t\t\t// Truncate large bodies\n\t\t\t\tspan.setAttribute(\n\t\t\t\t\t'http.request.body',\n\t\t\t\t\tbodyStr.length > 4096 ? `${bodyStr.slice(0, 4096)}...` : bodyStr,\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\n\t\tafter: async (request) => {\n\t\t\tconst span = getSpanFromEvent(request.event);\n\t\t\tif (!span) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst response = request.response;\n\t\t\tconst statusCode =\n\t\t\t\ttypeof response?.statusCode === 'number' ? response.statusCode : 200;\n\n\t\t\tif (options.recordResponseBody && response?.body) {\n\t\t\t\tconst bodyStr =\n\t\t\t\t\ttypeof response.body === 'string'\n\t\t\t\t\t\t? response.body\n\t\t\t\t\t\t: JSON.stringify(response.body);\n\t\t\t\tspan.setAttribute(\n\t\t\t\t\t'http.response.body',\n\t\t\t\t\tbodyStr.length > 4096 ? `${bodyStr.slice(0, 4096)}...` : bodyStr,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tendHttpSpan(span, { statusCode });\n\t\t},\n\n\t\tonError: async (request) => {\n\t\t\tconst span = getSpanFromEvent(request.event);\n\t\t\tif (!span) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst error = request.error;\n\t\t\tconst response = request.response;\n\n\t\t\t// Determine status code from response or error\n\t\t\tlet statusCode = 500;\n\t\t\tif (response?.statusCode) {\n\t\t\t\tstatusCode = response.statusCode;\n\t\t\t} else if (error && 'statusCode' in error) {\n\t\t\t\tstatusCode = (error as any).statusCode;\n\t\t\t}\n\n\t\t\tendHttpSpan(\n\t\t\t\tspan,\n\t\t\t\t{ statusCode },\n\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t);\n\t\t},\n\t};\n}\n\n/**\n * Build span attributes from Lambda event\n */\nfunction buildSpanAttributes(\n\tevent: any,\n\tlambdaContext: LambdaContext,\n\toptions: TelemetryMiddlewareOptions,\n): HttpSpanAttributes {\n\t// Detect event type (API Gateway v1, v2, ALB, or direct invoke)\n\tconst isV2 =\n\t\t'requestContext' in event && 'http' in (event.requestContext || {});\n\tconst isV1 =\n\t\t'requestContext' in event &&\n\t\t'httpMethod' in event &&\n\t\t!('http' in (event.requestContext || {}));\n\n\tlet method = 'INVOKE';\n\tlet path = '/';\n\tlet route: string | undefined;\n\tlet host: string | undefined;\n\tlet scheme: 'http' | 'https' | undefined;\n\tlet userAgent: string | undefined;\n\tlet clientIp: string | undefined;\n\tlet requestId: string | undefined;\n\n\tif (isV2) {\n\t\t// API Gateway v2 (HTTP API)\n\t\tconst http = event.requestContext.http;\n\t\tmethod = http.method;\n\t\tpath = event.rawPath || http.path;\n\t\troute = event.routeKey?.replace(/^[A-Z]+\\s+/, ''); // Remove HTTP method prefix\n\t\thost = event.headers?.host;\n\t\tscheme = 'https';\n\t\tuserAgent = http.userAgent;\n\t\tclientIp = http.sourceIp;\n\t\trequestId = event.requestContext.requestId;\n\t} else if (isV1) {\n\t\t// API Gateway v1 (REST API)\n\t\tmethod = event.httpMethod;\n\t\tpath = event.path;\n\t\troute = event.resource;\n\t\thost = event.headers?.Host || event.headers?.host;\n\t\tscheme = 'https';\n\t\tuserAgent = event.headers?.['User-Agent'] || event.headers?.['user-agent'];\n\t\tclientIp = event.requestContext?.identity?.sourceIp;\n\t\trequestId = event.requestContext?.requestId;\n\t} else if ('httpMethod' in event) {\n\t\t// ALB event\n\t\tmethod = event.httpMethod;\n\t\tpath = event.path;\n\t\thost = event.headers?.host;\n\t\tuserAgent = event.headers?.['user-agent'] || event.headers?.['User-Agent'];\n\t\tclientIp = event.headers?.['x-forwarded-for']?.split(',')[0]?.trim();\n\t}\n\n\tconst attrs: HttpSpanAttributes = {\n\t\tmethod,\n\t\tpath,\n\t\troute,\n\t\thost,\n\t\tscheme,\n\t\tuserAgent,\n\t\tclientIp,\n\t\trequestId: requestId || lambdaContext.awsRequestId,\n\t};\n\n\t// Add endpoint metadata if extractors provided\n\tif (options.getEndpointName || options.getOperationId) {\n\t\tattrs.endpoint = {\n\t\t\tname: options.getEndpointName?.(event),\n\t\t\toperationId: options.getOperationId?.(event),\n\t\t};\n\t}\n\n\t// Add user metadata if extractor provided\n\tif (options.getUserId) {\n\t\tconst userId = options.getUserId(event);\n\t\tif (userId) {\n\t\t\tattrs.user = { userId };\n\t\t}\n\t}\n\n\treturn attrs;\n}\n\n/**\n * Normalize headers to lowercase keys\n */\nfunction normalizeHeaders(\n\theaders: Record<string, string | string[] | undefined>,\n): Record<string, string | string[] | undefined> {\n\tconst normalized: Record<string, string | string[] | undefined> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tnormalized[key.toLowerCase()] = value;\n\t}\n\treturn normalized;\n}\n\n/**\n * Run a function within the span context from a Lambda event\n *\n * @example\n * ```typescript\n * await withEventContext(event, async () => {\n * // Any spans created here will be children of the request span\n * await doSomething();\n * });\n * ```\n */\nexport async function withEventContext<T>(\n\tevent: any,\n\tfn: () => Promise<T>,\n): Promise<T> {\n\tconst ctx = getContextFromEvent(event);\n\tif (!ctx) {\n\t\treturn fn();\n\t}\n\treturn context.with(ctx, fn);\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"otel-DPKNyJ0t.mjs","names":["options: OTelTelemetryOptions","request: TelemetryRequest","ctx: TelemetryContext","response: TelemetryResponse","error: Error","event: any","lambdaContext: any","route: string | undefined","host: string | undefined","scheme: 'http' | 'https' | undefined","userAgent: string | undefined","clientIp: string | undefined","requestId: string | undefined","attrs: HttpSpanAttributes","headers: Record<string, string | string[] | undefined>","normalized: Record<string, string | string[] | undefined>"],"sources":["../src/instrumentation/otel.ts"],"sourcesContent":["/**\n * OpenTelemetry implementation of the Telemetry interface\n *\n * This provides automatic span creation for Lambda handlers using\n * OpenTelemetry. Works with any OTel-compatible backend (Jaeger, Zipkin,\n * Datadog, Honeycomb, etc.)\n */\nimport { type Span, trace } from '@opentelemetry/api';\nimport type { Context as LambdaContext } from 'aws-lambda';\n\n/**\n * Context object returned by onRequestStart, passed to subsequent hooks.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface TelemetryContext {\n\t[key: string]: unknown;\n}\n\n/**\n * Request information passed to telemetry hooks.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface TelemetryRequest {\n\tevent: any;\n\tcontext: LambdaContext;\n}\n\n/**\n * Response information passed to onRequestEnd.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface TelemetryResponse {\n\tstatusCode: number;\n\tbody?: string;\n\theaders?: Record<string, string>;\n}\n\n/**\n * Telemetry interface for instrumenting endpoint requests.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface Telemetry {\n\tonRequestStart(request: TelemetryRequest): TelemetryContext;\n\tonRequestEnd(ctx: TelemetryContext, response: TelemetryResponse): void;\n\tonRequestError(ctx: TelemetryContext, error: Error): void;\n}\n\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\textractTraceContext,\n\ttype HttpSpanAttributes,\n} from './http';\n\n/**\n * Options for OTelTelemetry\n */\nexport interface OTelTelemetryOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract user ID from event\n\t */\n\tgetUserId?: (event: any) => string | undefined;\n\n\t/**\n\t * Custom function to extract endpoint name from event\n\t */\n\tgetEndpointName?: (event: any) => string | undefined;\n\n\t/**\n\t * Custom function to extract operation ID from event\n\t */\n\tgetOperationId?: (event: any) => string | undefined;\n}\n\n/**\n * Telemetry context with span reference\n */\ninterface OTelContext extends TelemetryContext {\n\tspan: Span;\n\totelContext: any;\n}\n\n/**\n * OpenTelemetry implementation of the Telemetry interface\n *\n * @example\n * ```typescript\n * import { OTelTelemetry } from '@geekmidas/telescope/instrumentation';\n *\n * const adaptor = new AmazonApiGatewayV2Endpoint(envParser, endpoint, {\n * telemetry: new OTelTelemetry(),\n * });\n * ```\n *\n * @example With options\n * ```typescript\n * const adaptor = new AmazonApiGatewayV2Endpoint(envParser, endpoint, {\n * telemetry: new OTelTelemetry({\n * recordBody: true,\n * getUserId: (event) => event.requestContext?.authorizer?.userId,\n * }),\n * });\n * ```\n */\nexport class OTelTelemetry implements Telemetry {\n\tconstructor(private options: OTelTelemetryOptions = {}) {}\n\n\tonRequestStart(request: TelemetryRequest): TelemetryContext {\n\t\tconst { event, context: lambdaContext } = request;\n\n\t\t// Extract trace context from headers\n\t\tconst headers = normalizeHeaders(event.headers || {});\n\t\tconst parentContext = extractTraceContext(headers);\n\n\t\t// Build span attributes from event\n\t\tconst attrs = this.buildSpanAttributes(event, lambdaContext);\n\n\t\t// Create the span\n\t\tconst span = createHttpServerSpan(attrs, parentContext);\n\t\tconst otelContext = trace.setSpan(parentContext, span);\n\n\t\t// Add Lambda-specific attributes\n\t\tspan.setAttribute('faas.invocation_id', lambdaContext.awsRequestId);\n\t\tspan.setAttribute('faas.name', lambdaContext.functionName);\n\n\t\t// Record body if requested\n\t\tif (this.options.recordBody && event.body) {\n\t\t\tconst bodyStr =\n\t\t\t\ttypeof event.body === 'string'\n\t\t\t\t\t? event.body\n\t\t\t\t\t: JSON.stringify(event.body);\n\t\t\tspan.setAttribute(\n\t\t\t\t'http.request.body',\n\t\t\t\tbodyStr.length > 4096 ? `${bodyStr.slice(0, 4096)}...` : bodyStr,\n\t\t\t);\n\t\t}\n\n\t\treturn { span, otelContext } as OTelContext;\n\t}\n\n\tonRequestEnd(ctx: TelemetryContext, response: TelemetryResponse): void {\n\t\tconst { span } = ctx as OTelContext;\n\n\t\tif (this.options.recordResponseBody && response.body) {\n\t\t\tspan.setAttribute(\n\t\t\t\t'http.response.body',\n\t\t\t\tresponse.body.length > 4096\n\t\t\t\t\t? `${response.body.slice(0, 4096)}...`\n\t\t\t\t\t: response.body,\n\t\t\t);\n\t\t}\n\n\t\tendHttpSpan(span, { statusCode: response.statusCode });\n\t}\n\n\tonRequestError(ctx: TelemetryContext, error: Error): void {\n\t\tconst { span } = ctx as OTelContext;\n\t\tendHttpSpan(span, { statusCode: 500 }, error);\n\t}\n\n\t/**\n\t * Build span attributes from Lambda event\n\t */\n\tprivate buildSpanAttributes(\n\t\tevent: any,\n\t\tlambdaContext: any,\n\t): HttpSpanAttributes {\n\t\t// Detect event type (API Gateway v1, v2, ALB, or direct invoke)\n\t\tconst isV2 =\n\t\t\t'requestContext' in event && 'http' in (event.requestContext || {});\n\t\tconst isV1 =\n\t\t\t'requestContext' in event &&\n\t\t\t'httpMethod' in event &&\n\t\t\t!('http' in (event.requestContext || {}));\n\n\t\tlet method = 'INVOKE';\n\t\tlet path = '/';\n\t\tlet route: string | undefined;\n\t\tlet host: string | undefined;\n\t\tlet scheme: 'http' | 'https' | undefined;\n\t\tlet userAgent: string | undefined;\n\t\tlet clientIp: string | undefined;\n\t\tlet requestId: string | undefined;\n\n\t\tif (isV2) {\n\t\t\t// API Gateway v2 (HTTP API)\n\t\t\tconst http = event.requestContext.http;\n\t\t\tmethod = http.method;\n\t\t\tpath = event.rawPath || http.path;\n\t\t\troute = event.routeKey?.replace(/^[A-Z]+\\s+/, ''); // Remove HTTP method prefix\n\t\t\thost = event.headers?.host;\n\t\t\tscheme = 'https';\n\t\t\tuserAgent = http.userAgent;\n\t\t\tclientIp = http.sourceIp;\n\t\t\trequestId = event.requestContext.requestId;\n\t\t} else if (isV1) {\n\t\t\t// API Gateway v1 (REST API)\n\t\t\tmethod = event.httpMethod;\n\t\t\tpath = event.path;\n\t\t\troute = event.resource;\n\t\t\thost = event.headers?.Host || event.headers?.host;\n\t\t\tscheme = 'https';\n\t\t\tuserAgent =\n\t\t\t\tevent.headers?.['User-Agent'] || event.headers?.['user-agent'];\n\t\t\tclientIp = event.requestContext?.identity?.sourceIp;\n\t\t\trequestId = event.requestContext?.requestId;\n\t\t} else if ('httpMethod' in event) {\n\t\t\t// ALB event\n\t\t\tmethod = event.httpMethod;\n\t\t\tpath = event.path;\n\t\t\thost = event.headers?.host;\n\t\t\tuserAgent =\n\t\t\t\tevent.headers?.['user-agent'] || event.headers?.['User-Agent'];\n\t\t\tclientIp = event.headers?.['x-forwarded-for']?.split(',')[0]?.trim();\n\t\t}\n\n\t\tconst attrs: HttpSpanAttributes = {\n\t\t\tmethod,\n\t\t\tpath,\n\t\t\troute,\n\t\t\thost,\n\t\t\tscheme,\n\t\t\tuserAgent,\n\t\t\tclientIp,\n\t\t\trequestId: requestId || lambdaContext.awsRequestId,\n\t\t};\n\n\t\t// Add endpoint metadata if extractors provided\n\t\tif (this.options.getEndpointName || this.options.getOperationId) {\n\t\t\tattrs.endpoint = {\n\t\t\t\tname: this.options.getEndpointName?.(event),\n\t\t\t\toperationId: this.options.getOperationId?.(event),\n\t\t\t};\n\t\t}\n\n\t\t// Add user metadata if extractor provided\n\t\tif (this.options.getUserId) {\n\t\t\tconst userId = this.options.getUserId(event);\n\t\t\tif (userId) {\n\t\t\t\tattrs.user = { userId };\n\t\t\t}\n\t\t}\n\n\t\treturn attrs;\n\t}\n}\n\n/**\n * Normalize headers to lowercase keys\n */\nfunction normalizeHeaders(\n\theaders: Record<string, string | string[] | undefined>,\n): Record<string, string | string[] | undefined> {\n\tconst normalized: Record<string, string | string[] | undefined> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tnormalized[key.toLowerCase()] = value;\n\t}\n\treturn normalized;\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"otel-DuiBeBPU.cjs","names":["options: OTelTelemetryOptions","request: TelemetryRequest","ctx: TelemetryContext","response: TelemetryResponse","error: Error","event: any","lambdaContext: any","route: string | undefined","host: string | undefined","scheme: 'http' | 'https' | undefined","userAgent: string | undefined","clientIp: string | undefined","requestId: string | undefined","attrs: HttpSpanAttributes","headers: Record<string, string | string[] | undefined>","normalized: Record<string, string | string[] | undefined>"],"sources":["../src/instrumentation/otel.ts"],"sourcesContent":["/**\n * OpenTelemetry implementation of the Telemetry interface\n *\n * This provides automatic span creation for Lambda handlers using\n * OpenTelemetry. Works with any OTel-compatible backend (Jaeger, Zipkin,\n * Datadog, Honeycomb, etc.)\n */\nimport { type Span, trace } from '@opentelemetry/api';\nimport type { Context as LambdaContext } from 'aws-lambda';\n\n/**\n * Context object returned by onRequestStart, passed to subsequent hooks.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface TelemetryContext {\n\t[key: string]: unknown;\n}\n\n/**\n * Request information passed to telemetry hooks.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface TelemetryRequest {\n\tevent: any;\n\tcontext: LambdaContext;\n}\n\n/**\n * Response information passed to onRequestEnd.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface TelemetryResponse {\n\tstatusCode: number;\n\tbody?: string;\n\theaders?: Record<string, string>;\n}\n\n/**\n * Telemetry interface for instrumenting endpoint requests.\n * This matches the Telemetry interface from @geekmidas/constructs.\n */\nexport interface Telemetry {\n\tonRequestStart(request: TelemetryRequest): TelemetryContext;\n\tonRequestEnd(ctx: TelemetryContext, response: TelemetryResponse): void;\n\tonRequestError(ctx: TelemetryContext, error: Error): void;\n}\n\nimport {\n\tcreateHttpServerSpan,\n\tendHttpSpan,\n\textractTraceContext,\n\ttype HttpSpanAttributes,\n} from './http';\n\n/**\n * Options for OTelTelemetry\n */\nexport interface OTelTelemetryOptions {\n\t/**\n\t * Whether to record request body as span attribute\n\t * @default false\n\t */\n\trecordBody?: boolean;\n\n\t/**\n\t * Whether to record response body as span attribute\n\t * @default false\n\t */\n\trecordResponseBody?: boolean;\n\n\t/**\n\t * Custom function to extract user ID from event\n\t */\n\tgetUserId?: (event: any) => string | undefined;\n\n\t/**\n\t * Custom function to extract endpoint name from event\n\t */\n\tgetEndpointName?: (event: any) => string | undefined;\n\n\t/**\n\t * Custom function to extract operation ID from event\n\t */\n\tgetOperationId?: (event: any) => string | undefined;\n}\n\n/**\n * Telemetry context with span reference\n */\ninterface OTelContext extends TelemetryContext {\n\tspan: Span;\n\totelContext: any;\n}\n\n/**\n * OpenTelemetry implementation of the Telemetry interface\n *\n * @example\n * ```typescript\n * import { OTelTelemetry } from '@geekmidas/telescope/instrumentation';\n *\n * const adaptor = new AmazonApiGatewayV2Endpoint(envParser, endpoint, {\n * telemetry: new OTelTelemetry(),\n * });\n * ```\n *\n * @example With options\n * ```typescript\n * const adaptor = new AmazonApiGatewayV2Endpoint(envParser, endpoint, {\n * telemetry: new OTelTelemetry({\n * recordBody: true,\n * getUserId: (event) => event.requestContext?.authorizer?.userId,\n * }),\n * });\n * ```\n */\nexport class OTelTelemetry implements Telemetry {\n\tconstructor(private options: OTelTelemetryOptions = {}) {}\n\n\tonRequestStart(request: TelemetryRequest): TelemetryContext {\n\t\tconst { event, context: lambdaContext } = request;\n\n\t\t// Extract trace context from headers\n\t\tconst headers = normalizeHeaders(event.headers || {});\n\t\tconst parentContext = extractTraceContext(headers);\n\n\t\t// Build span attributes from event\n\t\tconst attrs = this.buildSpanAttributes(event, lambdaContext);\n\n\t\t// Create the span\n\t\tconst span = createHttpServerSpan(attrs, parentContext);\n\t\tconst otelContext = trace.setSpan(parentContext, span);\n\n\t\t// Add Lambda-specific attributes\n\t\tspan.setAttribute('faas.invocation_id', lambdaContext.awsRequestId);\n\t\tspan.setAttribute('faas.name', lambdaContext.functionName);\n\n\t\t// Record body if requested\n\t\tif (this.options.recordBody && event.body) {\n\t\t\tconst bodyStr =\n\t\t\t\ttypeof event.body === 'string'\n\t\t\t\t\t? event.body\n\t\t\t\t\t: JSON.stringify(event.body);\n\t\t\tspan.setAttribute(\n\t\t\t\t'http.request.body',\n\t\t\t\tbodyStr.length > 4096 ? `${bodyStr.slice(0, 4096)}...` : bodyStr,\n\t\t\t);\n\t\t}\n\n\t\treturn { span, otelContext } as OTelContext;\n\t}\n\n\tonRequestEnd(ctx: TelemetryContext, response: TelemetryResponse): void {\n\t\tconst { span } = ctx as OTelContext;\n\n\t\tif (this.options.recordResponseBody && response.body) {\n\t\t\tspan.setAttribute(\n\t\t\t\t'http.response.body',\n\t\t\t\tresponse.body.length > 4096\n\t\t\t\t\t? `${response.body.slice(0, 4096)}...`\n\t\t\t\t\t: response.body,\n\t\t\t);\n\t\t}\n\n\t\tendHttpSpan(span, { statusCode: response.statusCode });\n\t}\n\n\tonRequestError(ctx: TelemetryContext, error: Error): void {\n\t\tconst { span } = ctx as OTelContext;\n\t\tendHttpSpan(span, { statusCode: 500 }, error);\n\t}\n\n\t/**\n\t * Build span attributes from Lambda event\n\t */\n\tprivate buildSpanAttributes(\n\t\tevent: any,\n\t\tlambdaContext: any,\n\t): HttpSpanAttributes {\n\t\t// Detect event type (API Gateway v1, v2, ALB, or direct invoke)\n\t\tconst isV2 =\n\t\t\t'requestContext' in event && 'http' in (event.requestContext || {});\n\t\tconst isV1 =\n\t\t\t'requestContext' in event &&\n\t\t\t'httpMethod' in event &&\n\t\t\t!('http' in (event.requestContext || {}));\n\n\t\tlet method = 'INVOKE';\n\t\tlet path = '/';\n\t\tlet route: string | undefined;\n\t\tlet host: string | undefined;\n\t\tlet scheme: 'http' | 'https' | undefined;\n\t\tlet userAgent: string | undefined;\n\t\tlet clientIp: string | undefined;\n\t\tlet requestId: string | undefined;\n\n\t\tif (isV2) {\n\t\t\t// API Gateway v2 (HTTP API)\n\t\t\tconst http = event.requestContext.http;\n\t\t\tmethod = http.method;\n\t\t\tpath = event.rawPath || http.path;\n\t\t\troute = event.routeKey?.replace(/^[A-Z]+\\s+/, ''); // Remove HTTP method prefix\n\t\t\thost = event.headers?.host;\n\t\t\tscheme = 'https';\n\t\t\tuserAgent = http.userAgent;\n\t\t\tclientIp = http.sourceIp;\n\t\t\trequestId = event.requestContext.requestId;\n\t\t} else if (isV1) {\n\t\t\t// API Gateway v1 (REST API)\n\t\t\tmethod = event.httpMethod;\n\t\t\tpath = event.path;\n\t\t\troute = event.resource;\n\t\t\thost = event.headers?.Host || event.headers?.host;\n\t\t\tscheme = 'https';\n\t\t\tuserAgent =\n\t\t\t\tevent.headers?.['User-Agent'] || event.headers?.['user-agent'];\n\t\t\tclientIp = event.requestContext?.identity?.sourceIp;\n\t\t\trequestId = event.requestContext?.requestId;\n\t\t} else if ('httpMethod' in event) {\n\t\t\t// ALB event\n\t\t\tmethod = event.httpMethod;\n\t\t\tpath = event.path;\n\t\t\thost = event.headers?.host;\n\t\t\tuserAgent =\n\t\t\t\tevent.headers?.['user-agent'] || event.headers?.['User-Agent'];\n\t\t\tclientIp = event.headers?.['x-forwarded-for']?.split(',')[0]?.trim();\n\t\t}\n\n\t\tconst attrs: HttpSpanAttributes = {\n\t\t\tmethod,\n\t\t\tpath,\n\t\t\troute,\n\t\t\thost,\n\t\t\tscheme,\n\t\t\tuserAgent,\n\t\t\tclientIp,\n\t\t\trequestId: requestId || lambdaContext.awsRequestId,\n\t\t};\n\n\t\t// Add endpoint metadata if extractors provided\n\t\tif (this.options.getEndpointName || this.options.getOperationId) {\n\t\t\tattrs.endpoint = {\n\t\t\t\tname: this.options.getEndpointName?.(event),\n\t\t\t\toperationId: this.options.getOperationId?.(event),\n\t\t\t};\n\t\t}\n\n\t\t// Add user metadata if extractor provided\n\t\tif (this.options.getUserId) {\n\t\t\tconst userId = this.options.getUserId(event);\n\t\t\tif (userId) {\n\t\t\t\tattrs.user = { userId };\n\t\t\t}\n\t\t}\n\n\t\treturn attrs;\n\t}\n}\n\n/**\n * Normalize headers to lowercase keys\n */\nfunction normalizeHeaders(\n\theaders: Record<string, string | string[] | undefined>,\n): Record<string, string | string[] | undefined> {\n\tconst normalized: Record<string, string | string[] | undefined> = {};\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tnormalized[key.toLowerCase()] = value;\n\t}\n\treturn normalized;\n}\n"],"mappings":"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"}
|
package/src/Telescope.ts
DELETED
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
import { nanoid } from 'nanoid';
|
|
2
|
-
import {
|
|
3
|
-
MetricsAggregator,
|
|
4
|
-
type MetricsAggregatorOptions,
|
|
5
|
-
} from './metrics/MetricsAggregator';
|
|
6
|
-
import { createRedactor, type Redactor } from './redact';
|
|
7
|
-
import type {
|
|
8
|
-
EndpointDetails,
|
|
9
|
-
EndpointMetrics,
|
|
10
|
-
ExceptionEntry,
|
|
11
|
-
LogEntry,
|
|
12
|
-
MetricsQueryOptions,
|
|
13
|
-
MetricsSnapshot,
|
|
14
|
-
NormalizedTelescopeOptions,
|
|
15
|
-
QueryOptions,
|
|
16
|
-
RequestEntry,
|
|
17
|
-
RequestMetrics,
|
|
18
|
-
StackFrame,
|
|
19
|
-
StatusDistribution,
|
|
20
|
-
TelescopeEvent,
|
|
21
|
-
TelescopeOptions,
|
|
22
|
-
TelescopeStorage,
|
|
23
|
-
} from './types';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Framework-agnostic Telescope class for debugging and monitoring applications.
|
|
27
|
-
* Use framework-specific adapters (e.g., @geekmidas/telescope/hono) for integration.
|
|
28
|
-
*/
|
|
29
|
-
export class Telescope {
|
|
30
|
-
private storage: TelescopeStorage;
|
|
31
|
-
private options: NormalizedTelescopeOptions;
|
|
32
|
-
private wsClients = new Set<WebSocket>();
|
|
33
|
-
private pruneInterval?: ReturnType<typeof setInterval>;
|
|
34
|
-
private metricsBroadcastInterval?: ReturnType<typeof setInterval>;
|
|
35
|
-
private redactor?: Redactor;
|
|
36
|
-
private metricsAggregator: MetricsAggregator;
|
|
37
|
-
|
|
38
|
-
constructor(
|
|
39
|
-
options: TelescopeOptions & { metrics?: MetricsAggregatorOptions },
|
|
40
|
-
) {
|
|
41
|
-
this.storage = options.storage;
|
|
42
|
-
this.options = this.normalizeOptions(options);
|
|
43
|
-
this.redactor = createRedactor(options.redact);
|
|
44
|
-
this.metricsAggregator = new MetricsAggregator(options.metrics);
|
|
45
|
-
|
|
46
|
-
// Set up auto-pruning if configured
|
|
47
|
-
if (this.options.pruneAfterHours) {
|
|
48
|
-
const intervalMs = 60 * 60 * 1000; // 1 hour
|
|
49
|
-
this.pruneInterval = setInterval(() => {
|
|
50
|
-
this.autoPrune().catch(console.error);
|
|
51
|
-
}, intervalMs);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ============================================
|
|
56
|
-
// Public API - Recording
|
|
57
|
-
// ============================================
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Record a request entry
|
|
61
|
-
*/
|
|
62
|
-
async recordRequest(
|
|
63
|
-
entry: Omit<RequestEntry, 'id' | 'timestamp'>,
|
|
64
|
-
): Promise<string> {
|
|
65
|
-
if (!this.options.enabled) return '';
|
|
66
|
-
|
|
67
|
-
// Use injected request ID from context if available, otherwise generate one
|
|
68
|
-
const id = this.options.getRequestId?.() ?? nanoid();
|
|
69
|
-
let fullEntry: RequestEntry = {
|
|
70
|
-
...entry,
|
|
71
|
-
id,
|
|
72
|
-
timestamp: new Date(),
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Apply redaction if configured
|
|
76
|
-
if (this.redactor) {
|
|
77
|
-
fullEntry = this.redactor(fullEntry);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Record metrics (before storage for real-time aggregation)
|
|
81
|
-
this.metricsAggregator.record(fullEntry);
|
|
82
|
-
|
|
83
|
-
await this.storage.saveRequest(fullEntry);
|
|
84
|
-
this.broadcast({
|
|
85
|
-
type: 'request',
|
|
86
|
-
payload: fullEntry,
|
|
87
|
-
timestamp: Date.now(),
|
|
88
|
-
});
|
|
89
|
-
return id;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Log entry input for batch operations
|
|
94
|
-
*/
|
|
95
|
-
private async saveLogEntries(entries: LogEntry[]): Promise<void> {
|
|
96
|
-
if (this.storage.saveLogs) {
|
|
97
|
-
await this.storage.saveLogs(entries);
|
|
98
|
-
} else {
|
|
99
|
-
await Promise.all(entries.map((entry) => this.storage.saveLog(entry)));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
for (const entry of entries) {
|
|
103
|
-
this.broadcast({ type: 'log', payload: entry, timestamp: Date.now() });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Record log entries in batch.
|
|
109
|
-
* More efficient than individual calls for database storage.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* await telescope.log([
|
|
113
|
-
* { level: 'info', message: 'Request started' },
|
|
114
|
-
* { level: 'debug', message: 'Processing...', context: { step: 1 } },
|
|
115
|
-
* ]);
|
|
116
|
-
*/
|
|
117
|
-
async log(
|
|
118
|
-
entries: Array<{
|
|
119
|
-
level: 'debug' | 'info' | 'warn' | 'error';
|
|
120
|
-
message: string;
|
|
121
|
-
context?: Record<string, unknown>;
|
|
122
|
-
requestId?: string;
|
|
123
|
-
}>,
|
|
124
|
-
): Promise<void> {
|
|
125
|
-
if (!this.options.enabled || entries.length === 0) return;
|
|
126
|
-
|
|
127
|
-
const timestamp = new Date();
|
|
128
|
-
const logEntries: LogEntry[] = entries.map((e) => {
|
|
129
|
-
const entry: LogEntry = {
|
|
130
|
-
id: nanoid(),
|
|
131
|
-
level: e.level,
|
|
132
|
-
message: e.message,
|
|
133
|
-
context: e.context,
|
|
134
|
-
requestId: e.requestId,
|
|
135
|
-
timestamp,
|
|
136
|
-
};
|
|
137
|
-
return this.redactor ? this.redactor(entry) : entry;
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
await this.saveLogEntries(logEntries);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Log a debug message
|
|
145
|
-
*/
|
|
146
|
-
async debug(
|
|
147
|
-
message: string,
|
|
148
|
-
context?: Record<string, unknown>,
|
|
149
|
-
requestId?: string,
|
|
150
|
-
): Promise<void> {
|
|
151
|
-
return this.logSingle('debug', message, context, requestId);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Log an info message
|
|
156
|
-
*/
|
|
157
|
-
async info(
|
|
158
|
-
message: string,
|
|
159
|
-
context?: Record<string, unknown>,
|
|
160
|
-
requestId?: string,
|
|
161
|
-
): Promise<void> {
|
|
162
|
-
return this.logSingle('info', message, context, requestId);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Log a warning message
|
|
167
|
-
*/
|
|
168
|
-
async warn(
|
|
169
|
-
message: string,
|
|
170
|
-
context?: Record<string, unknown>,
|
|
171
|
-
requestId?: string,
|
|
172
|
-
): Promise<void> {
|
|
173
|
-
return this.logSingle('warn', message, context, requestId);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Log an error message
|
|
178
|
-
*/
|
|
179
|
-
async error(
|
|
180
|
-
message: string,
|
|
181
|
-
context?: Record<string, unknown>,
|
|
182
|
-
requestId?: string,
|
|
183
|
-
): Promise<void> {
|
|
184
|
-
return this.logSingle('error', message, context, requestId);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private async logSingle(
|
|
188
|
-
level: LogEntry['level'],
|
|
189
|
-
message: string,
|
|
190
|
-
context?: Record<string, unknown>,
|
|
191
|
-
requestId?: string,
|
|
192
|
-
): Promise<void> {
|
|
193
|
-
if (!this.options.enabled) return;
|
|
194
|
-
|
|
195
|
-
let entry: LogEntry = {
|
|
196
|
-
id: nanoid(),
|
|
197
|
-
level,
|
|
198
|
-
message,
|
|
199
|
-
context,
|
|
200
|
-
requestId,
|
|
201
|
-
timestamp: new Date(),
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
if (this.redactor) {
|
|
205
|
-
entry = this.redactor(entry);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
await this.storage.saveLog(entry);
|
|
209
|
-
this.broadcast({ type: 'log', payload: entry, timestamp: Date.now() });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Record an exception
|
|
214
|
-
*/
|
|
215
|
-
async exception(error: Error, requestId?: string): Promise<void> {
|
|
216
|
-
if (!this.options.enabled) return;
|
|
217
|
-
|
|
218
|
-
const stack = this.parseStack(error.stack || '');
|
|
219
|
-
|
|
220
|
-
const entry: ExceptionEntry = {
|
|
221
|
-
id: nanoid(),
|
|
222
|
-
name: error.name,
|
|
223
|
-
message: error.message,
|
|
224
|
-
stack,
|
|
225
|
-
requestId,
|
|
226
|
-
timestamp: new Date(),
|
|
227
|
-
handled: false,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
await this.storage.saveException(entry);
|
|
231
|
-
this.broadcast({
|
|
232
|
-
type: 'exception',
|
|
233
|
-
payload: entry,
|
|
234
|
-
timestamp: Date.now(),
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ============================================
|
|
239
|
-
// Public API - Data Access
|
|
240
|
-
// ============================================
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Get requests from storage
|
|
244
|
-
*/
|
|
245
|
-
async getRequests(options?: QueryOptions): Promise<RequestEntry[]> {
|
|
246
|
-
return this.storage.getRequests(options);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Get a single request by ID
|
|
251
|
-
*/
|
|
252
|
-
async getRequest(id: string): Promise<RequestEntry | null> {
|
|
253
|
-
return this.storage.getRequest(id);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Get exceptions from storage
|
|
258
|
-
*/
|
|
259
|
-
async getExceptions(options?: QueryOptions): Promise<ExceptionEntry[]> {
|
|
260
|
-
return this.storage.getExceptions(options);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Get a single exception by ID
|
|
265
|
-
*/
|
|
266
|
-
async getException(id: string): Promise<ExceptionEntry | null> {
|
|
267
|
-
return this.storage.getException(id);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get logs from storage
|
|
272
|
-
*/
|
|
273
|
-
async getLogs(options?: QueryOptions): Promise<LogEntry[]> {
|
|
274
|
-
return this.storage.getLogs(options);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Get storage statistics
|
|
279
|
-
*/
|
|
280
|
-
async getStats() {
|
|
281
|
-
return this.storage.getStats();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ============================================
|
|
285
|
-
// Public API - Metrics
|
|
286
|
-
// ============================================
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Get aggregated request metrics
|
|
290
|
-
*/
|
|
291
|
-
getMetrics(options?: MetricsQueryOptions): RequestMetrics {
|
|
292
|
-
return this.metricsAggregator.getMetrics(options);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Get metrics for individual endpoints
|
|
297
|
-
*/
|
|
298
|
-
getEndpointMetrics(options?: MetricsQueryOptions): EndpointMetrics[] {
|
|
299
|
-
return this.metricsAggregator.getEndpointMetrics(options);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Get detailed metrics for a specific endpoint
|
|
304
|
-
*/
|
|
305
|
-
getEndpointDetails(
|
|
306
|
-
method: string,
|
|
307
|
-
path: string,
|
|
308
|
-
options?: MetricsQueryOptions,
|
|
309
|
-
): EndpointDetails | null {
|
|
310
|
-
return this.metricsAggregator.getEndpointDetails(method, path, options);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Get status code distribution
|
|
315
|
-
*/
|
|
316
|
-
getStatusDistribution(options?: MetricsQueryOptions): StatusDistribution {
|
|
317
|
-
return this.metricsAggregator.getStatusDistribution(options);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Reset metrics data
|
|
322
|
-
*/
|
|
323
|
-
resetMetrics(): void {
|
|
324
|
-
this.metricsAggregator.reset();
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Get a real-time metrics snapshot for broadcasting
|
|
329
|
-
*/
|
|
330
|
-
getMetricsSnapshot(): MetricsSnapshot {
|
|
331
|
-
const metrics = this.metricsAggregator.getMetrics();
|
|
332
|
-
const statusDist = this.metricsAggregator.getStatusDistribution();
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
timestamp: Date.now(),
|
|
336
|
-
totalRequests: metrics.totalRequests,
|
|
337
|
-
requestsPerSecond: metrics.requestsPerSecond,
|
|
338
|
-
avgDuration: metrics.avgDuration,
|
|
339
|
-
errorRate: metrics.errorRate,
|
|
340
|
-
p50: metrics.p50Duration,
|
|
341
|
-
p95: metrics.p95Duration,
|
|
342
|
-
p99: metrics.p99Duration,
|
|
343
|
-
statusDistribution: statusDist,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Start broadcasting metrics to WebSocket clients at a given interval.
|
|
349
|
-
* Call this after setting up WebSocket to enable real-time metrics updates.
|
|
350
|
-
*
|
|
351
|
-
* @param intervalMs - Broadcast interval in milliseconds (default: 5000)
|
|
352
|
-
*/
|
|
353
|
-
startMetricsBroadcast(intervalMs = 5000): void {
|
|
354
|
-
if (this.metricsBroadcastInterval) {
|
|
355
|
-
return; // Already running
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
this.metricsBroadcastInterval = setInterval(() => {
|
|
359
|
-
if (this.wsClients.size > 0) {
|
|
360
|
-
const snapshot = this.getMetricsSnapshot();
|
|
361
|
-
this.broadcast({
|
|
362
|
-
type: 'metrics',
|
|
363
|
-
payload: snapshot,
|
|
364
|
-
timestamp: Date.now(),
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
}, intervalMs);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Stop broadcasting metrics to WebSocket clients
|
|
372
|
-
*/
|
|
373
|
-
stopMetricsBroadcast(): void {
|
|
374
|
-
if (this.metricsBroadcastInterval) {
|
|
375
|
-
clearInterval(this.metricsBroadcastInterval);
|
|
376
|
-
this.metricsBroadcastInterval = undefined;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ============================================
|
|
381
|
-
// Public API - WebSocket
|
|
382
|
-
// ============================================
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Add a WebSocket client for real-time updates
|
|
386
|
-
*/
|
|
387
|
-
addWsClient(ws: WebSocket): void {
|
|
388
|
-
this.wsClients.add(ws);
|
|
389
|
-
this.broadcast({
|
|
390
|
-
type: 'connected',
|
|
391
|
-
payload: { clientCount: this.wsClients.size },
|
|
392
|
-
timestamp: Date.now(),
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Remove a WebSocket client
|
|
398
|
-
*/
|
|
399
|
-
removeWsClient(ws: WebSocket): void {
|
|
400
|
-
this.wsClients.delete(ws);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Broadcast an event to all connected WebSocket clients
|
|
405
|
-
*/
|
|
406
|
-
broadcast(event: TelescopeEvent): void {
|
|
407
|
-
const data = JSON.stringify(event);
|
|
408
|
-
for (const client of this.wsClients) {
|
|
409
|
-
try {
|
|
410
|
-
client.send(data);
|
|
411
|
-
} catch {
|
|
412
|
-
this.wsClients.delete(client);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ============================================
|
|
418
|
-
// Public API - Lifecycle
|
|
419
|
-
// ============================================
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Manually prune old entries
|
|
423
|
-
*/
|
|
424
|
-
async prune(olderThan: Date): Promise<number> {
|
|
425
|
-
return this.storage.prune(olderThan);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Clean up resources
|
|
430
|
-
*/
|
|
431
|
-
destroy(): void {
|
|
432
|
-
if (this.pruneInterval) {
|
|
433
|
-
clearInterval(this.pruneInterval);
|
|
434
|
-
}
|
|
435
|
-
this.stopMetricsBroadcast();
|
|
436
|
-
this.wsClients.clear();
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// ============================================
|
|
440
|
-
// Public API - Configuration
|
|
441
|
-
// ============================================
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Get the telescope path
|
|
445
|
-
*/
|
|
446
|
-
get path(): string {
|
|
447
|
-
return this.options.path;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Check if telescope is enabled
|
|
452
|
-
*/
|
|
453
|
-
get enabled(): boolean {
|
|
454
|
-
return this.options.enabled;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Check if body recording is enabled
|
|
459
|
-
*/
|
|
460
|
-
get recordBody(): boolean {
|
|
461
|
-
return this.options.recordBody;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Get max body size
|
|
466
|
-
*/
|
|
467
|
-
get maxBodySize(): number {
|
|
468
|
-
return this.options.maxBodySize;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Check if a path should be ignored
|
|
473
|
-
*/
|
|
474
|
-
shouldIgnore(path: string): boolean {
|
|
475
|
-
// Always ignore telescope's own routes
|
|
476
|
-
if (path.startsWith(this.options.path)) {
|
|
477
|
-
return true;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return this.options.ignorePatterns.some((pattern) => {
|
|
481
|
-
if (pattern.includes('*')) {
|
|
482
|
-
const regex = new RegExp(
|
|
483
|
-
`^${pattern.replace(/\*/g, '.*').replace(/\?/g, '.')}$`,
|
|
484
|
-
);
|
|
485
|
-
return regex.test(path);
|
|
486
|
-
}
|
|
487
|
-
return path.startsWith(pattern);
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// ============================================
|
|
492
|
-
// Private Methods
|
|
493
|
-
// ============================================
|
|
494
|
-
|
|
495
|
-
private normalizeOptions(
|
|
496
|
-
options: TelescopeOptions,
|
|
497
|
-
): NormalizedTelescopeOptions {
|
|
498
|
-
return {
|
|
499
|
-
storage: options.storage,
|
|
500
|
-
enabled: options.enabled ?? true,
|
|
501
|
-
path: options.path ?? '/__telescope',
|
|
502
|
-
recordBody: options.recordBody ?? true,
|
|
503
|
-
maxBodySize: options.maxBodySize ?? 64 * 1024, // 64KB
|
|
504
|
-
ignorePatterns: options.ignorePatterns ?? [],
|
|
505
|
-
pruneAfterHours: options.pruneAfterHours,
|
|
506
|
-
getRequestId: options.getRequestId,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private parseStack(stack: string): StackFrame[] {
|
|
511
|
-
const lines = stack.split('\n').slice(1);
|
|
512
|
-
const frames: StackFrame[] = [];
|
|
513
|
-
|
|
514
|
-
for (const line of lines) {
|
|
515
|
-
// Match: " at functionName (file:line:column)"
|
|
516
|
-
// or: " at file:line:column"
|
|
517
|
-
const match =
|
|
518
|
-
line.match(/at\s+(.+?)\s+\((.+):(\d+):(\d+)\)/) ||
|
|
519
|
-
line.match(/at\s+(.+):(\d+):(\d+)/);
|
|
520
|
-
|
|
521
|
-
if (match) {
|
|
522
|
-
if (
|
|
523
|
-
match.length === 5 &&
|
|
524
|
-
match[1] &&
|
|
525
|
-
match[2] &&
|
|
526
|
-
match[3] &&
|
|
527
|
-
match[4]
|
|
528
|
-
) {
|
|
529
|
-
// Has function name
|
|
530
|
-
frames.push({
|
|
531
|
-
function: match[1],
|
|
532
|
-
file: match[2],
|
|
533
|
-
line: parseInt(match[3], 10),
|
|
534
|
-
column: parseInt(match[4], 10),
|
|
535
|
-
isApp: !match[2].includes('node_modules'),
|
|
536
|
-
});
|
|
537
|
-
} else if (match.length === 4 && match[1] && match[2] && match[3]) {
|
|
538
|
-
// No function name
|
|
539
|
-
frames.push({
|
|
540
|
-
function: '<anonymous>',
|
|
541
|
-
file: match[1],
|
|
542
|
-
line: parseInt(match[2], 10),
|
|
543
|
-
column: parseInt(match[3], 10),
|
|
544
|
-
isApp: !match[1].includes('node_modules'),
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return frames;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
private async autoPrune(): Promise<void> {
|
|
554
|
-
if (!this.options.pruneAfterHours) return;
|
|
555
|
-
|
|
556
|
-
const olderThan = new Date(
|
|
557
|
-
Date.now() - this.options.pruneAfterHours * 60 * 60 * 1000,
|
|
558
|
-
);
|
|
559
|
-
await this.storage.prune(olderThan);
|
|
560
|
-
}
|
|
561
|
-
}
|