SKILL.md
Create evlog Adapter
Add a new built-in adapter to evlog. Every adapter follows the same architecture and is built on the public toolkit primitives in evlog/toolkit — so a community adapter has the same shape as a built-in one.
PR Title
Recommended format for the pull request title:
feat: add {name} adapter
The exact wording may vary depending on the adapter (e.g., feat: add OTLP adapter, feat: add Axiom drain adapter), but it should always follow the feat: conventional commit prefix.
Touchpoints Checklist
#
File
Action
1
packages/evlog/src/adapters/{name}.ts
Create adapter source (built on defineHttpDrain from ../shared/drain)
2
packages/evlog/tsdown.config.ts
Add build entry
3
packages/evlog/package.json
Add exports + typesVersions entries
4
packages/evlog/test/adapters/{name}.test.ts
Create tests
5
apps/docs/content/4.adapters/{n}.{name}.md
Create adapter doc page (before custom.md)
6
apps/docs/content/4.adapters/1.overview.md
Add adapter to overview (links, card, env vars)
7
skills/review-logging-patterns/SKILL.md
Add adapter row in the Drain Adapters table
8
Renumber custom.md
Ensure custom.md stays last after the new adapter
Important: Do NOT consider the task complete until all 8 touchpoints have been addressed.
Naming Conventions
Use these placeholders consistently:
Placeholder
Example (Datadog)
Usage
{name}
datadog
File names, import paths, env var suffix
{Name}
Datadog
PascalCase in function/interface names
{NAME}
DATADOG
SCREAMING_CASE in env var prefixes
Standard option naming (use these exact names):
Concept
Standard option name
Bearer-style API secret
apiKey
Base URL of the ingest API
endpoint
Service identifier
serviceName
Request timeout (ms)
timeout
If a service historically used a different name (token, sourceToken, …) keep it as a deprecated alias — see Axiom and Better Stack for the pattern.
Step 1: Adapter Source — built on defineHttpDrain
Create packages/evlog/src/adapters/{name}.ts. Read references/adapter-template.md for the full annotated template.
The contract is now defineHttpDrain<TConfig>({ resolve, encode }). You only ship two pieces of logic:
- **
resolve()** — produce a fully-resolved config ornullto skip. UseresolveAdapterConfigfor the standard precedence (overrides →runtimeConfig.evlog.{name}→runtimeConfig.{name}→NUXT_{NAME}_*→{NAME}_*).
- **
encode(events, config)** — produce{ url, headers, body }for a batch of events (ornullto skip). HTTP transport, retries, timeout, and error logging are handled bydefineHttpDrain.
Key rules:
- Single factory. Export one
create{Name}Drain(overrides?: Partial<{Name}Config>). No dual-API factories: if a service has multiple ingest modes (logs vs events), expose them via amodeoption (see PostHog).
- No HTTP code in the adapter. Don't call
fetchdirectly — letdefineHttpDraindo it. If your service truly needs custom transport (e.g. binary envelopes), usedefineDrainand callhttpPostfromevlog/toolkit.
- No bespoke config resolution. Always go through
resolveAdapterConfig. If you need to support a deprecated alias (token→apiKey), include both in theConfigField[]and fall through inresolve().
- Exported converters. If the service needs a specific event shape, export a
to{Name}Event()(orbuildPayload()) helper so it can be tested independently.
Step 2: Build Config
Add a build entry in packages/evlog/tsdown.config.ts alongside the existing adapters:
'adapters/{name}': 'src/adapters/{name}.ts',
Place it after the last adapter entry in tsdown.config.ts (follow existing ordering in that file).
Step 3: Package Exports
In packages/evlog/package.json, add two entries:
**In exports** (after the last adapter, currently ./posthog):
"./{name}": {
"types": "./dist/adapters/{name}.d.mts",
"import": "./dist/adapters/{name}.mjs"
}
**In typesVersions["*"]** (after the last adapter):
"{name}": [
"./dist/adapters/{name}.d.mts"
]
Step 4: Tests
Create packages/evlog/test/adapters/{name}.test.ts.
Read references/test-template.md for the full annotated template.
Required test categories:
- URL construction (default + custom endpoint)
- Headers (auth, content-type, service-specific)
- Request body format (JSON structure matches service API)
- Skip behavior when
apiKey(or required field) is missing
- Batch operations
- Deprecated alias still works (when applicable)
Step 5: Adapter Documentation Page
Create apps/docs/content/4.adapters/{n}.{name}.md where {n} is the next number before custom.md (custom should always be last).
Use the existing Axiom adapter page (apps/docs/content/4.adapters/2.axiom.md) as a reference for frontmatter structure, tone, and sections. Key sections: intro, quick setup, configuration (env vars table + priority), advanced usage, querying in the target service, troubleshooting, direct API usage, next steps.
Important: multi-framework examples. The Quick Start section must include a ::code-group with tabs for all supported frameworks (Nuxt/Nitro, Hono, Express, Fastify, Elysia, NestJS, Standalone). Do not only show Nitro examples. See any existing adapter page for the pattern.
Step 6: Update Adapters Overview Page
Edit apps/docs/content/4.adapters/1.overview.md to add the new adapter in three places (follow the pattern of existing adapters):
- **Frontmatter
linksarray** — add a link entry with icon and path
- **
::card-groupsection** — add a card block before the Custom card
- **Zero-Config Setup
.envexample** — add the adapter's env vars
Step 7: Update skills/review-logging-patterns/SKILL.md
In skills/review-logging-patterns/SKILL.md (the public skill distributed to users), find the Drain Adapters table and add a new row:
| {Name} | `evlog/{name}` | `{NAME}_API_KEY`, `{NAME}_DATASET` (or equivalent) |
Follow the pattern of the existing rows (Axiom, OTLP, PostHog, Sentry, Better Stack).
Step 8: Renumber custom.md
If the new adapter's number conflicts with custom.md, renumber custom.md to be the last entry. For example, if the new adapter is 5.{name}.md, rename 5.custom.md to 6.custom.md.
Verification
After completing all steps, run:
cd packages/evlog
pnpm run lint
pnpm run typecheck
pnpm run test
pnpm run build