diff --git a/.changeset/adapters-runtime-workers-hyperdx.md b/.changeset/adapters-runtime-workers-hyperdx.md new file mode 100644 index 0000000..753082d --- /dev/null +++ b/.changeset/adapters-runtime-workers-hyperdx.md @@ -0,0 +1,5 @@ +--- +"evlog": patch +--- + +Resolve Nitro runtime config in drain adapters via dynamic `import()` (Cloudflare Workers and other runtimes without `require`). Cache Nitro module namespaces after first load to avoid repeated imports on every drain. Fix HyperDX drain to `await` `resolveAdapterConfig()` so env/runtime config is applied when using `createHyperDXDrain()` without inline overrides. diff --git a/packages/evlog/src/adapters/_config.ts b/packages/evlog/src/adapters/_config.ts index aebee8e..7a25343 100644 --- a/packages/evlog/src/adapters/_config.ts +++ b/packages/evlog/src/adapters/_config.ts @@ -1,24 +1,37 @@ /** - * Try to get runtime config from Nitro/Nuxt environment. - * Supports both Nitro v2 (nitropack/runtime) and Nitro v3 (nitro/runtime-config). - * Returns undefined if not in a Nitro context. + * Nitro runtime modules resolved via dynamic `import()` (Workers-safe: avoids a bundler-injected + * `createRequire` polyfill from sync `require()`). Module namespaces are cached after first + * successful load; `useRuntimeConfig()` is still invoked on each call so config stays current. + * + * Drain handlers remain non-blocking for the HTTP response when the host provides `waitUntil` + * (see Nitro plugin); the extra `await` here only sequences work inside that background drain. */ -export function getRuntimeConfig(): Record | undefined { - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitropack/runtime') - return useRuntimeConfig() - } catch { - // nitropack not available — try Nitro v3 +let nitropackRuntime: typeof import('nitropack/runtime') | null | undefined +let nitroV3Runtime: typeof import('nitro/runtime-config') | null | undefined + +export async function getRuntimeConfig(): Promise | undefined> { + if (nitropackRuntime === undefined) { + try { + nitropackRuntime = await import('nitropack/runtime') + } catch { + nitropackRuntime = null + } + } + if (nitropackRuntime) { + return nitropackRuntime.useRuntimeConfig() } - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useRuntimeConfig } = require('nitro/runtime-config') - return useRuntimeConfig() - } catch { - return undefined + if (nitroV3Runtime === undefined) { + try { + nitroV3Runtime = await import('nitro/runtime-config') + } catch { + nitroV3Runtime = null + } } + if (nitroV3Runtime) { + return nitroV3Runtime.useRuntimeConfig() + } + return undefined } export interface ConfigField { @@ -26,12 +39,12 @@ export interface ConfigField { env?: string[] } -export function resolveAdapterConfig( +export async function resolveAdapterConfig( namespace: string, fields: ConfigField[], overrides?: Partial, -): Partial { - const runtimeConfig = getRuntimeConfig() +): Promise> { + const runtimeConfig = await getRuntimeConfig() const evlogNs = runtimeConfig?.evlog?.[namespace] const rootNs = runtimeConfig?.[namespace] diff --git a/packages/evlog/src/adapters/_drain.ts b/packages/evlog/src/adapters/_drain.ts index abfe555..ca37ccf 100644 --- a/packages/evlog/src/adapters/_drain.ts +++ b/packages/evlog/src/adapters/_drain.ts @@ -2,16 +2,21 @@ import type { DrainContext, WideEvent } from '../types' export interface DrainOptions { name: string - resolve: () => TConfig | null + resolve: () => TConfig | null | Promise send: (events: WideEvent[], config: TConfig) => Promise } +/** + * Build a drain callback for `evlog:drain` (or `initLogger({ drain })`). + * The returned function is async so `resolve` can load Nitro runtime config; hosts typically attach + * the resulting promise to `waitUntil` so the HTTP response is not blocked (see Nitro plugin). + */ export function defineDrain(options: DrainOptions): (ctx: DrainContext | DrainContext[]) => Promise { return async (ctx: DrainContext | DrainContext[]) => { const contexts = Array.isArray(ctx) ? ctx : [ctx] if (contexts.length === 0) return - const config = options.resolve() + const config = await options.resolve() if (!config) return try { diff --git a/packages/evlog/src/adapters/axiom.ts b/packages/evlog/src/adapters/axiom.ts index fd70906..5184dcc 100644 --- a/packages/evlog/src/adapters/axiom.ts +++ b/packages/evlog/src/adapters/axiom.ts @@ -75,8 +75,8 @@ const AXIOM_FIELDS: ConfigField[] = [ export function createAxiomDrain(overrides?: Partial) { return defineDrain({ name: 'axiom', - resolve: () => { - const config = resolveAdapterConfig( + resolve: async () => { + const config = await resolveAdapterConfig( 'axiom', AXIOM_FIELDS, overrides as Partial, diff --git a/packages/evlog/src/adapters/better-stack.ts b/packages/evlog/src/adapters/better-stack.ts index 2e0a384..9cd8058 100644 --- a/packages/evlog/src/adapters/better-stack.ts +++ b/packages/evlog/src/adapters/better-stack.ts @@ -54,8 +54,8 @@ export function toBetterStackEvent(event: WideEvent): Record { export function createBetterStackDrain(overrides?: Partial) { return defineDrain({ name: 'better-stack', - resolve: () => { - const config = resolveAdapterConfig('betterStack', BETTER_STACK_FIELDS, overrides) + resolve: async () => { + const config = await resolveAdapterConfig('betterStack', BETTER_STACK_FIELDS, overrides) if (!config.sourceToken) { console.error('[evlog/better-stack] Missing source token. Set NUXT_BETTER_STACK_SOURCE_TOKEN env var or pass to createBetterStackDrain()') return null diff --git a/packages/evlog/src/adapters/hyperdx.ts b/packages/evlog/src/adapters/hyperdx.ts index 290da64..befbf76 100644 --- a/packages/evlog/src/adapters/hyperdx.ts +++ b/packages/evlog/src/adapters/hyperdx.ts @@ -81,8 +81,8 @@ export function toHyperDXOTLPConfig(config: HyperDXConfig): OTLPConfig { export function createHyperDXDrain(overrides?: Partial) { return defineDrain({ name: 'hyperdx', - resolve: () => { - const config = resolveAdapterConfig('hyperdx', HYPERDX_FIELDS, overrides) + resolve: async () => { + const config = await resolveAdapterConfig('hyperdx', HYPERDX_FIELDS, overrides) if (!config.apiKey) { console.error('[evlog/hyperdx] Missing apiKey. Set HYPERDX_API_KEY or NUXT_HYPERDX_API_KEY, or pass to createHyperDXDrain()') return null diff --git a/packages/evlog/src/adapters/otlp.ts b/packages/evlog/src/adapters/otlp.ts index 08f2b82..ed3764c 100644 --- a/packages/evlog/src/adapters/otlp.ts +++ b/packages/evlog/src/adapters/otlp.ts @@ -245,8 +245,8 @@ function getHeadersFromEnv(): Record | undefined { export function createOTLPDrain(overrides?: Partial) { return defineDrain({ name: 'otlp', - resolve: () => { - const config = resolveAdapterConfig('otlp', OTLP_FIELDS, overrides) + resolve: async () => { + const config = await resolveAdapterConfig('otlp', OTLP_FIELDS, overrides) // OTLP-specific: resolve headers from env if not provided via config if (!config.headers) { diff --git a/packages/evlog/src/adapters/posthog.ts b/packages/evlog/src/adapters/posthog.ts index 904d1c2..9aef8b7 100644 --- a/packages/evlog/src/adapters/posthog.ts +++ b/packages/evlog/src/adapters/posthog.ts @@ -105,8 +105,8 @@ export function toPostHogEvent(event: WideEvent, config: PostHogEventsConfig): P export function createPostHogDrain(overrides?: Partial) { return defineDrain({ name: 'posthog', - resolve: () => { - const config = resolveAdapterConfig('posthog', POSTHOG_FIELDS, overrides) + resolve: async () => { + const config = await resolveAdapterConfig('posthog', POSTHOG_FIELDS, overrides) if (!config.apiKey) { console.error('[evlog/posthog] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogDrain()') return null @@ -172,8 +172,8 @@ export async function sendBatchToPostHog(events: WideEvent[], config: PostHogCon export function createPostHogEventsDrain(overrides?: Partial) { return defineDrain({ name: 'posthog-events', - resolve: () => { - const config = resolveAdapterConfig('posthog', POSTHOG_EVENTS_FIELDS, overrides) + resolve: async () => { + const config = await resolveAdapterConfig('posthog', POSTHOG_EVENTS_FIELDS, overrides) if (!config.apiKey) { console.error('[evlog/posthog-events] Missing apiKey. Set NUXT_POSTHOG_API_KEY/POSTHOG_API_KEY env var or pass to createPostHogEventsDrain()') return null diff --git a/packages/evlog/src/adapters/sentry.ts b/packages/evlog/src/adapters/sentry.ts index 21f722d..cadbaa3 100644 --- a/packages/evlog/src/adapters/sentry.ts +++ b/packages/evlog/src/adapters/sentry.ts @@ -224,8 +224,8 @@ function buildEnvelopeBody(logs: SentryLog[], dsn: string): string { export function createSentryDrain(overrides?: Partial) { return defineDrain({ name: 'sentry', - resolve: () => { - const config = resolveAdapterConfig('sentry', SENTRY_FIELDS, overrides) + resolve: async () => { + const config = await resolveAdapterConfig('sentry', SENTRY_FIELDS, overrides) if (!config.dsn) { console.error('[evlog/sentry] Missing DSN. Set NUXT_SENTRY_DSN/SENTRY_DSN env var or pass to createSentryDrain()') return null