Skip to content

Logging

Structured logging at build(), levels and formats, DI access, and how records reach transports.

AI generated, pending review Updated 10 days ago · Flare 0.3

Every Flare app gets a structured Logger during host.build(). Each log call assembles a LogRecord (timestamp, level, message, and optional meta, error, context, and state) and sends it to every registered transport. Minimum level comes from the log section of resolved config (flare.json plus FLARE__* overrides). log.format applies to the framework default console transport only.

Resolve the logger from dependency injection as the Logger token, or read host.logger after build.

flare.json
{
"host": { "env": "development" },
"log": {
"level": "info",
"format": "json",
"enableContext": false
}
}

Schema defaults are info / json / enableContext: false. When merged host.env is "development" and log.level or log.format are still unset, Flare promotes them to debug / pretty before validation. In any other environment, omitted fields keep the schema defaults.

Optional per-transport level overrides:

{
"log": {
"transports": {
"console": { "level": "debug" },
"metrics": { "level": "error" }
}
}
}

Keys under log.transports must match each transport class’s static transportName. A transport with no entry uses the global log.level.

Override at runtime with environment variables:

Terminal window
FLARE__log__level=warn
FLARE__log__format=json
FLARE__log__enableContext=true

Field reference: flare.json reference.

An error that reaches the framework’s fallback response, meaning no error handler claimed it, is logged at error level before the response envelope replaces it, on both the FlareError and plain Error branches. Set log.unhandledErrors: false (default true) to silence the built-in report and own that reporting entirely from an error handler:

{
"log": { "unhandledErrors": false }
}

Logger is a framework singleton compiled during build(). Declare Logger in static deps and resolve it with this.inject(Logger):

import { FlareService, Logger } from "@flare-ts/core";
class OrderService extends FlareService {
public static override deps = [Logger];
readonly #log = this.inject(Logger);
place(order: { id: string }) {
this.#log.info("placing order", { orderId: order.id });
}
}

In inline HTTP routes, use a named inject map and read scope.log:

import { FlareHost, FlareResponse, Logger } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
const host = new FlareHost(node);
host.http.post(
"/orders",
{ inject: { log: Logger } },
(ctx, scope) => {
scope.log.info("got order", { path: ctx.req.path });
return new FlareResponse(201, { id: 1 });
},
);
const app = host.build();
app.run();

After build(), you can also call host.logger directly. Reading host.logger before build() throws a plain Error. See Host.

Each method emits a LogRecord. Every transport receives the same assembled record. Records below a transport’s effective minimum level are dropped before write() runs.

log.trace("very chatty");
log.debug("debug");
log.info("normal");
log.warn("notable");
log.error(err, "failed");
log.fatal(err, "unrecoverable");

Signatures:

  • trace / debug / info / warn: (message, meta?)
  • error / fatal: (message, meta?) or (error, message, meta?)

For Error instances, record.error is { name, message, stack? }. Any other thrown value becomes { message: string } via String(error).

FieldWhen present
timestampAlways (Unix ms)
levelAlways
messageAlways
metaWhen passed to the log call
errorWhen error() / fatal() receive an error value
contextWhen log.enableContext is true and the framework has active log context
stateWhen context is on and includes request state

The context field above is what lets a request’s log lines be correlated without any call passing an id. With log.enableContext on, Flare enters an AsyncLocalStorage scope at the request boundary and stamps requestId, method, and url onto it once; every emit inside that request reads the active scope, so a line logged several calls deep carries the same requestId as the handler’s own. The alternative is threading a request id through every function signature that might log, or a module-level logger that knows nothing about the request. Both work until the one call path nobody threaded logs without it, and that orphaned line is the one you cannot line up with its request while reading production logs.

With log.enableContext: true, the framework copies active log context onto each record. On Cloudflare Workers, callbacks passed to waitUntil run outside the request’s AsyncLocalStorage scope and lose that context unless you restore it. See Context and waitUntil.

The default console transport is wired in at build. Add sinks (metrics, files, external services) by subclassing LoggerTransport on Node or CfLoggerTransport on Workers and registering with host.logging.transport() before the first build().

Step-by-step: Custom transports.

RuntimeTransport base classDefault output
Node.jsLoggerTransportFramework console transport; log.format selects pretty or JSON lines
Cloudflare WorkersCfLoggerTransportSame log.format contract as Node

Use import { cf } from "@flare-ts/core/cloudflare" (or buildCf when bundling flare.json) as the host adapter. See Host.