Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/adapters-runtime-workers-hyperdx.md
Original file line number Diff line number Diff line change
@@ -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.
51 changes: 32 additions & 19 deletions packages/evlog/src/adapters/_config.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,50 @@
/**
* 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<string, any> | 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<Record<string, any> | 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<T> {
key: keyof T & string
env?: string[]
}

export function resolveAdapterConfig<T>(
export async function resolveAdapterConfig<T>(
namespace: string,
fields: ConfigField<T>[],
overrides?: Partial<T>,
): Partial<T> {
const runtimeConfig = getRuntimeConfig()
): Promise<Partial<T>> {
const runtimeConfig = await getRuntimeConfig()
const evlogNs = runtimeConfig?.evlog?.[namespace]
const rootNs = runtimeConfig?.[namespace]

Expand Down
9 changes: 7 additions & 2 deletions packages/evlog/src/adapters/_drain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { DrainContext, WideEvent } from '../types'

export interface DrainOptions<TConfig> {
name: string
resolve: () => TConfig | null
resolve: () => TConfig | null | Promise<TConfig | null>
send: (events: WideEvent[], config: TConfig) => Promise<void>
}

/**
* 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<TConfig>(options: DrainOptions<TConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {
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 {
Expand Down
4 changes: 2 additions & 2 deletions packages/evlog/src/adapters/axiom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ const AXIOM_FIELDS: ConfigField<ResolvedAxiomConfig>[] = [
export function createAxiomDrain(overrides?: Partial<AxiomConfig>) {
return defineDrain<AxiomConfig>({
name: 'axiom',
resolve: () => {
const config = resolveAdapterConfig<ResolvedAxiomConfig>(
resolve: async () => {
const config = await resolveAdapterConfig<ResolvedAxiomConfig>(
'axiom',
AXIOM_FIELDS,
overrides as Partial<ResolvedAxiomConfig>,
Expand Down
4 changes: 2 additions & 2 deletions packages/evlog/src/adapters/better-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export function toBetterStackEvent(event: WideEvent): Record<string, unknown> {
export function createBetterStackDrain(overrides?: Partial<BetterStackConfig>) {
return defineDrain<BetterStackConfig>({
name: 'better-stack',
resolve: () => {
const config = resolveAdapterConfig<BetterStackConfig>('betterStack', BETTER_STACK_FIELDS, overrides)
resolve: async () => {
const config = await resolveAdapterConfig<BetterStackConfig>('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
Expand Down
4 changes: 2 additions & 2 deletions packages/evlog/src/adapters/hyperdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export function toHyperDXOTLPConfig(config: HyperDXConfig): OTLPConfig {
export function createHyperDXDrain(overrides?: Partial<HyperDXConfig>) {
return defineDrain<HyperDXConfig>({
name: 'hyperdx',
resolve: () => {
const config = resolveAdapterConfig<HyperDXConfig>('hyperdx', HYPERDX_FIELDS, overrides)
resolve: async () => {
const config = await resolveAdapterConfig<HyperDXConfig>('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
Expand Down
4 changes: 2 additions & 2 deletions packages/evlog/src/adapters/otlp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ function getHeadersFromEnv(): Record<string, string> | undefined {
export function createOTLPDrain(overrides?: Partial<OTLPConfig>) {
return defineDrain<OTLPConfig>({
name: 'otlp',
resolve: () => {
const config = resolveAdapterConfig<OTLPConfig>('otlp', OTLP_FIELDS, overrides)
resolve: async () => {
const config = await resolveAdapterConfig<OTLPConfig>('otlp', OTLP_FIELDS, overrides)

// OTLP-specific: resolve headers from env if not provided via config
if (!config.headers) {
Expand Down
8 changes: 4 additions & 4 deletions packages/evlog/src/adapters/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ export function toPostHogEvent(event: WideEvent, config: PostHogEventsConfig): P
export function createPostHogDrain(overrides?: Partial<PostHogConfig>) {
return defineDrain<PostHogConfig>({
name: 'posthog',
resolve: () => {
const config = resolveAdapterConfig<PostHogConfig>('posthog', POSTHOG_FIELDS, overrides)
resolve: async () => {
const config = await resolveAdapterConfig<PostHogConfig>('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
Expand Down Expand Up @@ -172,8 +172,8 @@ export async function sendBatchToPostHog(events: WideEvent[], config: PostHogCon
export function createPostHogEventsDrain(overrides?: Partial<PostHogEventsConfig>) {
return defineDrain<PostHogEventsConfig>({
name: 'posthog-events',
resolve: () => {
const config = resolveAdapterConfig<PostHogEventsConfig>('posthog', POSTHOG_EVENTS_FIELDS, overrides)
resolve: async () => {
const config = await resolveAdapterConfig<PostHogEventsConfig>('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
Expand Down
4 changes: 2 additions & 2 deletions packages/evlog/src/adapters/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ function buildEnvelopeBody(logs: SentryLog[], dsn: string): string {
export function createSentryDrain(overrides?: Partial<SentryConfig>) {
return defineDrain<SentryConfig>({
name: 'sentry',
resolve: () => {
const config = resolveAdapterConfig<SentryConfig>('sentry', SENTRY_FIELDS, overrides)
resolve: async () => {
const config = await resolveAdapterConfig<SentryConfig>('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
Expand Down
Loading