@hugorcd/create-adapter
Logging that makes sense. Wide events, structured errors, zero chaos.
| 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. |
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 | apps/docs/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 ornullto skip. UseresolveAdapterConfigfor the standard precedence (overrides →runtimeConfig.evlog.{name}→runtimeConfig.{name}→ env vars). ListNUXT_{NAME}_*before{NAME}_*inConfigField.envfor silent Nuxt compat; show only{NAME}_*in user-facing messages viaformatPublicEnvKeys.encode(events, config): a privateencode{Name}Request(events, config): HttpDrainRequestreturning{ url, headers, body }for a batch. HTTP transport, identity headers, retries, timeout, and error logging are handled bydefineHttpDrain(viahttpPost).
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. Never call
fetchdirectly. If the service truly needs custom transport (binary envelopes, non-HTTP), usedefineDrainfrom../shared/draininstead, seefs.tsandmemory.ts. - Encode parity. The standalone
sendTo{Name}/sendBatchTo{Name}helpers must reuse the same privateencode{Name}Request()and go throughsendEncodedDrainRequest(request, { label, source, timeout, retries }), never a separate fetch path.test/adapters/encode-parity.test.tspins 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 throughapplyDeprecatedAlias. - 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(useTextEncoder+btoa, seeloki.ts), no Node-only APIs.fs.tsshows theisEdgeRuntime()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/getFetchHeadersfromtest/helpers/fetch.ts, never hand-rollvi.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, onedescribeper 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 servicepackages/evlog/test/e2e/seed.mjs: fan the seeder out to the new backendpackages/evlog/test/e2e/README.md: document it- Root
package.jsonsandbox:e2escript: 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
linksarray: add a link entry with icon and/integrate/adapters/{category}/{name}path, in category order ::card-groupsection: add a card block in the matching position
Step 8: Update the Public Skill
In apps/docs/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 # required before test — api-surface snapshot is gated on dist/
pnpm run test
Loading...
Select a file to preview
Analyzing security...
Checking scan reports and verification data.
Bill of Materials
Everything this skill can do — files, network, commands, and more.