create-evlog-adapter

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.

hugorcd/evlog818 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
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.
license: MIT
---

# 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](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:

1. **`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`.
2. **`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:

```typescript
'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`**:

```json
"./{name}": {
  "types": "./dist/adapters/{name}.d.mts",
  "import": "./dist/adapters/{name}.mjs"
}
```

**In `typesVersions["*"]`**:

```json
"{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](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:

1. URL construction (default + custom endpoint, trailing-slash tolerance)
2. Headers (auth, content-type, service-specific)
3. Request body format (JSON structure matches service API)
4. Skip behavior when `apiKey` (or required field) is missing
5. Batch operations (multiple events in one request, empty batch skips fetch)
6. Deprecated alias still works (when applicable)
7. 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):

1. **Frontmatter `links` array**: add a link entry with icon and `/integrate/adapters/{category}/{name}` path, in category order
2. **`::card-group` section**: 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/`):

1. Add a row to the **Drain Adapters** table: `| {Name} | evlog/{name} | {NAME}_API_KEY, ... |`
2. 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:

```bash
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
```

More Testing skills

← All Testing skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY