Better Auth
secondsky/claude-skills
Skill for integrating Better Auth - comprehensive TypeScript authentication framework for Cloudflare D1, Next.js, Nuxt, and 15+ frameworks.
Review code for logging patterns and suggest evlog adoption.
$ npx skills add activepieces/activepieces --skill review-logging-patterns -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install activepieces/activepieces review-logging-patterns --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/activepieces/activepieces.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/review-logging-patterns .claude/skills/review-logging-patterns && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "review-logging-patterns" agent skill from https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patterns into .claude/skills/review-logging-patterns/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "review-logging-patterns", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patternsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add activepieces/activepieces --skill review-logging-patterns -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install activepieces/activepieces review-logging-patterns --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/activepieces/activepieces.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/review-logging-patterns .agents/skills/review-logging-patterns && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "review-logging-patterns" agent skill from https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patterns into .agents/skills/review-logging-patterns/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "review-logging-patterns", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add activepieces/activepieces --skill review-logging-patterns -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install activepieces/activepieces review-logging-patterns --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/activepieces/activepieces.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/review-logging-patterns .cursor/skills/review-logging-patterns && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "review-logging-patterns" agent skill from https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patterns into .cursor/skills/review-logging-patterns/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "review-logging-patterns", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/activepieces/activepieces.git --path .agents/skills/review-logging-patterns--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add activepieces/activepieces --skill review-logging-patterns -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install activepieces/activepieces review-logging-patterns --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/activepieces/activepieces.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/review-logging-patterns .gemini/skills/review-logging-patterns && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "review-logging-patterns" agent skill from https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patterns into .gemini/skills/review-logging-patterns/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "review-logging-patterns", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install activepieces/activepieces review-logging-patternsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add activepieces/activepieces --skill review-logging-patterns -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/activepieces/activepieces.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/review-logging-patterns .github/skills/review-logging-patterns && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "review-logging-patterns" agent skill from https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patterns into .github/skills/review-logging-patterns/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "review-logging-patterns", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add activepieces/activepieces --skill review-logging-patterns -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install activepieces/activepieces review-logging-patterns --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/activepieces/activepieces.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/review-logging-patterns .opencode/skills/review-logging-patterns && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "review-logging-patterns" agent skill from https://github.com/activepieces/activepieces/tree/main/.agents/skills/review-logging-patterns into .opencode/skills/review-logging-patterns/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "review-logging-patterns", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
review-logging-patternsReview code for logging patterns and suggest evlog adoption.
Review Logging Patterns is an agent skill from activepieces/activepieces. Review code for logging patterns and suggest evlog adoption. Guides setup on Nuxt, Next.js, SvelteKit, Nitro, TanStack Start, React Router, NestJS, Express, Hono, Fastify, Elysia, oRPC, Cloudflare Workers, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack, Datadog), sampling, enrichers, and AI SDK integration (token usage, tool calls, streaming metrics, telemetry…
Its SKILL.md is about 8.2k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including reference files (for example `references/code-review.md`, `references/drain-pipeline.md` and `references/structured-errors.md`).
It sits in AI & LLM Engineering, covering Code review, Embeddings and MCP servers. It works with Next.js, Nuxt, SvelteKit and TypeScript. The repository describes itself as: AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents. The licence is MIT.
Read from SKILL.md and the folder at commit 8656fb8. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
npmFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
in-otel.hyperdx.ioAlso links to:
evlog.devFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
AXIOM_API_KEYHYPERDX_API_KEYPOSTHOG_API_KEYBETTER_STACK_SOURCE_TOKENDD_API_KEYDATADOG_API_KEYNUXT_AXIOM_API_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Review Logging Patterns loads about 8.2k tokens when it runs, and up to ~16k if it reads all its reference files. Until then it costs about 148 tokens; SKILL.md has 1,416 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from activepieces/activepieces at commit 8656fb8, republished under its MIT licence (© activepieces). 1,416 words, ~8,248 tokens.
.claude/skills/review-logging-patterns/SKILL.md (or your agent's skills folder). This skill also uses 4 other files; get the full folder from GitHub.Review and improve logging patterns in TypeScript/JavaScript codebases. Transform scattered console.logs into structured wide events and convert generic errors into self-documenting structured errors.
| Working on... | Resource |
|---|---|
| Wide events patterns | references/wide-events.md |
| Error handling | references/structured-errors.md |
| Code review checklist | references/code-review.md |
| Drain pipeline | references/drain-pipeline.md |
| Audit logs | build-audit-logs skill + docs |
For security-sensitive actions (auth, billing, admin, data export), use evlog's audit layer — a typed audit field on wide events, not a parallel logger. See the build-audit-logs skill for end-to-end setup (log.audit, withAudit, denials, auditEnricher, auditOnly, signed, mockAudit).
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
})Docs: https://www.evlog.dev/use-cases/audit/overview
npm install evlog// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
include: ['/api/**'],
},
})All evlog functions (useLogger, createError, parseError, log) are auto-imported — no import statements needed.
// server/api/checkout.post.ts — no imports needed
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ user: { id: user.id, plan: user.plan } })
return { success: true }
})Drain, enrich, and tail sampling use Nitro hooks in server plugins:
// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})Client transport (auto-configured Vue plugin):
// nuxt.config.ts
evlog: {
transport: { enabled: true }, // logs sent to /api/_evlog/ingest
}Client-side: log, setIdentity, clearIdentity are auto-imported in components.
Step 1: Create central config — all exports come from here:
// lib/evlog.ts
import type { DrainContext } from 'evlog'
import { createEvlog } from 'evlog/next'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'
import { createDrainPipeline } from 'evlog/pipeline'
const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50, intervalMs: 5000 } })
const drain = pipeline(createAxiomDrain({ dataset: 'logs', apiKey: process.env.AXIOM_API_KEY! }))
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
sampling: {
rates: { info: 10 },
keep: [{ status: 400 }, { duration: 1000 }],
},
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/checkout/**': { service: 'checkout-service' },
},
keep: (ctx) => {
const user = ctx.context.user as { premium?: boolean } | undefined
if (user?.premium) ctx.shouldKeep = true
},
enrich: (ctx) => {
for (const enricher of enrichers) enricher(ctx)
},
drain,
})Step 2: Wrap route handlers with withEvlog():
// app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
export const POST = withEvlog(async (request: Request) => {
const log = useLogger() // Zero arguments — uses AsyncLocalStorage
log.set({ user: { id: 'user_123', plan: 'enterprise' } })
log.set({ cart: { items: 3, total: 14999 } })
return Response.json({ success: true })
})Step 3: Server Actions — same withEvlog() wrapper:
// app/actions.ts
'use server'
import { withEvlog, useLogger } from '@/lib/evlog'
export const checkout = withEvlog(async (formData: FormData) => {
const log = useLogger()
log.set({ action: 'checkout', source: 'server-action' })
return { success: true }
})Step 4: Middleware (optional — sets x-request-id + timing headers):
// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }Step 5: Client Provider — wrap root layout:
// app/layout.tsx
import { EvlogProvider } from 'evlog/next/client'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<EvlogProvider service="my-app" transport={{ enabled: true, endpoint: '/api/evlog/ingest' }}>
{children}
</EvlogProvider>
</body>
</html>
)
}Step 6: Client logging — in any client component:
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'
setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()Step 7 (optional): Instrumentation — startup + global onRequestError (SSR/RSC errors outside withEvlog). Use defineNodeInstrumentation(() => import('./lib/evlog')) in root instrumentation.ts to gate Node + cache the import, or write register/onRequestError manually — both are valid. For custom logic, wrap evlog’s register/onRequestError inside lib/evlog.ts (compose with your own init or metrics), then re-export.
Export createInstrumentation() from lib/evlog.ts alongside createEvlog(). See framework docs for coexistence with lockLogger.
Step 8: Client ingest endpoint — receives client logs:
// app/api/evlog/ingest/route.ts
import { NextRequest } from 'next/server'
const VALID_LEVELS = ['info', 'error', 'warn', 'debug'] as const
export async function POST(request: NextRequest) {
const origin = request.headers.get('origin')
const host = request.headers.get('host')
if (origin && new URL(origin).host !== host) {
return Response.json({ error: 'Invalid origin' }, { status: 403 })
}
const body = await request.json()
if (!body?.timestamp || !body?.level || !VALID_LEVELS.includes(body.level)) {
return Response.json({ error: 'Invalid payload' }, { status: 400 })
}
const { service: _, ...sanitized } = body
console.log('[CLIENT LOG]', JSON.stringify({ ...sanitized, service: 'my-app', source: 'client' }))
return new Response(null, { status: 204 })
}// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'
initLogger({ env: { service: 'my-app' } })
export const { handle, handleError } = createEvlogHooks()Access the logger via event.locals.log in route handlers or useLogger() from anywhere in the call stack:
// src/routes/api/users/[id]/+server.ts
import { json } from '@sveltejs/kit'
export const GET = ({ locals, params }) => {
locals.log.set({ user: { id: params.id } })
return json({ id: params.id })
}import { useLogger } from 'evlog/sveltekit'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
export const { handle, handleError } = createEvlogHooks({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})// routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
export default defineHandler(async (event) => {
const log = useLogger(event)
log.set({ action: 'checkout' })
return { ok: true }
})TanStack Start uses Nitro v3. Install evlog and add a nitro.config.ts:
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
experimental: { asyncContext: true },
modules: [evlog({ env: { service: 'my-app' } })],
})Add the error handling middleware to __root.tsx:
// src/routes/__root.tsx
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'
export const Route = createRootRoute({
server: {
middleware: [createMiddleware().server(evlogErrorHandler)],
},
})Use useRequest() from nitro/context to access the logger:
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'
const req = useRequest()
const log = req.context.log as RequestLogger
log.set({ user: { id: 'user_123' } })// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})Import useLogger from evlog/nitro in routes.
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [EvlogModule.forRoot()],
})
export class AppModule {}EvlogModule.forRoot() registers a global middleware. Use useLogger() to access the request-scoped logger from any controller or service:
import { useLogger } from 'evlog/nestjs'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})For async configuration with NestJS DI, use forRootAsync():
EvlogModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config) => ({
drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
}),
})import express from 'express'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/express'
initLogger({ env: { service: 'my-api' } })
const app = express()
app.use(evlog())
app.get('/api/users', (req, res) => {
req.log.set({ users: { count: 42 } })
res.json({ users: [] })
})Use useLogger() to access the logger from anywhere in the call stack without passing req:
import { useLogger } from 'evlog/express'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
initLogger({ env: { service: 'my-api' } })
const app = new Hono<EvlogVariables>()
app.use(evlog())
app.get('/api/users', (c) => {
const log = c.get('log')
log.set({ users: { count: 42 } })
return c.json({ users: [] })
})Access the logger via c.get('log') in handlers. No useLogger() — use c.get('log') and pass it down explicitly, or use Express/Fastify/Elysia if you need useLogger() across async boundaries.
Structured errors: throw createError(), then in app.onError use parseError() and pass parsed.status as ContentfulStatusCode to c.json() (Hono types the status argument as ContentfulStatusCode, not number).
import { createError, parseError } from 'evlog'
import type { ContentfulStatusCode } from 'hono/utils/http-status'
app.onError((error, c) => {
c.get('log').error(error)
const parsed = parseError(error)
return c.json(
{ message: parsed.message, why: parsed.why, fix: parsed.fix, link: parsed.link },
parsed.status as ContentfulStatusCode,
)
})Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/fastify'
initLogger({ env: { service: 'my-api' } })
const app = Fastify({ logger: false })
await app.register(evlog)
app.get('/api/users', async (request) => {
request.log.set({ users: { count: 42 } })
return { users: [] }
})request.log is the evlog wide-event logger (shadows Fastify's built-in pino logger on the request). Fastify's pino logger remains accessible via fastify.log.
Use useLogger() to access the logger from anywhere in the call stack without passing request:
import { useLogger } from 'evlog/fastify'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, {
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/elysia'
initLogger({ env: { service: 'my-api' } })
const app = new Elysia()
.use(evlog())
.get('/api/users', ({ log }) => {
log.set({ users: { count: 42 } })
return { users: [] }
})
.listen(3000)Use useLogger() to access the logger from anywhere in the call stack:
import { useLogger } from 'evlog/elysia'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))// react-router.config.ts
import type { Config } from '@react-router/dev/config'
export default {
future: {
v8_middleware: true,
},
} satisfies Config// app/root.tsx
import { initLogger } from 'evlog'
import { evlog } from 'evlog/react-router'
initLogger({ env: { service: 'my-api' } })
export const middleware: Route.MiddlewareFunction[] = [
evlog(),
]Access the logger via context.get(loggerContext) in loaders and actions:
// app/routes/api.users.$id.tsx
import { loggerContext } from 'evlog/react-router'
export async function loader({ params, context }: Route.LoaderArgs) {
const log = context.get(loggerContext)
log.set({ user: { id: params.id } })
return { users: [] }
}Use useLogger() to access the logger from anywhere in the call stack without passing context:
import { useLogger } from 'evlog/react-router'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
export const middleware: Route.MiddlewareFunction[] = [
evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}),
]import { os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { initLogger } from 'evlog'
import { evlog, withEvlog, type EvlogOrpcContext } from 'evlog/orpc'
initLogger({ env: { service: 'my-rpc' } })
const base = os.$context<EvlogOrpcContext>().use(evlog())
const router = {
ping: base.handler(({ context }) => {
context.log.set({ pinged: true })
return { ok: true }
}),
}
const handler = withEvlog(new RPCHandler(router))
export default async function fetch(request: Request) {
const { matched, response } = await handler.handle(request, { prefix: '/rpc' })
return matched ? response : new Response('Not Found', { status: 404 })
}withEvlog() wraps the handler so each matched request emits one wide event; os.use(evlog()) exposes context.log on every procedure that descends from base and tags the wide event with operation (the procedure path joined with .).
Use useLogger() to access the logger from utility modules:
import { useLogger } from 'evlog/orpc'
async function chargeCard(amount: number) {
const log = useLogger()
log.set({ payment: { amount } })
}Full pipeline with drain, enrich, and tail sampling:
import { createAxiomDrain } from 'evlog/axiom'
const handler = withEvlog(new RPCHandler(router), {
include: ['/rpc/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
initWorkersLogger({ env: { service: 'edge-api' } })
export default {
async fetch(request: Request) {
const log = createWorkersLogger(request)
try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
},
}For any Vite-based project (SvelteKit, Astro, SolidStart, React+Vite, etc.), use the Vite plugin for auto-init, auto-imports, and build-time features:
// vite.config.ts
import evlog from 'evlog/vite'
export default defineConfig({
plugins: [
evlog({
service: 'my-app',
autoImports: true, // auto-import log, createEvlogError, parseError
strip: ['debug'], // remove log.debug() in production
sourceLocation: true, // inject file:line in dev + prod
client: { // client-side logging
transport: { endpoint: '/api/logs' },
},
}),
],
})Server-side middleware (drain, enrich, keep, routes) is still configured in the framework integration (e.g., evlog() middleware for Hono/Express/SvelteKit). The Vite plugin handles build-time DX only.
import { initLogger, createRequestLogger } from 'evlog'
initLogger({ env: { service: 'my-worker', environment: 'production' } })
const log = createRequestLogger({ jobId: job.id })
log.set({ source: job.source, recordsSynced: 150 })
log.emit() // Manual emit required in standaloneAll options work in Nuxt (evlog key), Nitro (passed to evlog()), Next.js (createEvlog()), and standalone (initLogger()).
| Option | Type | Default | Description |
|---|---|---|---|
env.service / service | string | 'app' | Service name in logs |
enabled | boolean | true | Global toggle (no-ops when false) |
pretty | boolean | true in dev | Pretty tree format vs JSON |
silent | boolean | false | Suppress console output. Events still go to drains |
include | string[] | All routes | Route glob patterns to log |
exclude | string[] | None | Route patterns to exclude (takes precedence) |
routes | Record<string, { service }> | -- | Route-specific service names |
minLevel | 'debug' | 'info' | 'warn' | 'error' | 'debug' | Hard threshold for the global log API and client log (not request wide events). Use sampling.rates for probabilistic volume on requests |
sampling.rates | object | -- | Head sampling: { info: 10, warn: 50 } (0-100%) |
sampling.keep | array | -- | Tail sampling: [{ status: 400 }, { duration: 1000 }] |
drain | (ctx) => void | -- | Drain callback (Next.js, standalone) |
enrich | (ctx) => void | -- | Enrich callback (Next.js) |
keep | (ctx) => void | -- | Custom tail sampling callback (Next.js) |
redact | boolean | RedactConfig | true in production | Enabled by default in production. false to disable. Object for fine-grained control |
| Hook | When | Use |
|---|---|---|
evlog:drain | After enrichment | Send events to external services |
evlog:enrich | After emit, before drain | Add derived context |
evlog:emit:keep | During emit | Custom tail sampling logic |
close | Server shutdown | Flush drain pipeline buffers |
| Adapter | Import | Env Vars |
|---|---|---|
| Axiom | evlog/axiom | AXIOM_API_KEY, AXIOM_DATASET |
| OTLP | evlog/otlp | OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_ENDPOINT) |
| HyperDX | evlog/hyperdx | HYPERDX_API_KEY (optional HYPERDX_OTLP_ENDPOINT; defaults to https://in-otel.hyperdx.io) |
| PostHog | evlog/posthog | POSTHOG_API_KEY, POSTHOG_HOST |
| Sentry | evlog/sentry | SENTRY_DSN |
| Better Stack | evlog/better-stack | BETTER_STACK_SOURCE_TOKEN |
| Datadog | evlog/datadog | DD_API_KEY or DATADOG_API_KEY, optional DD_SITE / DATADOG_LOGS_URL |
| File System | evlog/fs | None (local file system) |
| HTTP (browser ingest) | evlog/http | None (configure endpoint in code). evlog/browser is deprecated; same API, removed next major |
In Nuxt/Nitro, use the NUXT_ prefix (e.g., NUXT_AXIOM_API_KEY) so values are available via useRuntimeConfig(). All adapters also read unprefixed variables as fallback.
Setup pattern per framework:
// Nuxt/Nitro: server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
// Hono / Express / Elysia: pass drain in middleware options
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({ drain: createAxiomDrain() }))
// Fastify: pass drain in plugin options
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, { drain: createAxiomDrain() })
// NestJS: pass drain in module options
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({ drain: createAxiomDrain() })
// Next.js: pass drain to createEvlog()
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50 } })
const drain = pipeline(createAxiomDrain())
// then: createEvlog({ ..., drain })
// Standalone: pass drain to initLogger()
initLogger({ env: { service: 'my-app' }, drain: createAxiomDrain() })See references/drain-pipeline.md for batching, retry, and buffer overflow config.
Built-in: createUserAgentEnricher(), createGeoEnricher(), createRequestSizeEnricher(), createTraceContextEnricher() — all from evlog/enrichers.
// Nuxt/Nitro: server/plugins/evlog-enrich.ts
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'
export default defineNitroPlugin((nitroApp) => {
const enrichers = [createUserAgentEnricher(), createGeoEnricher()]
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
for (const enricher of enrichers) enricher(ctx)
})
})
// Next.js: in lib/evlog.ts
createEvlog({
enrich: (ctx) => {
for (const enricher of enrichers) enricher(ctx)
ctx.event.region = process.env.VERCEL_REGION
},
})Built-in redaction scrubs sensitive data from wide events before console output and before any drain sees the data. Enabled by default in production (NODE_ENV === 'production'), disabled in development. Uses smart partial masking — preserving enough context for debugging.
// Disable in production (opt-out)
evlog: { redact: false }
// Add custom paths on top of built-ins
evlog: {
redact: {
paths: ['user.password', 'headers.authorization'],
}
}
// Only specific built-ins
evlog: {
redact: {
builtins: ['email', 'creditCard'],
}
}
// No built-ins, only custom (uses flat [REDACTED] replacement)
evlog: {
redact: {
builtins: false,
paths: ['user.ssn'],
patterns: [/SECRET_\w+/g],
}
}Built-in patterns with smart masking output:
| Pattern | Example Input | Masked Output |
|---|---|---|
creditCard | 4111111111111111 | ****1111 |
email | alice@example.com | a***@***.com |
ipv4 | 192.168.1.100 | ***.***.***.100 |
phone | +33 6 12 34 56 78 | +33 ****5678 |
jwt | eyJhbGciOi... | eyJ***.*** |
bearer | Bearer sk_live_abc... | Bearer *** |
iban | FR76 3000 6000 ...189 | FR76****189 |
Works in all frameworks: Nuxt (evlog config), Nitro (evlog() module options), Next.js (createEvlog()), standalone (initLogger()), and all middleware integrations (Hono, Express, Fastify, Elysia, NestJS).
Capture token usage, tool calls, model info, streaming metrics, tool execution timing, cost estimation, and embedding metadata from the Vercel AI SDK into wide events. Import from evlog/ai. Requires ai >= 6.0.0 as a peer dependency.
import { createAILogger } from 'evlog/ai'
const log = useLogger(event) // or any RequestLogger
const ai = createAILogger(log)
const result = streamText({
model: ai.wrap('anthropic/claude-sonnet-4.6'), // accepts string or model object
messages,
})ai.wrap() uses model middleware to transparently capture all LLM calls. Works with generateText, streamText, and ToolLoopAgent.
For tool execution timing, success/failure tracking, and total generation wall time, add createEvlogIntegration():
import { createAILogger, createEvlogIntegration } from 'evlog/ai'
const ai = createAILogger(log)
const agent = new ToolLoopAgent({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
tools: { searchWeb, queryDatabase },
stopWhen: stepCountIs(5),
experimental_telemetry: {
isEnabled: true,
integrations: [createEvlogIntegration(ai)],
},
})This adds ai.tools (per-tool { name, durationMs, success, error? }) and ai.totalDurationMs to the wide event.
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage, model: 'text-embedding-3-small', dimensions: 1536 })For embedMany, pass the batch count:
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })Pass a pricing map to get ai.estimatedCost in the wide event:
const ai = createAILogger(log, {
cost: {
'claude-sonnet-4.6': { input: 3, output: 15 },
'gpt-4o': { input: 2.5, output: 10 },
},
})ai fieldIncludes: calls, model, provider, inputTokens, outputTokens, totalTokens, cacheReadTokens, reasoningTokens, finishReason, toolCalls, steps, msToFirstChunk, msToFinish, tokensPerSecond, error, tools (via telemetry integration), totalDurationMs (via telemetry integration), embedding (via captureEmbed), estimatedCost (via cost option).
Anti-patterns to detect:
| Anti-Pattern | Fix |
|---|---|
Manual token tracking in onFinish | ai.wrap() — middleware captures automatically |
console.log('tokens:', result.usage) | ai.wrap() — structured ai.* fields in wide event |
| No AI observability | Add createAILogger(log) + ai.wrap() |
| No tool execution timing | Add createEvlogIntegration(ai) to experimental_telemetry.integrations |
| Manual cost calculation | Use cost option in createAILogger() |
import { createError } from 'evlog' // or auto-imported in Nuxt
// Minimal
throw createError({ message: 'Database connection failed', status: 500 })
// Standard
throw createError({ message: 'Payment failed', status: 402, why: 'Card declined by issuer' })
// Complete
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer - insufficient funds',
fix: 'Please use a different payment method or contact your bank',
link: 'https://docs.example.com/payments/declined',
cause: originalError,
})
// Backend-only context (wide events / drains — never HTTP body or parseError())
throw createError({
message: 'Not allowed',
status: 403,
why: 'Insufficient permissions',
internal: { correlationId: 'req_abc', resourceId: 'proj_123' },
})Frontend — extract user-facing fields with parseError() (internal is never returned to clients):
import { parseError } from 'evlog'
const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.linkSee references/structured-errors.md for common patterns and templates.
| Anti-Pattern | Fix |
|---|---|
Multiple console.log in one function | Single wide event with log.set() |
throw new Error('...') | throw createError({ message, status, why, fix }) |
console.error(e); throw e | log.error(e); throw createError(...) |
| No logging in request handlers | Add useLogger(event) / useLogger() / createRequestLogger() |
Flat log data { uid, n, t } | Grouped objects: { user: {...}, cart: {...} } |
Logging sensitive data log.set({ user: body }) | Explicit fields: { user: { id: body.id, plan: body.plan } } + enable redact: true |
Putting support-only IDs in why / message | Use createError({ ..., internal: { ... } }) for non-user-facing diagnostics |
See references/code-review.md for the full checklist.
Load based on what you're working on — do not load all at once:
© activepieces, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 4 other files (references) in .agents/skills/review-logging-patterns of activepieces/activepieces.
Open the folder on GitHubat commit 8656fb8
Review Logging Patterns next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Review Logging Patterns this skillactivepieces/activepieces | 25k | — | ~8.2k | Automated safety check: Pass | MIT | |
| Better Authsecondsky/claude-skills | 227 | — | ~7.5k | Automated safety check: Pass | MIT | |
| Review Logging Patternsevloghq/evlog | 1.9k | — | ~11k | Automated safety check: Pass | MIT | |
| Cloudflare Workers Frameworkssecondsky/claude-skills | 227 | — | ~1.4k | Automated safety check: Pass | MIT | |
| AI SDKvercel-labs/ai-facts | 168 | 21 repos | ~1.2k | Automated safety check: Pass | None | |
| Neurolink Guidejuspay/neurolink | 143 | — | ~1.4k | Automated safety check: Pass | MIT |
secondsky/claude-skills
Skill for integrating Better Auth - comprehensive TypeScript authentication framework for Cloudflare D1, Next.js, Nuxt, and 15+ frameworks.
evloghq/evlog
Review code for logging patterns and suggest evlog adoption.
secondsky/claude-skills
Framework integration for Cloudflare Workers. An agent skill from secondsky/claude-skills.
vercel-labs/ai-facts
Answer questions about the AI SDK and help build AI-powered features.
juspay/neurolink
Guide for using the NeuroLink SDK and CLI. An agent skill from juspay/neurolink.
github/awesome-copilot
Build agentic applications with GitHub Copilot SDK. An agent skill from github/awesome-copilot.
activepieces/activepieces
Analyze application logs from the .evlog/logs/ directory. An agent skill from activepieces/activepieces.
activepieces/activepieces
Design system for Activepieces (open-source AI automation platform, "open source replacement for Zapier").
activepieces/activepieces
Build and edit Activepieces pieces (integrations) — creating new pieces, adding actions or triggers, or fixing bugs in existing ones.
activepieces/activepieces
Find what is burning CPU in a live app container without restarting it: separate the JS thread from GC and libuv, open the V8 inspector in place with SIGUSR1, take a CPU profile over CDP, and…
activepieces/activepieces
Diagnose a worker that is growing memory or being OOM-killed: separate a JS-heap leak from native growth, take a V8 heap snapshot of a live worker in place (never uploading it), and walk the…
activepieces/activepieces
Triage Dependabot dependency vulnerability alerts for the Activepieces repo — pull open alerts, dedupe to distinct (package, advisory), confirm the vulnerable package + API is actually used, and…
Review code for logging patterns and suggest evlog adoption. Review Logging Patterns is an agent skill from activepieces/activepieces. Review code for logging patterns and suggest evlog adoption.
Review Logging Patterns fits situations like: tasks that involve Code review; tasks that involve Embeddings; tasks that involve MCP servers.
Run `npx skills add activepieces/activepieces --skill review-logging-patterns -a claude-code`. Or copy the skill folder (.agents/skills/review-logging-patterns in activepieces/activepieces) into .claude/skills/review-logging-patterns in your project. Claude Code loads it when a task matches its description.
Run `npx skills add activepieces/activepieces --skill review-logging-patterns -a codex`. Or copy the skill folder (.agents/skills/review-logging-patterns in activepieces/activepieces) into .agents/skills/review-logging-patterns in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add activepieces/activepieces --skill review-logging-patterns -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/review-logging-patterns, .gemini/skills/review-logging-patterns, .github/skills/review-logging-patterns and .opencode/skills/review-logging-patterns in your project.
Going by SKILL.md and its folder, Review Logging Patterns needs the command-line tools its instructions call (npm) and credentials named AXIOM_API_KEY, HYPERDX_API_KEY, POSTHOG_API_KEY and BETTER_STACK_SOURCE_TOKEN. Our summary lists: Node.js; A credential in AXIOM_API_KEY.
SKILL.md names 2 domains. In commands or code: in-otel.hyperdx.io; the agent is likely to contact it when it follows the instructions. As links in the text: evlog.dev. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Review Logging Patterns is published under the MIT licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 8.2k tokens (SKILL.md is roughly 33k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 8.2k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Review Logging Patterns: Better Auth (secondsky/claude-skills, 227 stars), Review Logging Patterns (evloghq/evlog, 1.9k stars), Cloudflare Workers Frameworks (secondsky/claude-skills, 227 stars) and AI SDK (vercel-labs/ai-facts, 168 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
activepieces (a GitHub organization) maintains it in activepieces/activepieces, which has 24,941 GitHub stars. The repository holds 13 skills in this directory. The repository was last updated on October 8, 2026.
Source: activepieces/activepieces on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.