@safaricom-mxl/log 0.0.3
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 +1040 -0
- package/dist/_http-DmaJ426Z.mjs +76 -0
- package/dist/_http-DmaJ426Z.mjs.map +1 -0
- package/dist/_severity-D_IU9-90.mjs +17 -0
- package/dist/_severity-D_IU9-90.mjs.map +1 -0
- package/dist/adapters/axiom.d.mts +64 -0
- package/dist/adapters/axiom.d.mts.map +1 -0
- package/dist/adapters/axiom.mjs +100 -0
- package/dist/adapters/axiom.mjs.map +1 -0
- package/dist/adapters/better-stack.d.mts +63 -0
- package/dist/adapters/better-stack.d.mts.map +1 -0
- package/dist/adapters/better-stack.mjs +98 -0
- package/dist/adapters/better-stack.mjs.map +1 -0
- package/dist/adapters/otlp.d.mts +85 -0
- package/dist/adapters/otlp.d.mts.map +1 -0
- package/dist/adapters/otlp.mjs +196 -0
- package/dist/adapters/otlp.mjs.map +1 -0
- package/dist/adapters/posthog.d.mts +107 -0
- package/dist/adapters/posthog.d.mts.map +1 -0
- package/dist/adapters/posthog.mjs +166 -0
- package/dist/adapters/posthog.mjs.map +1 -0
- package/dist/adapters/sentry.d.mts +80 -0
- package/dist/adapters/sentry.d.mts.map +1 -0
- package/dist/adapters/sentry.mjs +221 -0
- package/dist/adapters/sentry.mjs.map +1 -0
- package/dist/browser.d.mts +63 -0
- package/dist/browser.d.mts.map +1 -0
- package/dist/browser.mjs +95 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/enrichers.d.mts +74 -0
- package/dist/enrichers.d.mts.map +1 -0
- package/dist/enrichers.mjs +172 -0
- package/dist/enrichers.mjs.map +1 -0
- package/dist/error.d.mts +65 -0
- package/dist/error.d.mts.map +1 -0
- package/dist/error.mjs +112 -0
- package/dist/error.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +6 -0
- package/dist/logger.d.mts +46 -0
- package/dist/logger.d.mts.map +1 -0
- package/dist/logger.mjs +287 -0
- package/dist/logger.mjs.map +1 -0
- package/dist/next/client.d.mts +55 -0
- package/dist/next/client.d.mts.map +1 -0
- package/dist/next/client.mjs +44 -0
- package/dist/next/client.mjs.map +1 -0
- package/dist/next/index.d.mts +169 -0
- package/dist/next/index.d.mts.map +1 -0
- package/dist/next/index.mjs +280 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/nitro/errorHandler.d.mts +15 -0
- package/dist/nitro/errorHandler.d.mts.map +1 -0
- package/dist/nitro/errorHandler.mjs +41 -0
- package/dist/nitro/errorHandler.mjs.map +1 -0
- package/dist/nitro/module.d.mts +11 -0
- package/dist/nitro/module.d.mts.map +1 -0
- package/dist/nitro/module.mjs +23 -0
- package/dist/nitro/module.mjs.map +1 -0
- package/dist/nitro/plugin.d.mts +7 -0
- package/dist/nitro/plugin.d.mts.map +1 -0
- package/dist/nitro/plugin.mjs +145 -0
- package/dist/nitro/plugin.mjs.map +1 -0
- package/dist/nitro/v3/errorHandler.d.mts +24 -0
- package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
- package/dist/nitro/v3/errorHandler.mjs +36 -0
- package/dist/nitro/v3/errorHandler.mjs.map +1 -0
- package/dist/nitro/v3/index.d.mts +5 -0
- package/dist/nitro/v3/index.mjs +5 -0
- package/dist/nitro/v3/middleware.d.mts +25 -0
- package/dist/nitro/v3/middleware.d.mts.map +1 -0
- package/dist/nitro/v3/middleware.mjs +45 -0
- package/dist/nitro/v3/middleware.mjs.map +1 -0
- package/dist/nitro/v3/module.d.mts +10 -0
- package/dist/nitro/v3/module.d.mts.map +1 -0
- package/dist/nitro/v3/module.mjs +22 -0
- package/dist/nitro/v3/module.mjs.map +1 -0
- package/dist/nitro/v3/plugin.d.mts +14 -0
- package/dist/nitro/v3/plugin.d.mts.map +1 -0
- package/dist/nitro/v3/plugin.mjs +162 -0
- package/dist/nitro/v3/plugin.mjs.map +1 -0
- package/dist/nitro/v3/useLogger.d.mts +24 -0
- package/dist/nitro/v3/useLogger.d.mts.map +1 -0
- package/dist/nitro/v3/useLogger.mjs +27 -0
- package/dist/nitro/v3/useLogger.mjs.map +1 -0
- package/dist/nitro-CrFBjY1Y.d.mts +42 -0
- package/dist/nitro-CrFBjY1Y.d.mts.map +1 -0
- package/dist/nitro-Dsv6dSzv.mjs +39 -0
- package/dist/nitro-Dsv6dSzv.mjs.map +1 -0
- package/dist/nuxt/module.d.mts +164 -0
- package/dist/nuxt/module.d.mts.map +1 -0
- package/dist/nuxt/module.mjs +84 -0
- package/dist/nuxt/module.mjs.map +1 -0
- package/dist/pipeline.d.mts +46 -0
- package/dist/pipeline.d.mts.map +1 -0
- package/dist/pipeline.mjs +122 -0
- package/dist/pipeline.mjs.map +1 -0
- package/dist/routes-BNbrnm14.mjs +39 -0
- package/dist/routes-BNbrnm14.mjs.map +1 -0
- package/dist/runtime/client/log.d.mts +15 -0
- package/dist/runtime/client/log.d.mts.map +1 -0
- package/dist/runtime/client/log.mjs +92 -0
- package/dist/runtime/client/log.mjs.map +1 -0
- package/dist/runtime/client/plugin.d.mts +5 -0
- package/dist/runtime/client/plugin.d.mts.map +1 -0
- package/dist/runtime/client/plugin.mjs +17 -0
- package/dist/runtime/client/plugin.mjs.map +1 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.d.mts +7 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.d.mts.map +1 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.mjs +123 -0
- package/dist/runtime/server/routes/_mxllog/ingest.post.mjs.map +1 -0
- package/dist/runtime/server/useLogger.d.mts +39 -0
- package/dist/runtime/server/useLogger.d.mts.map +1 -0
- package/dist/runtime/server/useLogger.mjs +43 -0
- package/dist/runtime/server/useLogger.mjs.map +1 -0
- package/dist/runtime/utils/parseError.d.mts +7 -0
- package/dist/runtime/utils/parseError.d.mts.map +1 -0
- package/dist/runtime/utils/parseError.mjs +29 -0
- package/dist/runtime/utils/parseError.mjs.map +1 -0
- package/dist/types.d.mts +496 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +1 -0
- package/dist/utils.d.mts +34 -0
- package/dist/utils.d.mts.map +1 -0
- package/dist/utils.mjs +78 -0
- package/dist/utils.mjs.map +1 -0
- package/dist/workers.d.mts +46 -0
- package/dist/workers.d.mts.map +1 -0
- package/dist/workers.mjs +81 -0
- package/dist/workers.mjs.map +1 -0
- package/package.json +195 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../../../src/nitro-v3/plugin.ts"],"sourcesContent":["import { definePlugin } from 'nitro'\nimport { useRuntimeConfig } from 'nitro/runtime-config'\nimport type { CaptureError } from 'nitro/types'\nimport type { HTTPEvent } from 'nitro/h3'\nimport { parseURL } from 'ufo'\nimport { createRequestLogger, initLogger, isEnabled } from '../logger'\nimport { shouldLog, getServiceForPath, extractErrorStatus } from '../nitro'\nimport type { MxllogConfig } from '../nitro'\nimport type { EnrichContext, RequestLogger, TailSamplingContext, WideEvent } from '../types'\nimport { filterSafeHeaders } from '../utils'\n\n// Nitro v3 doesn't fully export hook types yet\n// https://github.com/nitrojs/nitro/blob/8882bc9e1dbf2d342e73097f22a2156f70f50575/src/types/runtime/nitro.ts#L48-L53\ninterface NitroV3Hooks {\n close: () => void\n error: CaptureError\n request: (event: HTTPEvent) => void | Promise<void>\n response: (res: Response, event: HTTPEvent) => void | Promise<void>\n '@safaricom-mxl/log:emit:keep': (ctx: TailSamplingContext) => void | Promise<void>\n '@safaricom-mxl/log:enrich': (ctx: EnrichContext) => void | Promise<void>\n '@safaricom-mxl/log:drain': (ctx: { event: WideEvent; request?: { method?: string; path: string; requestId?: string }; headers?: Record<string, string> }) => void | Promise<void>\n}\n\ntype Hooks = {\n hook: <T extends keyof NitroV3Hooks>(name: T, listener: NitroV3Hooks[T]) => void\n callHook: <T extends keyof NitroV3Hooks>(name: T, ...args: Parameters<NitroV3Hooks[T]>) => Promise<void>\n}\n\nfunction getContext(event: HTTPEvent): Record<string, unknown> {\n if (!event.req.context) {\n event.req.context = {}\n }\n return event.req.context\n}\n\nfunction getSafeRequestHeaders(event: HTTPEvent): Record<string, string> {\n const headers: Record<string, string> = {}\n event.req.headers.forEach((value, key) => {\n headers[key] = value\n })\n return filterSafeHeaders(headers)\n}\n\nfunction getSafeResponseHeaders(res: Response): Record<string, string> | undefined {\n const headers: Record<string, string> = {}\n res.headers.forEach((value, key) => {\n headers[key] = value\n })\n if (Object.keys(headers).length === 0) return undefined\n return filterSafeHeaders(headers)\n}\n\nfunction buildHookContext(\n event: HTTPEvent,\n res?: Response,\n): Omit<EnrichContext, 'event'> {\n const { pathname } = parseURL(event.req.url)\n const responseHeaders = res ? getSafeResponseHeaders(res) : undefined\n return {\n request: { method: event.req.method, path: pathname },\n headers: getSafeRequestHeaders(event),\n response: {\n status: res?.status ?? 200,\n headers: responseHeaders,\n },\n }\n}\n\nasync function callDrainHook(\n hooks: Hooks,\n emittedEvent: WideEvent | null,\n event: HTTPEvent,\n hookContext: Omit<EnrichContext, 'event'>,\n): Promise<void> {\n if (!emittedEvent) return\n\n let drainPromise: Promise<unknown> | undefined\n try {\n const result = hooks.callHook('@safaricom-mxl/log:drain', {\n event: emittedEvent,\n request: hookContext.request,\n headers: hookContext.headers,\n })\n drainPromise = result?.catch?.((err: unknown) => {\n console.error('[mxllog] drain failed:', err)\n })\n } catch (err) {\n console.error('[mxllog] drain failed:', err)\n }\n\n if (!drainPromise) return\n\n // Use waitUntil if available (srvx native — Cloudflare Workers, Vercel Edge, etc.)\n // This keeps the runtime alive for background work without blocking the response\n if (typeof event.req.waitUntil === 'function') {\n event.req.waitUntil(drainPromise)\n } else {\n // Fallback: await drain to prevent lost logs in serverless environments\n // (e.g. Vercel Fluid Compute). On the normal path this runs from the\n // response hook (response already sent); on the error path it may run\n // before the error response is finalized.\n await drainPromise\n }\n}\n\nasync function callEnrichAndDrain(\n hooks: Hooks,\n emittedEvent: WideEvent | null,\n event: HTTPEvent,\n res?: Response,\n): Promise<void> {\n if (!emittedEvent) return\n\n const hookContext = buildHookContext(event, res)\n\n try {\n await hooks.callHook('@safaricom-mxl/log:enrich', { event: emittedEvent, ...hookContext })\n } catch (err) {\n console.error('[mxllog] enrich failed:', err)\n }\n\n await callDrainHook(hooks, emittedEvent, event, hookContext)\n}\n\n/**\n * Nitro v3 plugin entry point.\n *\n * Usage in Nitro v3:\n * ```ts\n * // plugins/mxllog.ts\n * export { default } from '@safaricom-mxl/log/nitro/v3'\n * ```\n */\nexport default definePlugin((nitroApp) => {\n // In production builds the plugin is bundled and useRuntimeConfig()\n // resolves the virtual module correctly. In dev mode the plugin is\n // loaded externally so useRuntimeConfig() returns a stub — fall back\n // to the env var bridge set by the module.\n const mxllogConfig = (useRuntimeConfig().mxllog ?? (process.env.__MXLLOG_CONFIG ? JSON.parse(process.env.__MXLLOG_CONFIG) : undefined)) as MxllogConfig | undefined\n\n initLogger({\n enabled: mxllogConfig?.enabled,\n env: mxllogConfig?.env,\n pretty: mxllogConfig?.pretty,\n sampling: mxllogConfig?.sampling,\n })\n\n if (!isEnabled()) return\n\n const hooks = nitroApp.hooks as unknown as Hooks\n\n hooks.hook('request', (event) => {\n const { pathname } = parseURL(event.req.url)\n\n // Skip logging for routes not matching include/exclude patterns\n if (!shouldLog(pathname, mxllogConfig?.include, mxllogConfig?.exclude)) {\n return\n }\n\n const ctx = getContext(event)\n\n // Store start time for duration calculation in tail sampling\n ctx._mxllogStartTime = Date.now()\n\n let requestIdOverride: string | undefined = undefined\n if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') {\n const cfRay = event.req.headers.get('cf-ray')\n if (cfRay) requestIdOverride = cfRay\n }\n\n const log = createRequestLogger({\n method: event.req.method,\n path: pathname,\n requestId: requestIdOverride || ctx.requestId as string | undefined || crypto.randomUUID(),\n })\n\n // Apply route-based service configuration if a matching route is found\n const routeService = getServiceForPath(pathname, mxllogConfig?.routes)\n if (routeService) {\n log.set({ service: routeService })\n }\n\n ctx.log = log\n })\n\n hooks.hook('response', async (res, event) => {\n const ctx = event.req.context\n // Skip if already emitted by error hook\n if (ctx?._mxllogEmitted) return\n\n const log = ctx?.log as RequestLogger | undefined\n if (!log || !ctx) return\n\n const { status } = res\n log.set({ status })\n\n const startTime = ctx._mxllogStartTime as number | undefined\n const durationMs = startTime ? Date.now() - startTime : undefined\n\n const { pathname } = parseURL(event.req.url)\n\n const tailCtx: TailSamplingContext = {\n status,\n duration: durationMs,\n path: pathname,\n method: event.req.method,\n context: log.getContext(),\n shouldKeep: false,\n }\n\n await hooks.callHook('@safaricom-mxl/log:emit:keep', tailCtx)\n\n const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep })\n await callEnrichAndDrain(hooks, emittedEvent, event, res)\n })\n\n hooks.hook('error', async (error, { event }) => {\n if (!event) return\n const e = event as HTTPEvent\n\n const ctx = e.req.context\n const log = ctx?.log as RequestLogger | undefined\n if (!log || !ctx) return\n\n // Check if error.cause is an MxllogError (thrown errors get wrapped in HTTPError by nitro)\n const actualError = (error.cause as Error)?.name === 'MxllogError'\n ? error.cause as Error\n : error as Error\n\n log.error(actualError)\n\n const errorStatus = extractErrorStatus(actualError)\n log.set({ status: errorStatus })\n\n const { pathname } = parseURL(e.req.url)\n const startTime = ctx._mxllogStartTime as number | undefined\n const durationMs = startTime ? Date.now() - startTime : undefined\n\n const tailCtx: TailSamplingContext = {\n status: errorStatus,\n duration: durationMs,\n path: pathname,\n method: e.req.method,\n context: log.getContext(),\n shouldKeep: false,\n }\n\n await hooks.callHook('@safaricom-mxl/log:emit:keep', tailCtx)\n\n ctx._mxllogEmitted = true\n\n const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep })\n await callEnrichAndDrain(hooks, emittedEvent, e)\n })\n})\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RequestLogger } from "../../types.mjs";
|
|
2
|
+
import { HTTPEvent } from "nitro/h3";
|
|
3
|
+
|
|
4
|
+
//#region src/nitro-v3/useLogger.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Returns the request logger attached to the given Nitro v3 HTTP event.
|
|
7
|
+
*
|
|
8
|
+
* @param event - The current HTTPEvent from Nitro v3.
|
|
9
|
+
* @param service - Optional service name to override the default service.
|
|
10
|
+
* @returns The request-scoped logger.
|
|
11
|
+
* @throws Error if the logger is not initialized on the event context.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { useLogger } from '@safaricom-mxl/log/nitro/v3'
|
|
15
|
+
*
|
|
16
|
+
* export default defineHandler((event) => {
|
|
17
|
+
* const log = useLogger(event)
|
|
18
|
+
* log.set({ foo: 'bar' })
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
declare function useLogger<T extends object = Record<string, unknown>>(event: HTTPEvent, service?: string): RequestLogger<T>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { useLogger };
|
|
24
|
+
//# sourceMappingURL=useLogger.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLogger.d.mts","names":[],"sources":["../../../src/nitro-v3/useLogger.ts"],"mappings":";;;;;;AAmBA;;;;;;;;;;;;;;iBAAgB,SAAA,oBAA6B,MAAA,kBAAA,CAAyB,KAAA,EAAO,SAAA,EAAW,OAAA,YAAmB,aAAA,CAAc,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/nitro-v3/useLogger.ts
|
|
2
|
+
/**
|
|
3
|
+
* Returns the request logger attached to the given Nitro v3 HTTP event.
|
|
4
|
+
*
|
|
5
|
+
* @param event - The current HTTPEvent from Nitro v3.
|
|
6
|
+
* @param service - Optional service name to override the default service.
|
|
7
|
+
* @returns The request-scoped logger.
|
|
8
|
+
* @throws Error if the logger is not initialized on the event context.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { useLogger } from '@safaricom-mxl/log/nitro/v3'
|
|
12
|
+
*
|
|
13
|
+
* export default defineHandler((event) => {
|
|
14
|
+
* const log = useLogger(event)
|
|
15
|
+
* log.set({ foo: 'bar' })
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
function useLogger(event, service) {
|
|
19
|
+
const log = event.req.context?.log;
|
|
20
|
+
if (!log) throw new Error("[mxllog] Logger not initialized. Make sure the mxllog Nitro module is registered in nitro.config.ts. Example: modules: [mxllog({ env: { service: 'my-app' } })]");
|
|
21
|
+
if (service) log.set({ service });
|
|
22
|
+
return log;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { useLogger };
|
|
27
|
+
//# sourceMappingURL=useLogger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLogger.mjs","names":[],"sources":["../../../src/nitro-v3/useLogger.ts"],"sourcesContent":["import type { HTTPEvent } from 'nitro/h3'\nimport type { RequestLogger } from '../types'\n\n/**\n * Returns the request logger attached to the given Nitro v3 HTTP event.\n *\n * @param event - The current HTTPEvent from Nitro v3.\n * @param service - Optional service name to override the default service.\n * @returns The request-scoped logger.\n * @throws Error if the logger is not initialized on the event context.\n *\n * @example\n * import { useLogger } from '@safaricom-mxl/log/nitro/v3'\n *\n * export default defineHandler((event) => {\n * const log = useLogger(event)\n * log.set({ foo: 'bar' })\n * })\n */\nexport function useLogger<T extends object = Record<string, unknown>>(event: HTTPEvent, service?: string): RequestLogger<T> {\n const ctx = event.req.context as Record<string, unknown> | undefined\n const log = ctx?.log as RequestLogger<T> | undefined\n\n if (!log) {\n throw new Error(\n '[mxllog] Logger not initialized. Make sure the mxllog Nitro module is registered in nitro.config.ts. '\n + 'Example: modules: [mxllog({ env: { service: \\'my-app\\' } })]',\n )\n }\n\n if (service) {\n const untyped = log as unknown as RequestLogger\n untyped.set({ service })\n }\n\n return log\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,SAAgB,UAAsD,OAAkB,SAAoC;CAE1H,MAAM,MADM,MAAM,IAAI,SACL;AAEjB,KAAI,CAAC,IACH,OAAM,IAAI,MACR,kKAED;AAGH,KAAI,QAEF,CADgB,IACR,IAAI,EAAE,SAAS,CAAC;AAG1B,QAAO"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EnvironmentContext, RouteConfig, SamplingConfig } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/nitro.d.ts
|
|
4
|
+
interface NitroModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Enable or disable all logging globally.
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Environment context overrides.
|
|
12
|
+
*/
|
|
13
|
+
env?: Partial<EnvironmentContext>;
|
|
14
|
+
/**
|
|
15
|
+
* Enable pretty printing.
|
|
16
|
+
* @default true in development, false in production
|
|
17
|
+
*/
|
|
18
|
+
pretty?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Route patterns to include in logging.
|
|
21
|
+
* Supports glob patterns like '/api/**'.
|
|
22
|
+
* If not set, all routes are logged.
|
|
23
|
+
*/
|
|
24
|
+
include?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Route patterns to exclude from logging.
|
|
27
|
+
* Supports glob patterns like '/_nitro/**'.
|
|
28
|
+
* Exclusions take precedence over inclusions.
|
|
29
|
+
*/
|
|
30
|
+
exclude?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Route-specific service configuration.
|
|
33
|
+
*/
|
|
34
|
+
routes?: Record<string, RouteConfig>;
|
|
35
|
+
/**
|
|
36
|
+
* Sampling configuration for filtering logs.
|
|
37
|
+
*/
|
|
38
|
+
sampling?: SamplingConfig;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { NitroModuleOptions as t };
|
|
42
|
+
//# sourceMappingURL=nitro-CrFBjY1Y.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nitro-CrFBjY1Y.d.mts","names":[],"sources":["../src/nitro.ts"],"mappings":";;;UAIiB,kBAAA;EAAA;;;;EAKf,OAAA;EA8BwB;;;EAzBxB,GAAA,GAAM,OAAA,CAAQ,kBAAA;EA8BW;;;;EAxBzB,MAAA;EAAA;;;;;EAOA,OAAA;EAiBA;;;;;EAVA,OAAA;;;;EAKA,MAAA,GAAS,MAAA,SAAe,WAAA;;;;EAKxB,QAAA,GAAW,cAAA;AAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//#region src/nitro.ts
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an MxllogError from an error or its cause chain.
|
|
4
|
+
* Both Nitro v2 (h3) and v3 wrap thrown errors — this unwraps them.
|
|
5
|
+
*/
|
|
6
|
+
function resolveMxllogError(error) {
|
|
7
|
+
if (error.name === "MxllogError") return error;
|
|
8
|
+
if (error.cause?.name === "MxllogError") return error.cause;
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract HTTP status from an error, checking both `status` and `statusCode`.
|
|
13
|
+
*/
|
|
14
|
+
function extractErrorStatus(error) {
|
|
15
|
+
return error.status ?? error.statusCode ?? 500;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a standard mxllog error JSON response body.
|
|
19
|
+
* Used by both v2 and v3 error handlers to ensure consistent shape.
|
|
20
|
+
*/
|
|
21
|
+
function serializeMxllogErrorResponse(error, url) {
|
|
22
|
+
const status = extractErrorStatus(error);
|
|
23
|
+
const { data } = error;
|
|
24
|
+
const statusMessage = error.statusMessage || error.message;
|
|
25
|
+
return {
|
|
26
|
+
url,
|
|
27
|
+
status,
|
|
28
|
+
statusCode: status,
|
|
29
|
+
statusText: statusMessage,
|
|
30
|
+
statusMessage,
|
|
31
|
+
message: error.message,
|
|
32
|
+
error: true,
|
|
33
|
+
...data !== void 0 && { data }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { resolveMxllogError as n, serializeMxllogErrorResponse as r, extractErrorStatus as t };
|
|
39
|
+
//# sourceMappingURL=nitro-Dsv6dSzv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nitro-Dsv6dSzv.mjs","names":[],"sources":["../src/nitro.ts"],"sourcesContent":["import type { EnvironmentContext, RouteConfig, SamplingConfig } from './types'\n\nexport { shouldLog, getServiceForPath } from './shared/routes'\n\nexport interface NitroModuleOptions {\n /**\n * Enable or disable all logging globally.\n * @default true\n */\n enabled?: boolean\n\n /**\n * Environment context overrides.\n */\n env?: Partial<EnvironmentContext>\n\n /**\n * Enable pretty printing.\n * @default true in development, false in production\n */\n pretty?: boolean\n\n /**\n * Route patterns to include in logging.\n * Supports glob patterns like '/api/**'.\n * If not set, all routes are logged.\n */\n include?: string[]\n\n /**\n * Route patterns to exclude from logging.\n * Supports glob patterns like '/_nitro/**'.\n * Exclusions take precedence over inclusions.\n */\n exclude?: string[]\n\n /**\n * Route-specific service configuration.\n */\n routes?: Record<string, RouteConfig>\n\n /**\n * Sampling configuration for filtering logs.\n */\n sampling?: SamplingConfig\n}\n\nexport interface MxllogConfig {\n enabled?: boolean\n env?: Record<string, unknown>\n pretty?: boolean\n include?: string[]\n exclude?: string[]\n routes?: Record<string, RouteConfig>\n sampling?: SamplingConfig\n}\n\n/**\n * Resolve an MxllogError from an error or its cause chain.\n * Both Nitro v2 (h3) and v3 wrap thrown errors — this unwraps them.\n */\nexport function resolveMxllogError(error: Error): Error | null {\n if (error.name === 'MxllogError') return error\n if ((error.cause as Error)?.name === 'MxllogError') return error.cause as Error\n return null\n}\n\n/**\n * Extract HTTP status from an error, checking both `status` and `statusCode`.\n */\nexport function extractErrorStatus(error: unknown): number {\n return (error as { status?: number }).status\n ?? (error as { statusCode?: number }).statusCode\n ?? 500\n}\n\n/**\n * Build a standard mxllog error JSON response body.\n * Used by both v2 and v3 error handlers to ensure consistent shape.\n */\nexport function serializeMxllogErrorResponse(error: Error, url: string): Record<string, unknown> {\n const status = extractErrorStatus(error)\n const { data } = error as { data?: unknown }\n const statusMessage = (error as { statusMessage?: string }).statusMessage || error.message\n return {\n url,\n status,\n statusCode: status,\n statusText: statusMessage,\n statusMessage,\n message: error.message,\n error: true,\n ...(data !== undefined && { data }),\n }\n}\n\n"],"mappings":";;;;;AA6DA,SAAgB,mBAAmB,OAA4B;AAC7D,KAAI,MAAM,SAAS,cAAe,QAAO;AACzC,KAAK,MAAM,OAAiB,SAAS,cAAe,QAAO,MAAM;AACjE,QAAO;;;;;AAMT,SAAgB,mBAAmB,OAAwB;AACzD,QAAQ,MAA8B,UAChC,MAAkC,cACnC;;;;;;AAOP,SAAgB,6BAA6B,OAAc,KAAsC;CAC/F,MAAM,SAAS,mBAAmB,MAAM;CACxC,MAAM,EAAE,SAAS;CACjB,MAAM,gBAAiB,MAAqC,iBAAiB,MAAM;AACnF,QAAO;EACL;EACA;EACA,YAAY;EACZ,YAAY;EACZ;EACA,SAAS,MAAM;EACf,OAAO;EACP,GAAI,SAAS,UAAa,EAAE,MAAM;EACnC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { EnvironmentContext, RouteConfig, SamplingConfig, TransportConfig } from "../types.mjs";
|
|
2
|
+
import * as _nuxt_schema0 from "@nuxt/schema";
|
|
3
|
+
|
|
4
|
+
//#region src/nuxt/module.d.ts
|
|
5
|
+
interface ModuleOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Enable or disable all logging globally.
|
|
8
|
+
* When false, all emits, tagged logs, and request logger operations become no-ops.
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Environment context overrides.
|
|
14
|
+
*/
|
|
15
|
+
env?: Partial<EnvironmentContext>;
|
|
16
|
+
/**
|
|
17
|
+
* Enable pretty printing.
|
|
18
|
+
* @default true in development, false in production
|
|
19
|
+
*/
|
|
20
|
+
pretty?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Route patterns to include in logging.
|
|
23
|
+
* Supports glob patterns like '/api/**'.
|
|
24
|
+
* If not set, all routes are logged.
|
|
25
|
+
* @example ['/api/**', '/auth/**']
|
|
26
|
+
*/
|
|
27
|
+
include?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Route patterns to exclude from logging.
|
|
30
|
+
* Supports glob patterns like '/api/_nuxt_icon/**'.
|
|
31
|
+
* Exclusions take precedence over inclusions.
|
|
32
|
+
* @example ['/api/_nuxt_icon/**', '/health']
|
|
33
|
+
*/
|
|
34
|
+
exclude?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Route-specific service configuration.
|
|
37
|
+
* Allows setting different service names for different routes.
|
|
38
|
+
* Patterns are matched using glob syntax.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* routes: {
|
|
43
|
+
* '/api/foo/**': { service: 'service1' },
|
|
44
|
+
* '/api/bar/**': { service: 'service2' }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
routes?: Record<string, RouteConfig>;
|
|
49
|
+
/**
|
|
50
|
+
* Sampling configuration for filtering logs.
|
|
51
|
+
* Allows configuring what percentage of logs to keep per level.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* sampling: {
|
|
56
|
+
* rates: {
|
|
57
|
+
* info: 10, // Keep 10% of info logs
|
|
58
|
+
* warn: 50, // Keep 50% of warning logs
|
|
59
|
+
* debug: 5, // Keep 5% of debug logs
|
|
60
|
+
* error: 100, // Always keep errors (default)
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
sampling?: SamplingConfig;
|
|
66
|
+
/**
|
|
67
|
+
* Transport configuration for sending client logs to the server.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* transport: {
|
|
72
|
+
* enabled: true, // Send logs to server API
|
|
73
|
+
* endpoint: '/api/_mxllog/ingest' // Custom endpoint
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
transport?: TransportConfig;
|
|
78
|
+
/**
|
|
79
|
+
* Axiom adapter configuration.
|
|
80
|
+
* When configured, use `createAxiomDrain()` from `mxllog/axiom` to send logs.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* axiom: {
|
|
85
|
+
* dataset: 'my-app-logs',
|
|
86
|
+
* token: process.env.AXIOM_TOKEN,
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
axiom?: {
|
|
91
|
+
/** Axiom dataset name */dataset: string; /** Axiom API token */
|
|
92
|
+
token: string; /** Organization ID (required for Personal Access Tokens) */
|
|
93
|
+
orgId?: string; /** Base URL for Axiom API. Default: https://api.axiom.co */
|
|
94
|
+
baseUrl?: string; /** Request timeout in milliseconds. Default: 5000 */
|
|
95
|
+
timeout?: number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* OTLP adapter configuration.
|
|
99
|
+
* When configured, use `createOTLPDrain()` from `mxllog/otlp` to send logs.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* otlp: {
|
|
104
|
+
* endpoint: 'http://localhost:4318',
|
|
105
|
+
* headers: {
|
|
106
|
+
* 'Authorization': `Basic ${process.env.GRAFANA_TOKEN}`,
|
|
107
|
+
* },
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
otlp?: {
|
|
112
|
+
/** OTLP HTTP endpoint (e.g., http://localhost:4318) */endpoint: string; /** Override service name (defaults to event.service) */
|
|
113
|
+
serviceName?: string; /** Additional resource attributes */
|
|
114
|
+
resourceAttributes?: Record<string, string | number | boolean>; /** Custom headers (e.g., for authentication) */
|
|
115
|
+
headers?: Record<string, string>; /** Request timeout in milliseconds. Default: 5000 */
|
|
116
|
+
timeout?: number;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* PostHog adapter configuration.
|
|
120
|
+
* When configured, use `createPostHogDrain()` from `mxllog/posthog` to send logs.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* posthog: {
|
|
125
|
+
* apiKey: process.env.POSTHOG_API_KEY,
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
posthog?: {
|
|
130
|
+
/** PostHog project API key */apiKey: string; /** PostHog host URL. Default: https://us.i.posthog.com */
|
|
131
|
+
host?: string; /** PostHog event name. Default: mxllog_wide_event */
|
|
132
|
+
eventName?: string; /** Override distinct_id (defaults to event.service) */
|
|
133
|
+
distinctId?: string; /** Request timeout in milliseconds. Default: 5000 */
|
|
134
|
+
timeout?: number;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Sentry adapter configuration.
|
|
138
|
+
* When configured, use `createSentryDrain()` from `mxllog/sentry` to send logs.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* sentry: {
|
|
143
|
+
* dsn: process.env.SENTRY_DSN,
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
sentry?: {
|
|
148
|
+
/** Sentry DSN */dsn: string; /** Environment override (defaults to event.environment) */
|
|
149
|
+
environment?: string; /** Release version override (defaults to event.version) */
|
|
150
|
+
release?: string; /** Additional tags to attach as attributes */
|
|
151
|
+
tags?: Record<string, string>; /** Request timeout in milliseconds. Default: 5000 */
|
|
152
|
+
timeout?: number;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* How long to retain events before cleanup (used by @mxllog/nuxthub).
|
|
156
|
+
* Supports "30d" (days), "24h" (hours), "60m" (minutes).
|
|
157
|
+
* @default '30d'
|
|
158
|
+
*/
|
|
159
|
+
retention?: string;
|
|
160
|
+
}
|
|
161
|
+
declare const _default: _nuxt_schema0.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
162
|
+
//#endregion
|
|
163
|
+
export { ModuleOptions, _default as default };
|
|
164
|
+
//# sourceMappingURL=module.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.d.mts","names":[],"sources":["../../src/nuxt/module.ts"],"mappings":";;;;UAaiB,aAAA;;;AAAjB;;;EAME,OAAA;EAKM;;;EAAN,GAAA,GAAM,OAAA,CAAQ,kBAAA;EAoEF;;;;EA9DZ,MAAA;EA+Je;;;;;;EAvJf,OAAA;EAQA;;;;;;EAAA,OAAA;EA8CY;;;;;;;;;;;;;EA/BZ,MAAA,GAAS,MAAA,SAAe,WAAA;EAkFtB;;;;;;;;;;;;;;;;EAhEF,QAAA,GAAW,cAAA;EAyHZ;;;;;;;;;;;EA5GC,SAAA,GAAY,eAAA;;;;;;;;;;;;;EAcZ,KAAA;6BAEE,OAAA;IAEA,KAAA;IAEA,KAAA;IAEA,OAAA;IAEA,OAAA;EAAA;;;;;;;;;;;;;;;EAiBF,IAAA;2DAEE,QAAA;IAEA,WAAA;IAEA,kBAAA,GAAqB,MAAA;IAErB,OAAA,GAAU,MAAA;IAEV,OAAA;EAAA;;;;;;;;;;;;EAcF,OAAA;kCAEE,MAAA;IAEA,IAAA;IAEA,SAAA;IAEA,UAAA;IAEA,OAAA;EAAA;;;;;;;;;;;;EAcF,MAAA;qBAEE,GAAA;IAEA,WAAA;IAEA,OAAA;IAEA,IAAA,GAAO,MAAA;IAEP,OAAA;EAAA;;;;;;EAQF,SAAA;AAAA;AAAA,cACD,QAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { addImports, addPlugin, addServerHandler, addServerImports, addServerPlugin, createResolver, defineNuxtModule } from "@nuxt/kit";
|
|
2
|
+
|
|
3
|
+
//#region package.json
|
|
4
|
+
var name = "@safaricom-mxl/log";
|
|
5
|
+
var version = "0.0.3";
|
|
6
|
+
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/nuxt/module.ts
|
|
9
|
+
var module_default = defineNuxtModule({
|
|
10
|
+
meta: {
|
|
11
|
+
name,
|
|
12
|
+
version,
|
|
13
|
+
configKey: name,
|
|
14
|
+
docs: "https://mxllog.dev"
|
|
15
|
+
},
|
|
16
|
+
defaults: {},
|
|
17
|
+
setup(options, nuxt) {
|
|
18
|
+
const resolver = createResolver(import.meta.url);
|
|
19
|
+
const transportEnabled = options.transport?.enabled ?? false;
|
|
20
|
+
const transportEndpoint = options.transport?.endpoint ?? "/api/_mxllog/ingest";
|
|
21
|
+
nuxt.hook("nitro:config", (nitroConfig) => {
|
|
22
|
+
nitroConfig.errorHandler = nitroConfig.errorHandler || resolver.resolve("../nitro/errorHandler");
|
|
23
|
+
});
|
|
24
|
+
nuxt.options.runtimeConfig.mxllog = options;
|
|
25
|
+
nuxt.options.runtimeConfig.public.mxllog = {
|
|
26
|
+
enabled: options.enabled ?? true,
|
|
27
|
+
pretty: options.pretty,
|
|
28
|
+
transport: {
|
|
29
|
+
enabled: transportEnabled,
|
|
30
|
+
endpoint: transportEndpoint
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
if (transportEnabled) addServerHandler({
|
|
34
|
+
route: transportEndpoint,
|
|
35
|
+
method: "post",
|
|
36
|
+
handler: resolver.resolve("../runtime/server/routes/_mxllog/ingest.post")
|
|
37
|
+
});
|
|
38
|
+
addServerPlugin(resolver.resolve("../nitro/plugin"));
|
|
39
|
+
addPlugin({
|
|
40
|
+
src: resolver.resolve("../runtime/client/plugin"),
|
|
41
|
+
mode: "client"
|
|
42
|
+
});
|
|
43
|
+
addImports([
|
|
44
|
+
{
|
|
45
|
+
name: "log",
|
|
46
|
+
from: resolver.resolve("../runtime/client/log")
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "setIdentity",
|
|
50
|
+
from: resolver.resolve("../runtime/client/log")
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "clearIdentity",
|
|
54
|
+
from: resolver.resolve("../runtime/client/log")
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "createMxllogError",
|
|
58
|
+
from: resolver.resolve("../error")
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "parseError",
|
|
62
|
+
from: resolver.resolve("../runtime/utils/parseError")
|
|
63
|
+
}
|
|
64
|
+
]);
|
|
65
|
+
addServerImports([
|
|
66
|
+
{
|
|
67
|
+
name: "useLogger",
|
|
68
|
+
from: resolver.resolve("../runtime/server/useLogger")
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "log",
|
|
72
|
+
from: resolver.resolve("../logger")
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "createMxllogError",
|
|
76
|
+
from: resolver.resolve("../error")
|
|
77
|
+
}
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { module_default as default };
|
|
84
|
+
//# sourceMappingURL=module.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module.mjs","names":[],"sources":["../../package.json","../../src/nuxt/module.ts"],"sourcesContent":["","import {\n addImports,\n addPlugin,\n addServerHandler,\n addServerImports,\n addServerPlugin,\n createResolver,\n defineNuxtModule,\n} from '@nuxt/kit'\nimport type { NitroConfig } from 'nitropack'\nimport type { EnvironmentContext, RouteConfig, SamplingConfig, TransportConfig } from '../types'\nimport { name, version } from '../../package.json'\n\nexport interface ModuleOptions {\n /**\n * Enable or disable all logging globally.\n * When false, all emits, tagged logs, and request logger operations become no-ops.\n * @default true\n */\n enabled?: boolean\n\n /**\n * Environment context overrides.\n */\n env?: Partial<EnvironmentContext>\n\n /**\n * Enable pretty printing.\n * @default true in development, false in production\n */\n pretty?: boolean\n\n /**\n * Route patterns to include in logging.\n * Supports glob patterns like '/api/**'.\n * If not set, all routes are logged.\n * @example ['/api/**', '/auth/**']\n */\n include?: string[]\n\n /**\n * Route patterns to exclude from logging.\n * Supports glob patterns like '/api/_nuxt_icon/**'.\n * Exclusions take precedence over inclusions.\n * @example ['/api/_nuxt_icon/**', '/health']\n */\n exclude?: string[]\n\n /**\n * Route-specific service configuration.\n * Allows setting different service names for different routes.\n * Patterns are matched using glob syntax.\n *\n * @example\n * ```ts\n * routes: {\n * '/api/foo/**': { service: 'service1' },\n * '/api/bar/**': { service: 'service2' }\n * }\n * ```\n */\n routes?: Record<string, RouteConfig>\n\n /**\n * Sampling configuration for filtering logs.\n * Allows configuring what percentage of logs to keep per level.\n *\n * @example\n * ```ts\n * sampling: {\n * rates: {\n * info: 10, // Keep 10% of info logs\n * warn: 50, // Keep 50% of warning logs\n * debug: 5, // Keep 5% of debug logs\n * error: 100, // Always keep errors (default)\n * }\n * }\n * ```\n */\n sampling?: SamplingConfig\n\n /**\n * Transport configuration for sending client logs to the server.\n *\n * @example\n * ```ts\n * transport: {\n * enabled: true, // Send logs to server API\n * endpoint: '/api/_mxllog/ingest' // Custom endpoint\n * }\n * ```\n */\n transport?: TransportConfig\n\n /**\n * Axiom adapter configuration.\n * When configured, use `createAxiomDrain()` from `mxllog/axiom` to send logs.\n *\n * @example\n * ```ts\n * axiom: {\n * dataset: 'my-app-logs',\n * token: process.env.AXIOM_TOKEN,\n * }\n * ```\n */\n axiom?: {\n /** Axiom dataset name */\n dataset: string\n /** Axiom API token */\n token: string\n /** Organization ID (required for Personal Access Tokens) */\n orgId?: string\n /** Base URL for Axiom API. Default: https://api.axiom.co */\n baseUrl?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n }\n\n /**\n * OTLP adapter configuration.\n * When configured, use `createOTLPDrain()` from `mxllog/otlp` to send logs.\n *\n * @example\n * ```ts\n * otlp: {\n * endpoint: 'http://localhost:4318',\n * headers: {\n * 'Authorization': `Basic ${process.env.GRAFANA_TOKEN}`,\n * },\n * }\n * ```\n */\n otlp?: {\n /** OTLP HTTP endpoint (e.g., http://localhost:4318) */\n endpoint: string\n /** Override service name (defaults to event.service) */\n serviceName?: string\n /** Additional resource attributes */\n resourceAttributes?: Record<string, string | number | boolean>\n /** Custom headers (e.g., for authentication) */\n headers?: Record<string, string>\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n }\n\n /**\n * PostHog adapter configuration.\n * When configured, use `createPostHogDrain()` from `mxllog/posthog` to send logs.\n *\n * @example\n * ```ts\n * posthog: {\n * apiKey: process.env.POSTHOG_API_KEY,\n * }\n * ```\n */\n posthog?: {\n /** PostHog project API key */\n apiKey: string\n /** PostHog host URL. Default: https://us.i.posthog.com */\n host?: string\n /** PostHog event name. Default: mxllog_wide_event */\n eventName?: string\n /** Override distinct_id (defaults to event.service) */\n distinctId?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n }\n\n /**\n * Sentry adapter configuration.\n * When configured, use `createSentryDrain()` from `mxllog/sentry` to send logs.\n *\n * @example\n * ```ts\n * sentry: {\n * dsn: process.env.SENTRY_DSN,\n * }\n * ```\n */\n sentry?: {\n /** Sentry DSN */\n dsn: string\n /** Environment override (defaults to event.environment) */\n environment?: string\n /** Release version override (defaults to event.version) */\n release?: string\n /** Additional tags to attach as attributes */\n tags?: Record<string, string>\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n }\n\n /**\n * How long to retain events before cleanup (used by @mxllog/nuxthub).\n * Supports \"30d\" (days), \"24h\" (hours), \"60m\" (minutes).\n * @default '30d'\n */\n retention?: string\n}\n\nexport default defineNuxtModule<ModuleOptions>({\n meta: {\n name,\n version,\n configKey: name,\n docs: 'https://mxllog.dev',\n },\n defaults: {},\n setup(options, nuxt) {\n const resolver = createResolver(import.meta.url)\n\n const transportEnabled = options.transport?.enabled ?? false\n const transportEndpoint = options.transport?.endpoint ?? '/api/_mxllog/ingest'\n\n // Register custom error handler for proper MxllogError serialization\n // Only set if not already configured to avoid overwriting user's custom handler\n // @ts-expect-error nitro:config hook exists but is not in NuxtHooks type\n nuxt.hook('nitro:config', (nitroConfig: NitroConfig) => {\n nitroConfig.errorHandler = nitroConfig.errorHandler || resolver.resolve('../nitro/errorHandler')\n })\n\n nuxt.options.runtimeConfig.mxllog = options\n nuxt.options.runtimeConfig.public.mxllog = {\n enabled: options.enabled ?? true,\n pretty: options.pretty,\n transport: {\n enabled: transportEnabled,\n endpoint: transportEndpoint,\n },\n }\n\n if (transportEnabled) {\n addServerHandler({\n route: transportEndpoint,\n method: 'post',\n handler: resolver.resolve('../runtime/server/routes/_mxllog/ingest.post'),\n })\n }\n\n addServerPlugin(resolver.resolve('../nitro/plugin'))\n\n addPlugin({\n src: resolver.resolve('../runtime/client/plugin'),\n mode: 'client',\n })\n\n addImports([\n {\n name: 'log',\n from: resolver.resolve('../runtime/client/log'),\n },\n {\n name: 'setIdentity',\n from: resolver.resolve('../runtime/client/log'),\n },\n {\n name: 'clearIdentity',\n from: resolver.resolve('../runtime/client/log'),\n },\n {\n name: 'createMxllogError',\n from: resolver.resolve('../error'),\n },\n {\n name: 'parseError',\n from: resolver.resolve('../runtime/utils/parseError'),\n },\n ])\n\n addServerImports([\n {\n name: 'useLogger',\n from: resolver.resolve('../runtime/server/useLogger'),\n },\n {\n name: 'log',\n from: resolver.resolve('../logger'),\n },\n {\n name: 'createMxllogError',\n from: resolver.resolve('../error'),\n },\n ])\n },\n})\n"],"mappings":";;;;;;;;AC0MA,qBAAe,iBAAgC;CAC7C,MAAM;EACJ;EACA;EACA,WAAW;EACX,MAAM;EACP;CACD,UAAU,EAAE;CACZ,MAAM,SAAS,MAAM;EACnB,MAAM,WAAW,eAAe,OAAO,KAAK,IAAI;EAEhD,MAAM,mBAAmB,QAAQ,WAAW,WAAW;EACvD,MAAM,oBAAoB,QAAQ,WAAW,YAAY;AAKzD,OAAK,KAAK,iBAAiB,gBAA6B;AACtD,eAAY,eAAe,YAAY,gBAAgB,SAAS,QAAQ,wBAAwB;IAChG;AAEF,OAAK,QAAQ,cAAc,SAAS;AACpC,OAAK,QAAQ,cAAc,OAAO,SAAS;GACzC,SAAS,QAAQ,WAAW;GAC5B,QAAQ,QAAQ;GAChB,WAAW;IACT,SAAS;IACT,UAAU;IACX;GACF;AAED,MAAI,iBACF,kBAAiB;GACf,OAAO;GACP,QAAQ;GACR,SAAS,SAAS,QAAQ,+CAA+C;GAC1E,CAAC;AAGJ,kBAAgB,SAAS,QAAQ,kBAAkB,CAAC;AAEpD,YAAU;GACR,KAAK,SAAS,QAAQ,2BAA2B;GACjD,MAAM;GACP,CAAC;AAEF,aAAW;GACT;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,wBAAwB;IAChD;GACD;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,wBAAwB;IAChD;GACD;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,wBAAwB;IAChD;GACD;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,WAAW;IACnC;GACD;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,8BAA8B;IACtD;GACF,CAAC;AAEF,mBAAiB;GACf;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,8BAA8B;IACtD;GACD;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,YAAY;IACpC;GACD;IACE,MAAM;IACN,MAAM,SAAS,QAAQ,WAAW;IACnC;GACF,CAAC;;CAEL,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/pipeline.d.ts
|
|
2
|
+
interface DrainPipelineOptions<T = unknown> {
|
|
3
|
+
batch?: {
|
|
4
|
+
/** Maximum number of events per batch sent to the drain function. @default 50 */size?: number; /** Maximum time (ms) an event can stay buffered before a flush is triggered, even if the batch is not full. @default 5000 */
|
|
5
|
+
intervalMs?: number;
|
|
6
|
+
};
|
|
7
|
+
retry?: {
|
|
8
|
+
/** Total number of attempts (including the initial one) before dropping the batch. @default 3 */maxAttempts?: number; /** Delay strategy between retry attempts. @default 'exponential' */
|
|
9
|
+
backoff?: 'exponential' | 'linear' | 'fixed'; /** Base delay (ms) for the first retry. Scaled by the backoff strategy on subsequent retries. @default 1000 */
|
|
10
|
+
initialDelayMs?: number; /** Upper bound (ms) for any single retry delay. @default 30000 */
|
|
11
|
+
maxDelayMs?: number;
|
|
12
|
+
};
|
|
13
|
+
/** Maximum number of events held in the buffer. When exceeded, the oldest event is dropped. @default 1000 */
|
|
14
|
+
maxBufferSize?: number;
|
|
15
|
+
/** Called when a batch is dropped after all retry attempts are exhausted, or when the buffer overflows. */
|
|
16
|
+
onDropped?: (events: T[], error?: Error) => void;
|
|
17
|
+
}
|
|
18
|
+
interface PipelineDrainFn<T> {
|
|
19
|
+
(ctx: T): void;
|
|
20
|
+
/** Flush all buffered events. Call on server shutdown. */
|
|
21
|
+
flush: () => Promise<void>;
|
|
22
|
+
readonly pending: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a drain pipeline that batches events, retries on failure, and manages buffer overflow.
|
|
26
|
+
*
|
|
27
|
+
* Returns a higher-order function: pass your drain adapter to get a hook-compatible function.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const pipeline = createDrainPipeline({ batch: { size: 50 } })
|
|
32
|
+
* const drain = pipeline(async (batch) => {
|
|
33
|
+
* await sendToBackend(batch)
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* // Use as a hook
|
|
37
|
+
* nitroApp.hooks.hook('@safaricom-mxl/log:drain', drain)
|
|
38
|
+
*
|
|
39
|
+
* // Flush on shutdown
|
|
40
|
+
* nitroApp.hooks.hook('close', () => drain.flush())
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function createDrainPipeline<T = unknown>(options?: DrainPipelineOptions<T>): (drain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { DrainPipelineOptions, PipelineDrainFn, createDrainPipeline };
|
|
46
|
+
//# sourceMappingURL=pipeline.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.mts","names":[],"sources":["../src/pipeline.ts"],"mappings":";UAAiB,oBAAA;EACf,KAAA;IADmC,iFAGjC,IAAA,WAiBqC;IAfrC,UAAA;EAAA;EAEF,KAAA;IAFE,iGAIA,WAAA,WAAA;IAEA,OAAA,uCAEA;IAAA,cAAA,WAKF;IAHE,UAAA;EAAA;EAKW;EAFb,aAAA;EAE0B;EAA1B,SAAA,IAAa,MAAA,EAAQ,CAAA,IAAK,KAAA,GAAQ,KAAA;AAAA;AAAA,UAGnB,eAAA;EAAA,CACd,GAAA,EAAK,CAAA;EADwB;EAG9B,KAAA,QAAa,OAAA;EAAA,SACJ,OAAA;AAAA;;;;;;;AAsBX;;;;;;;;;;;;;iBAAgB,mBAAA,aAAA,CAAiC,OAAA,GAAU,oBAAA,CAAqB,CAAA,KAAM,KAAA,GAAQ,KAAA,EAAO,CAAA,cAAe,OAAA,WAAkB,eAAA,CAAgB,CAAA"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//#region src/pipeline.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create a drain pipeline that batches events, retries on failure, and manages buffer overflow.
|
|
4
|
+
*
|
|
5
|
+
* Returns a higher-order function: pass your drain adapter to get a hook-compatible function.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const pipeline = createDrainPipeline({ batch: { size: 50 } })
|
|
10
|
+
* const drain = pipeline(async (batch) => {
|
|
11
|
+
* await sendToBackend(batch)
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* // Use as a hook
|
|
15
|
+
* nitroApp.hooks.hook('@safaricom-mxl/log:drain', drain)
|
|
16
|
+
*
|
|
17
|
+
* // Flush on shutdown
|
|
18
|
+
* nitroApp.hooks.hook('close', () => drain.flush())
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function createDrainPipeline(options) {
|
|
22
|
+
const batchSize = options?.batch?.size ?? 50;
|
|
23
|
+
const intervalMs = options?.batch?.intervalMs ?? 5e3;
|
|
24
|
+
const maxBufferSize = options?.maxBufferSize ?? 1e3;
|
|
25
|
+
const maxAttempts = options?.retry?.maxAttempts ?? 3;
|
|
26
|
+
const backoffStrategy = options?.retry?.backoff ?? "exponential";
|
|
27
|
+
const initialDelayMs = options?.retry?.initialDelayMs ?? 1e3;
|
|
28
|
+
const maxDelayMs = options?.retry?.maxDelayMs ?? 3e4;
|
|
29
|
+
const onDropped = options?.onDropped;
|
|
30
|
+
if (batchSize <= 0 || !Number.isFinite(batchSize)) throw new Error(`[mxllog/pipeline] batch.size must be a positive finite number, got: ${batchSize}`);
|
|
31
|
+
if (intervalMs <= 0 || !Number.isFinite(intervalMs)) throw new Error(`[mxllog/pipeline] batch.intervalMs must be a positive finite number, got: ${intervalMs}`);
|
|
32
|
+
if (maxBufferSize <= 0 || !Number.isFinite(maxBufferSize)) throw new Error(`[mxllog/pipeline] maxBufferSize must be a positive finite number, got: ${maxBufferSize}`);
|
|
33
|
+
if (maxAttempts <= 0 || !Number.isFinite(maxAttempts)) throw new Error(`[mxllog/pipeline] retry.maxAttempts must be a positive finite number, got: ${maxAttempts}`);
|
|
34
|
+
if (initialDelayMs < 0 || !Number.isFinite(initialDelayMs)) throw new Error(`[mxllog/pipeline] retry.initialDelayMs must be a non-negative finite number, got: ${initialDelayMs}`);
|
|
35
|
+
if (maxDelayMs < 0 || !Number.isFinite(maxDelayMs)) throw new Error(`[mxllog/pipeline] retry.maxDelayMs must be a non-negative finite number, got: ${maxDelayMs}`);
|
|
36
|
+
return (drain) => {
|
|
37
|
+
const buffer = [];
|
|
38
|
+
let timer = null;
|
|
39
|
+
let activeFlush = null;
|
|
40
|
+
function clearTimer() {
|
|
41
|
+
if (timer !== null) {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
timer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function scheduleFlush() {
|
|
47
|
+
if (timer !== null || activeFlush) return;
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
timer = null;
|
|
50
|
+
if (!activeFlush) startFlush();
|
|
51
|
+
}, intervalMs);
|
|
52
|
+
}
|
|
53
|
+
function getRetryDelay(attempt) {
|
|
54
|
+
let delay;
|
|
55
|
+
switch (backoffStrategy) {
|
|
56
|
+
case "linear":
|
|
57
|
+
delay = initialDelayMs * attempt;
|
|
58
|
+
break;
|
|
59
|
+
case "fixed":
|
|
60
|
+
delay = initialDelayMs;
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
delay = initialDelayMs * 2 ** (attempt - 1);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
return Math.min(delay, maxDelayMs);
|
|
67
|
+
}
|
|
68
|
+
async function sendWithRetry(batch) {
|
|
69
|
+
let lastError;
|
|
70
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
71
|
+
await drain(batch);
|
|
72
|
+
return;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
75
|
+
if (attempt < maxAttempts) await new Promise((r) => setTimeout(r, getRetryDelay(attempt)));
|
|
76
|
+
}
|
|
77
|
+
onDropped?.(batch, lastError);
|
|
78
|
+
}
|
|
79
|
+
async function drainBuffer() {
|
|
80
|
+
while (buffer.length > 0) await sendWithRetry(buffer.splice(0, batchSize));
|
|
81
|
+
}
|
|
82
|
+
function startFlush() {
|
|
83
|
+
if (activeFlush) return;
|
|
84
|
+
activeFlush = drainBuffer().finally(() => {
|
|
85
|
+
activeFlush = null;
|
|
86
|
+
if (buffer.length >= batchSize) startFlush();
|
|
87
|
+
else if (buffer.length > 0) scheduleFlush();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function push(ctx) {
|
|
91
|
+
if (buffer.length >= maxBufferSize) {
|
|
92
|
+
const dropped = buffer.splice(0, 1);
|
|
93
|
+
onDropped?.(dropped);
|
|
94
|
+
}
|
|
95
|
+
buffer.push(ctx);
|
|
96
|
+
if (buffer.length >= batchSize) {
|
|
97
|
+
clearTimer();
|
|
98
|
+
startFlush();
|
|
99
|
+
} else if (!activeFlush) scheduleFlush();
|
|
100
|
+
}
|
|
101
|
+
async function flush() {
|
|
102
|
+
clearTimer();
|
|
103
|
+
if (activeFlush) await activeFlush;
|
|
104
|
+
const snapshot = buffer.length;
|
|
105
|
+
if (snapshot > 0) {
|
|
106
|
+
const toFlush = buffer.splice(0, snapshot);
|
|
107
|
+
while (toFlush.length > 0) await sendWithRetry(toFlush.splice(0, batchSize));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const hookFn = push;
|
|
111
|
+
hookFn.flush = flush;
|
|
112
|
+
Object.defineProperty(hookFn, "pending", {
|
|
113
|
+
get: () => buffer.length,
|
|
114
|
+
enumerable: true
|
|
115
|
+
});
|
|
116
|
+
return hookFn;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
export { createDrainPipeline };
|
|
122
|
+
//# sourceMappingURL=pipeline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.mjs","names":[],"sources":["../src/pipeline.ts"],"sourcesContent":["export interface DrainPipelineOptions<T = unknown> {\n batch?: {\n /** Maximum number of events per batch sent to the drain function. @default 50 */\n size?: number\n /** Maximum time (ms) an event can stay buffered before a flush is triggered, even if the batch is not full. @default 5000 */\n intervalMs?: number\n }\n retry?: {\n /** Total number of attempts (including the initial one) before dropping the batch. @default 3 */\n maxAttempts?: number\n /** Delay strategy between retry attempts. @default 'exponential' */\n backoff?: 'exponential' | 'linear' | 'fixed'\n /** Base delay (ms) for the first retry. Scaled by the backoff strategy on subsequent retries. @default 1000 */\n initialDelayMs?: number\n /** Upper bound (ms) for any single retry delay. @default 30000 */\n maxDelayMs?: number\n }\n /** Maximum number of events held in the buffer. When exceeded, the oldest event is dropped. @default 1000 */\n maxBufferSize?: number\n /** Called when a batch is dropped after all retry attempts are exhausted, or when the buffer overflows. */\n onDropped?: (events: T[], error?: Error) => void\n}\n\nexport interface PipelineDrainFn<T> {\n (ctx: T): void\n /** Flush all buffered events. Call on server shutdown. */\n flush: () => Promise<void>\n readonly pending: number\n}\n\n/**\n * Create a drain pipeline that batches events, retries on failure, and manages buffer overflow.\n *\n * Returns a higher-order function: pass your drain adapter to get a hook-compatible function.\n *\n * @example\n * ```ts\n * const pipeline = createDrainPipeline({ batch: { size: 50 } })\n * const drain = pipeline(async (batch) => {\n * await sendToBackend(batch)\n * })\n *\n * // Use as a hook\n * nitroApp.hooks.hook('@safaricom-mxl/log:drain', drain)\n *\n * // Flush on shutdown\n * nitroApp.hooks.hook('close', () => drain.flush())\n * ```\n */\nexport function createDrainPipeline<T = unknown>(options?: DrainPipelineOptions<T>): (drain: (batch: T[]) => void | Promise<void>) => PipelineDrainFn<T> {\n const batchSize = options?.batch?.size ?? 50\n const intervalMs = options?.batch?.intervalMs ?? 5000\n const maxBufferSize = options?.maxBufferSize ?? 1000\n const maxAttempts = options?.retry?.maxAttempts ?? 3\n const backoffStrategy = options?.retry?.backoff ?? 'exponential'\n const initialDelayMs = options?.retry?.initialDelayMs ?? 1000\n const maxDelayMs = options?.retry?.maxDelayMs ?? 30000\n const onDropped = options?.onDropped\n\n if (batchSize <= 0 || !Number.isFinite(batchSize)) {\n throw new Error(`[mxllog/pipeline] batch.size must be a positive finite number, got: ${batchSize}`)\n }\n if (intervalMs <= 0 || !Number.isFinite(intervalMs)) {\n throw new Error(`[mxllog/pipeline] batch.intervalMs must be a positive finite number, got: ${intervalMs}`)\n }\n if (maxBufferSize <= 0 || !Number.isFinite(maxBufferSize)) {\n throw new Error(`[mxllog/pipeline] maxBufferSize must be a positive finite number, got: ${maxBufferSize}`)\n }\n if (maxAttempts <= 0 || !Number.isFinite(maxAttempts)) {\n throw new Error(`[mxllog/pipeline] retry.maxAttempts must be a positive finite number, got: ${maxAttempts}`)\n }\n if (initialDelayMs < 0 || !Number.isFinite(initialDelayMs)) {\n throw new Error(`[mxllog/pipeline] retry.initialDelayMs must be a non-negative finite number, got: ${initialDelayMs}`)\n }\n if (maxDelayMs < 0 || !Number.isFinite(maxDelayMs)) {\n throw new Error(`[mxllog/pipeline] retry.maxDelayMs must be a non-negative finite number, got: ${maxDelayMs}`)\n }\n\n return (drain: (batch: T[]) => void | Promise<void>): PipelineDrainFn<T> => {\n const buffer: T[] = []\n let timer: ReturnType<typeof setTimeout> | null = null\n let activeFlush: Promise<void> | null = null\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n\n function scheduleFlush(): void {\n if (timer !== null || activeFlush) return\n timer = setTimeout(() => {\n timer = null\n if (!activeFlush) startFlush()\n }, intervalMs)\n }\n\n function getRetryDelay(attempt: number): number {\n let delay: number\n switch (backoffStrategy) {\n case 'linear':\n delay = initialDelayMs * attempt\n break\n case 'fixed':\n delay = initialDelayMs\n break\n case 'exponential':\n default:\n delay = initialDelayMs * 2 ** (attempt - 1)\n break\n }\n return Math.min(delay, maxDelayMs)\n }\n\n async function sendWithRetry(batch: T[]): Promise<void> {\n let lastError: Error | undefined\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n await drain(batch)\n return\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error))\n if (attempt < maxAttempts) {\n await new Promise<void>(r => setTimeout(r, getRetryDelay(attempt)))\n }\n }\n }\n onDropped?.(batch, lastError)\n }\n\n async function drainBuffer(): Promise<void> {\n while (buffer.length > 0) {\n const batch = buffer.splice(0, batchSize)\n await sendWithRetry(batch)\n }\n }\n\n function startFlush(): void {\n if (activeFlush) return\n activeFlush = drainBuffer().finally(() => {\n activeFlush = null\n if (buffer.length >= batchSize) {\n startFlush()\n } else if (buffer.length > 0) {\n scheduleFlush()\n }\n })\n }\n\n function push(ctx: T): void {\n if (buffer.length >= maxBufferSize) {\n const dropped = buffer.splice(0, 1)\n onDropped?.(dropped)\n }\n buffer.push(ctx)\n\n if (buffer.length >= batchSize) {\n clearTimer()\n startFlush()\n } else if (!activeFlush) {\n scheduleFlush()\n }\n }\n\n async function flush(): Promise<void> {\n clearTimer()\n if (activeFlush) {\n await activeFlush\n }\n // Snapshot the buffer length to avoid infinite loop if push() is called during flush\n const snapshot = buffer.length\n if (snapshot > 0) {\n const toFlush = buffer.splice(0, snapshot)\n while (toFlush.length > 0) {\n const batch = toFlush.splice(0, batchSize)\n await sendWithRetry(batch)\n }\n }\n }\n\n const hookFn = push as PipelineDrainFn<T>\n hookFn.flush = flush\n Object.defineProperty(hookFn, 'pending', {\n get: () => buffer.length,\n enumerable: true,\n })\n\n return hookFn\n }\n}\n"],"mappings":"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"}
|