Logging
Furin installs evlog/elysia for server requests. Each request emits one structured wide event containing method, logical path, status, duration, framework render/cache fields, and the context your code adds.
Browser logging is separate and disabled by default.
Server Logging
import { Elysia } from "elysia";
import { furin } from "@teyik0/furin";
new Elysia()
.use(
await furin({
pagesDir: "./src/pages",
logger: {
exclude: ["/health", "/metrics"],
redact: true,
},
})
)
.listen(3000);
Furin excludes its content-hashed assets and development diagnostics from normal request noise. SPA route-data requests are logged under the logical destination path rather than only as the internal transport URL.
logger accepts evlog's Elysia middleware options: include, exclude, route overrides, enrich, keep, redact, plugins, drain, and waitUntil. Furin additionally accepts global sampling configuration.
Add Request Context
The loader context already contains log:
import { defineRoute } from "@teyik0/furin";
import { t } from "elysia";
export const route = defineRoute("/blog/[slug]")
.config({
mode: "ssr",
params: t.Object({ slug: t.String() }),
})
.loader(async ({ log, params }) => {
log.set({ post: { slug: params.slug } });
const post = await getPost(params.slug);
log.set({ post: { found: post !== null } });
return { post };
})
.page(({ post }) => <Post value={post} />);
Pass the request logger into services that contribute useful fields. Prefer one nested domain object over many unrelated line logs.
For live SSR, fields join the HTTP request event. SSG prerenders and ISR background work use detached loggers so their render/cache diagnostics can still reach the configured drain.
Sampling
await furin({
logger: {
sampling: {
rates: { info: 10 },
keep: [{ status: 400 }, { duration: 1000 }],
},
keep: (context) => {
if (context.method !== "GET") context.shouldKeep = true;
},
},
});
This head-samples ordinary info events, then force-keeps errors, slow requests, and mutations. Start without sampling until the event shape is stable; otherwise missing fields are harder to distinguish from sampled events.
Redaction
await furin({
logger: {
redact: {
paths: ["user.email", "**.password", "*_token"],
patterns: [/Bearer\s+\S+/gi],
},
},
});
redact: true enables evlog's built-in email, credit-card, IP, phone, JWT, bearer-token, and IBAN patterns. Redaction runs before enrichment and draining. Still avoid logging secrets in the first place.
Drains
import { createAxiomDrain } from "evlog/axiom";
await furin({
logger: {
drain: createAxiomDrain({
apiKey: process.env.AXIOM_API_KEY!,
dataset: "furin-production",
}),
},
});
Other isolated evlog entrypoints include evlog/datadog, evlog/otlp, evlog/sentry, evlog/hyperdx, evlog/better-stack, evlog/posthog, evlog/loki, and evlog/fs.
For local NDJSON files:
import { createFsDrain } from "evlog/fs";
await furin({
logger: {
drain: createFsDrain({ dir: ".evlog/logs" }),
},
});
Browser Logging
Enable the browser HTTP drain in the build configuration:
import { defineConfig } from "@teyik0/furin/config";
export default defineConfig({ clientLogging: true });
Or pass clientLogging: true to furin() when development/runtime configuration owns the choice. The generated browser entry initializes evlog/http, batches events to the instance's /_furin/ingest endpoint, and re-emits them nested under a browser field with service: "furin:browser", so browser input cannot overwrite server fields such as status, path or user.
Furin automatically records navigation and refresh failures. Application browser code can use evlog's client-safe logger:
import { log } from "evlog";
log.info({ action: "search_submitted", queryLength: query.length });
The ingest endpoint limits a request to 64 KiB and at most 100 entries. It accepts only event-shaped batches and strips __proto__, constructor and prototype keys at every depth before adding them to the server event.
Client logging adds browser code and HTTP traffic, so enable it deliberately and apply the same redaction/data-minimization policy as server logs.