| name | create-evlog-adapter |
| description | Create a new built-in evlog adapter to send wide events to an external observability platform. Use when adding a new drain adapter (e.g., for Elasticsearch, Honeycomb, SigNoz, etc.) to the evlog package. Covers source code, build config, package exports, tests, e2e, and all documentation. |
| metadata | {"internal":true} |
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
feat({name}): add the {Name} drain adapter
Recent examples: feat(loki): add the Grafana Loki drain adapter, feat(clickhouse): add the ClickHouse drain adapter. Use the adapter name as the conventional-commit scope, and register that scope (see touchpoint 11).
Scope timing caveat: the semantic PR check reads its scope list from the base branch, so a brand-new scope can't validate the very PR that introduces it. Either register the scope in a small preceding PR (the Loki/ClickHouse pattern), or use an unscoped title (feat: add the {Name} drain adapter) on the introducing PR.
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 unit tests (use test/helpers/fetch.ts) |
| 5 | packages/evlog/test/e2e/{name}.e2e.ts | Create e2e test gated on env vars; extend the docker sandbox if self-hostable |
| 6 | packages/evlog/test/toolkit/__snapshots__/api-surface.test.ts.snap | Regenerated by running the tests after a build (pnpm run build then pnpm test) |
| 7 | apps/docs/content/4.integrate/adapters/{category}/{NN}.{name}.md | Create adapter doc page in the right category |
| 8 | apps/docs/content/4.integrate/adapters/01.overview.md | Add adapter to overview (frontmatter link + card) |
| 9 | skills/review-logging-patterns/SKILL.md | Add adapter row in the Drain Adapters table + frontmatter description |
| 10 | .changeset/{name}-adapter.md | Create changeset (minor) describing the adapter |
| 11 | .github/workflows/semantic-pull-request.yml + .github/pull_request_template.md | Register {name} as a PR scope in both files |
Important: Do NOT consider the task complete until all 11 touchpoints have been addressed.
Naming Conventions
Use these placeholders consistently:
| Placeholder | Example (Loki) | Usage |
|---|
{name} | loki | File names, import paths, env var suffix, PR scope |
{Name} | Loki | PascalCase in function/interface names |
{NAME} | LOKI | 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 |
| Retry attempts on transient failures | retries |
If a service historically used a different name (token, sourceToken, …) keep it as a deprecated alias via applyDeprecatedAlias. 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. loki.ts and clickhouse.ts are the most recent reference implementations.
The contract is defineHttpDrain<TConfig>({ name, label, resolve, encode }). You only ship two pieces of logic:
resolve(): produce a fully-resolved config or null to skip. Use resolveAdapterConfig for the standard precedence (overrides → runtimeConfig.evlog.{name} → runtimeConfig.{name} → env vars). List NUXT_{NAME}_* before {NAME}_* in ConfigField.env for silent Nuxt compat; show only {NAME}_* in user-facing messages via formatPublicEnvKeys.
encode(events, config): a private encode{Name}Request(events, config): HttpDrainRequest returning { url, headers, body } for a batch. HTTP transport, identity headers, retries, timeout, and error logging are handled by defineHttpDrain (via httpPost).
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 a mode option (see PostHog).
- No HTTP code in the adapter. Never call
fetch directly. If the service truly needs custom transport (binary envelopes, non-HTTP), use defineDrain from ../shared/drain instead, see fs.ts and memory.ts.
- Encode parity. The standalone
sendTo{Name} / sendBatchTo{Name} helpers must reuse the same private encode{Name}Request() and go through sendEncodedDrainRequest(request, { label, source, timeout, retries }), never a separate fetch path. test/adapters/encode-parity.test.ts pins this for a subset of adapters; add the new one to it (not every existing adapter is registered there yet, and that is a gap, not a license to skip).
- No bespoke config resolution. Always go through
resolveAdapterConfig. Deprecated aliases (token → apiKey) go through applyDeprecatedAlias.
- Exported converters. If the service needs a specific event shape, export
to{Name}Event() / build{Name}Payload() helpers so they're testable independently.
- Edge-safe. Adapters run on Cloudflare Workers: no
Buffer (use TextEncoder + btoa, see loki.ts), no Node-only APIs. fs.ts shows the isEdgeRuntime() guard pattern when a runtime genuinely can't be supported.
Step 2: Build Config
Add a build entry in packages/evlog/tsdown.config.ts alongside the existing adapters:
'adapters/{name}': 'src/adapters/{name}.ts',
Follow the existing ordering in that file.
Step 3: Package Exports
In packages/evlog/package.json, add two entries (after the last adapter, and check the current list rather than assuming):
In exports:
"./{name}": {
"types": "./dist/adapters/{name}.d.mts",
"import": "./dist/adapters/{name}.mjs"
}
In typesVersions["*"]:
"{name}": [
"./dist/adapters/{name}.d.mts"
]
Any export added to package.json without a matching tsdown.config.ts entry (and vice versa) fails test/toolkit/api-surface.test.ts, which is touchpoint 6.
Step 4: Unit Tests
Create packages/evlog/test/adapters/{name}.test.ts. Read references/test-template.md for the full annotated template, and packages/evlog/test/README.md for the repo-wide conventions.
Non-negotiables from the test README:
- Use
mockFetch() / getFetchCall / getFetchJson / getFetchHeaders from test/helpers/fetch.ts, never hand-roll vi.spyOn(globalThis, 'fetch') boilerplate.
- Clean up any env vars the adapter reads in
afterEach.
- Test the exported pure helpers (
to{Name}Event, build{Name}Payload, URL resolvers) directly, one describe per helper.
Required test categories:
- URL construction (default + custom endpoint, trailing-slash tolerance)
- 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 (multiple events in one request, empty batch skips fetch)
- Deprecated alias still works (when applicable)
- Add the adapter to
test/adapters/encode-parity.test.ts
Step 5: E2E Test + Sandbox
Create packages/evlog/test/e2e/{name}.e2e.ts, gated on the adapter's env vars (skipped when absent). Run with pnpm test:e2e.
If the service is self-hostable, extend the local sandbox so the adapter can be exercised without cloud credentials:
packages/evlog/test/e2e/docker-compose.yml: add the service
packages/evlog/test/e2e/seed.mjs: fan the seeder out to the new backend
packages/evlog/test/e2e/README.md: document it
- Root
package.json sandbox:e2e script: add the local env var if needed
See the Loki and ClickHouse setups as references.
Step 6: Adapter Documentation Page
Read apps/docs/AGENTS.md before touching anything under apps/docs/ (steps 6 to 8).
Adapter docs live in three categories under apps/docs/content/4.integrate/adapters/:
| Category | Directory | Examples |
|---|
| Cloud (SaaS only) | cloud/ | Axiom, PostHog, Sentry, Better Stack, Datadog |
| Cloud or Self-Hosted | hybrid/ | Loki, ClickHouse, OTLP, HyperDX |
| Self-Hosted (local only) | self-hosted/ | FS, NuxtHub, Memory |
Create {NN}.{name}.md in the right category with the next available number. Use the Loki page (hybrid/01.loki.md) as a reference for frontmatter, 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 the supported frameworks (Nuxt/Nitro, Hono, Express, Fastify, Elysia, NestJS, Standalone). Do not only show Nitro examples.
Step 7: Update Adapters Overview Page
Edit apps/docs/content/4.integrate/adapters/01.overview.md in two places (follow the pattern of existing adapters):
- Frontmatter
links array: add a link entry with icon and /integrate/adapters/{category}/{name} path, in category order
::card-group section: add a card block in the matching position
Step 8: Update the Public Skill
In skills/review-logging-patterns/SKILL.md (published on evlog.dev via /.well-known/skills/):
- Add a row to the Drain Adapters table:
| {Name} | evlog/{name} | {NAME}_API_KEY, ... |
- Add the adapter name to the
description: line in the YAML frontmatter
Step 9: Changeset
Create .changeset/{name}-adapter.md with a minor bump. Write it like a release note: what the adapter does, the deployment modes it covers, the key options, the env vars, and the direct-send helpers. See .changeset entries from the Loki/ClickHouse PRs for the expected depth.
Step 10: PR Scopes
Add {name} to the scopes list in .github/workflows/semantic-pull-request.yml and to the Scopes section of .github/pull_request_template.md, in alphabetical order. Remember the timing caveat from the PR Title section: this registration only takes effect for PRs whose base branch already contains it.
Verification
After a clean install, prepare generated workspace types from the repo root first, then run the package checks:
pnpm run dev:prepare
cd packages/evlog
pnpm run lint
pnpm run typecheck
pnpm run build
pnpm run test