generated from HugoRCD/nuxt-module-starter
-
-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add /create-adapter agent skill
#49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
+517
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| --- | ||
| 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 Datadog, Sentry, Loki, Elasticsearch, etc.) to the evlog package. Covers source code, build config, package exports, tests, and documentation. | ||
| --- | ||
HugoRCD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Create evlog Adapter | ||
|
|
||
| Add a new built-in adapter to evlog. Every adapter follows the same architecture. This skill walks through all 5 touchpoints. | ||
|
|
||
| ## Touchpoints Checklist | ||
|
|
||
| | # | File | Action | | ||
| |---|------|--------| | ||
| | 1 | `packages/evlog/src/adapters/{name}.ts` | Create adapter source | | ||
| | 2 | `packages/evlog/build.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/3.adapters/{n}.{name}.md` | Create doc page (before `custom.md`) | | ||
|
|
||
| After all 5 steps, update `AGENTS.md` to list the new adapter in the adapters table. | ||
|
|
||
| ## 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 | | ||
|
|
||
| ## Step 1: Adapter Source | ||
|
|
||
| Create `packages/evlog/src/adapters/{name}.ts`. | ||
|
|
||
| Read [references/adapter-template.md](references/adapter-template.md) for the full annotated template. | ||
|
|
||
| Key architecture rules: | ||
|
|
||
| 1. **Config interface** -- service-specific fields (API key, endpoint, etc.) plus optional `timeout?: number` | ||
| 2. **`getRuntimeConfig()` helper** -- dynamic `require('nitropack/runtime')` wrapped in try/catch | ||
| 3. **Config priority** (highest to lowest): | ||
| - Overrides passed to `create{Name}Drain()` | ||
| - `runtimeConfig.evlog.{name}` | ||
| - `runtimeConfig.{name}` | ||
| - Environment variables: `NUXT_{NAME}_*` then `{NAME}_*` | ||
| 4. **Factory function** -- `create{Name}Drain(overrides?: Partial<Config>)` returns `(ctx: DrainContext) => Promise<void>` | ||
| 5. **Exported send functions** -- `sendTo{Name}(event, config)` and `sendBatchTo{Name}(events, config)` for direct use and testability | ||
| 6. **Error handling** -- try/catch with `console.error('[evlog/{name}] ...')`, never throw from the drain | ||
| 7. **Timeout** -- `AbortController` with 5000ms default, configurable via `config.timeout` | ||
| 8. **Event transformation** -- if the service needs a specific format, export a `to{Name}Event()` converter | ||
|
|
||
| ## Step 2: Build Config | ||
|
|
||
| Add a build entry in `packages/evlog/build.config.ts` alongside the existing adapters: | ||
|
|
||
| ```typescript | ||
| { input: 'src/adapters/{name}', name: 'adapters/{name}' }, | ||
| ``` | ||
|
|
||
| Place it after the last adapter entry (currently `posthog` at line ~21). | ||
|
|
||
| ## Step 3: Package Exports | ||
|
|
||
| In `packages/evlog/package.json`, add two entries: | ||
|
|
||
| **In `exports`** (after the last adapter, currently `./posthog`): | ||
|
|
||
| ```json | ||
| "./{name}": { | ||
| "types": "./dist/adapters/{name}.d.mts", | ||
| "import": "./dist/adapters/{name}.mjs" | ||
| } | ||
| ``` | ||
|
|
||
| **In `typesVersions["*"]`** (after the last adapter): | ||
|
|
||
| ```json | ||
| "{name}": [ | ||
| "./dist/adapters/{name}.d.mts" | ||
| ] | ||
| ``` | ||
|
|
||
| ## Step 4: Tests | ||
|
|
||
| Create `packages/evlog/test/adapters/{name}.test.ts`. | ||
|
|
||
| Read [references/test-template.md](references/test-template.md) for the full annotated template. | ||
|
|
||
| Required test categories: | ||
|
|
||
| 1. URL construction (default + custom endpoint) | ||
| 2. Headers (auth, content-type, service-specific) | ||
| 3. Request body format (JSON structure matches service API) | ||
| 4. Error handling (non-OK responses throw with status) | ||
| 5. Batch operations (`sendBatchTo{Name}`) | ||
| 6. Timeout handling (default 5000ms + custom) | ||
|
|
||
| ## Step 5: Documentation | ||
|
|
||
| Create `apps/docs/content/3.adapters/{n}.{name}.md` where `{n}` is the next number before `custom.md` (custom should always be last). | ||
|
|
||
| Use this frontmatter structure: | ||
|
|
||
| ```yaml | ||
| --- | ||
| title: "{Name} Adapter" | ||
| description: "Send logs to {Name} for [value prop]. Zero-config setup with environment variables." | ||
| navigation: | ||
| title: "{Name}" | ||
| icon: i-simple-icons-{name} # or i-lucide-* for generic | ||
| links: | ||
| - label: "{Name} Dashboard" | ||
| icon: i-lucide-external-link | ||
| to: https://{service-url} | ||
| target: _blank | ||
| color: neutral | ||
| variant: subtle | ||
| - label: "OTLP Adapter" | ||
| icon: i-simple-icons-opentelemetry | ||
| to: /adapters/otlp | ||
| color: neutral | ||
| variant: subtle | ||
| --- | ||
| ``` | ||
|
|
||
| Sections to include: | ||
|
|
||
| 1. **Intro paragraph** -- what the service is and what the adapter does | ||
| 2. **Installation** -- import path `evlog/{name}` | ||
| 3. **Quick Setup** -- Nitro plugin with `create{Name}Drain()` | ||
| 4. **Configuration** -- table of env vars and config options | ||
| 5. **Configuration Priority** -- overrides > runtimeConfig > env vars | ||
| 6. **Advanced** -- custom options, event transformation details | ||
| 7. **Querying/Using** -- how to find evlog events in the target service | ||
|
|
||
| Renumber `custom.md` if needed so it stays last. | ||
|
|
||
| ## Final Step: Update AGENTS.md | ||
|
|
||
| Add the new adapter to the adapters table in the root `AGENTS.md` file, in the "Log Draining & Adapters" section: | ||
|
|
||
| ```markdown | ||
| | {Name} | `evlog/{name}` | Send logs to {Name} for [description] | | ||
| ``` | ||
|
|
||
| ## Verification | ||
|
|
||
| After completing all steps, run: | ||
|
|
||
| ```bash | ||
| cd packages/evlog | ||
| bun run build # Verify build succeeds with new entry | ||
| bun run test # Verify tests pass | ||
| ``` | ||
177 changes: 177 additions & 0 deletions
177
.agents/skills/create-adapter/references/adapter-template.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| # Adapter Source Template | ||
|
|
||
| Complete TypeScript template for `packages/evlog/src/adapters/{name}.ts`. | ||
|
|
||
| Replace `{Name}`, `{name}`, and `{NAME}` with the actual service name. | ||
|
|
||
| ```typescript | ||
| import type { DrainContext, WideEvent } from '../types' | ||
|
|
||
| // --- 1. Config Interface --- | ||
| // Define all service-specific configuration fields. | ||
| // Always include optional `timeout`. | ||
| export interface {Name}Config { | ||
| /** {Name} API key / token */ | ||
| apiKey: string | ||
| /** {Name} API endpoint. Default: https://api.{name}.com */ | ||
| endpoint?: string | ||
| /** Request timeout in milliseconds. Default: 5000 */ | ||
| timeout?: number | ||
| // Add service-specific fields here (dataset, project, region, etc.) | ||
| } | ||
|
|
||
| // --- 2. Event Transformation (optional) --- | ||
| // Export a converter if the service needs a specific format. | ||
| // This makes the transformation testable independently. | ||
|
|
||
| /** {Name} event structure */ | ||
| export interface {Name}Event { | ||
| // Define the target service's event shape | ||
| timestamp: string | ||
| level: string | ||
| data: Record<string, unknown> | ||
| } | ||
|
|
||
| /** | ||
| * Convert a WideEvent to {Name}'s event format. | ||
| */ | ||
| export function to{Name}Event(event: WideEvent): {Name}Event { | ||
| const { timestamp, level, ...rest } = event | ||
|
|
||
| return { | ||
| timestamp, | ||
| level, | ||
| data: rest, | ||
| } | ||
| } | ||
|
|
||
| // --- 3. Runtime Config Helper --- | ||
| // Dynamic require to avoid bundling issues outside Nitro. | ||
| // Returns undefined when not in a Nitro context. | ||
| function getRuntimeConfig(): { | ||
| evlog?: { {name}?: Partial<{Name}Config> } | ||
| {name}?: Partial<{Name}Config> | ||
| } | undefined { | ||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const { useRuntimeConfig } = require('nitropack/runtime') | ||
| return useRuntimeConfig() | ||
| } catch { | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| // --- 4. Factory Function --- | ||
| // Returns a drain function that resolves config at call time. | ||
| // Config priority: overrides > runtimeConfig.evlog.{name} > runtimeConfig.{name} > env vars | ||
|
|
||
| /** | ||
| * Create a drain function for sending logs to {Name}. | ||
| * | ||
| * Configuration priority (highest to lowest): | ||
| * 1. Overrides passed to create{Name}Drain() | ||
| * 2. runtimeConfig.evlog.{name} | ||
| * 3. runtimeConfig.{name} | ||
| * 4. Environment variables: NUXT_{NAME}_*, {NAME}_* | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // Zero config - set NUXT_{NAME}_API_KEY env var | ||
| * nitroApp.hooks.hook('evlog:drain', create{Name}Drain()) | ||
| * | ||
| * // With overrides | ||
| * nitroApp.hooks.hook('evlog:drain', create{Name}Drain({ | ||
| * apiKey: 'my-key', | ||
| * })) | ||
| * ``` | ||
| */ | ||
| export function create{Name}Drain(overrides?: Partial<{Name}Config>): (ctx: DrainContext) => Promise<void> { | ||
| return async (ctx: DrainContext) => { | ||
| const runtimeConfig = getRuntimeConfig() | ||
| const evlogConfig = runtimeConfig?.evlog?.{name} | ||
| const rootConfig = runtimeConfig?.{name} | ||
|
|
||
| // Build config with fallbacks | ||
| const config: Partial<{Name}Config> = { | ||
| apiKey: overrides?.apiKey ?? evlogConfig?.apiKey ?? rootConfig?.apiKey | ||
| ?? process.env.NUXT_{NAME}_API_KEY ?? process.env.{NAME}_API_KEY, | ||
| endpoint: overrides?.endpoint ?? evlogConfig?.endpoint ?? rootConfig?.endpoint | ||
| ?? process.env.NUXT_{NAME}_ENDPOINT ?? process.env.{NAME}_ENDPOINT, | ||
| timeout: overrides?.timeout ?? evlogConfig?.timeout ?? rootConfig?.timeout, | ||
| } | ||
|
|
||
| // Validate required fields | ||
| if (!config.apiKey) { | ||
| console.error('[evlog/{name}] Missing apiKey. Set NUXT_{NAME}_API_KEY env var or pass to create{Name}Drain()') | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| await sendTo{Name}(ctx.event, config as {Name}Config) | ||
| } catch (error) { | ||
| console.error('[evlog/{name}] Failed to send event:', error) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // --- 5. Send Functions --- | ||
| // Exported for direct use and testability. | ||
| // sendTo{Name} wraps sendBatchTo{Name} for single events. | ||
|
|
||
| /** | ||
| * Send a single event to {Name}. | ||
| */ | ||
| export async function sendTo{Name}(event: WideEvent, config: {Name}Config): Promise<void> { | ||
| await sendBatchTo{Name}([event], config) | ||
| } | ||
|
|
||
| /** | ||
| * Send a batch of events to {Name}. | ||
| */ | ||
| export async function sendBatchTo{Name}(events: WideEvent[], config: {Name}Config): Promise<void> { | ||
| if (events.length === 0) return | ||
|
|
||
| const endpoint = (config.endpoint ?? 'https://api.{name}.com').replace(/\/$/, '') | ||
| const timeout = config.timeout ?? 5000 | ||
| // Construct the full URL for the service's ingest API | ||
| const url = `${endpoint}/v1/ingest` | ||
|
|
||
| const headers: Record<string, string> = { | ||
| 'Content-Type': 'application/json', | ||
| 'Authorization': `Bearer ${config.apiKey}`, | ||
| // Add service-specific headers here | ||
| } | ||
|
|
||
| // Transform events if the service needs a specific format | ||
| const payload = events.map(to{Name}Event) | ||
| // Or send raw: JSON.stringify(events) | ||
|
|
||
| const controller = new AbortController() | ||
| const timeoutId = setTimeout(() => controller.abort(), timeout) | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify(payload), | ||
| signal: controller.signal, | ||
| }) | ||
|
|
||
| if (!response.ok) { | ||
| const text = await response.text().catch(() => 'Unknown error') | ||
| const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text | ||
| throw new Error(`{Name} API error: ${response.status} ${response.statusText} - ${safeText}`) | ||
| } | ||
| } finally { | ||
| clearTimeout(timeoutId) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Customization Notes | ||
|
|
||
| - **Auth style**: Some services use `Authorization: Bearer`, others use a custom header like `X-API-Key`. Adjust the headers accordingly. | ||
| - **Payload format**: Some services accept raw JSON arrays (Axiom), others need a wrapper object (PostHog `{ api_key, batch }`), others need a protocol-specific structure (OTLP). Adapt `sendBatchTo{Name}` to match. | ||
| - **Event transformation**: If the service expects a specific schema, implement `to{Name}Event()`. If the service accepts arbitrary JSON, you can skip it and send `ctx.event` directly. | ||
| - **URL construction**: Match the service's API endpoint pattern. Some use path-based routing (`/v1/datasets/{id}/ingest`), others use a flat endpoint (`/batch/`). | ||
| - **Extra config fields**: Add service-specific fields to the config interface (e.g., `dataset` for Axiom, `orgId` for org-scoped APIs, `host` for region selection). |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.