PrivateconstructorConstructs a Logger.
Minimum weight to emit.
Fields repeated on every line.
Which context fields reach the line.
Private ReadonlycontextFields repeated on every line.
Private ReadonlyfieldsWhich of those fields the deployment actually wants ingested.
Private ReadonlythresholdMinimum weight a line must carry to be emitted.
Derives a logger with extra context, sharing this one's threshold.
Fields to add to every line.
The derived logger.
PrivatecomposeAssembles the object that becomes the line.
Severity of the line.
What happened, as a dotted event name.
Optionaldata: Record<string, unknown>
Structured detail.
Optionalmessage: string
A sentence for someone reading the stream.
The line, ready to serialise.
The three reserved keys are written first so a raw line reads
level-then-event-then-message, and re-asserted on every merge so nothing
can displace them. time sits behind the allow-list like any other field:
a runtime that stamps its own ingestion timestamp does not need a second
one, and it is computed here rather than carried in the context so it
records when the line was written and not when the logger was built.
Emits a debug line.
What happened, as a dotted event name.
Optionaldata: Record<string, unknown>
Structured detail.
Optionalmessage: string
A sentence for someone reading the stream. Defaults to the event name.
PrivateemitWrites one line through the console method matching its level.
Severity of the line.
What happened, as a dotted event name.
Optionaldata: Record<string, unknown>
Structured detail.
Optionalmessage: string
A sentence for someone reading the stream.
Context and payload are both flattened onto the line rather than nested,
so a collector indexes status rather than data.status and a dashboard
column reads as a field name instead of a path.
They are not treated alike. Context is ambient — stamped by middleware for the span of a request — and passes through the allow-list, because a deployment should be able to decide it does not want the point of presence on every line. Payload was typed out at the call site for this one call, so it is emitted whole: filtering it would silently discard the answer someone went to the trouble of logging.
Emits an error line.
What happened, as a dotted event name.
Optionaldata: Record<string, unknown>
Structured detail.
Optionalmessage: string
A sentence for someone reading the stream. Defaults to the event name.
StaticfromBuilds a logger for the current environment.
Anything carrying an optional LOG_LEVEL, SERVICE_NAME and
LOG_FIELDS. Tolerates null.
Fields to repeat on every line.
A logger honouring the configured (or LOG_LEVEL) threshold, and
emitting the context fields the deployment declared.
The parameter is structural rather than a named environment type. All this
needs is a possible LOG_LEVEL, SERVICE_NAME and LOG_FIELDS, so asking
for exactly that keeps the kernel free of any platform's environment shape
— a runtime's Env object, a process.env, or a bare object literal all
satisfy it.
The service name is read from the environment first and the configuration
surface second. The order is the point: a name in the environment sits
beside the deployment's own name — in wrangler.json, two lines below
"name" — where a rename that misses it shows up in the same diff. A name
compiled into the bundle from config/ sits in a different file that a
rename does not touch, which is how a Worker came to spend production
calling itself cloudflare-boilerplate. The configuration value remains
the fallback for runtimes with no environment to read.
With neither set the line still carries a service, reading unknown. A
missing name is a deployment defect, and an omitted key would make it an
invisible one — service = unknown is a query that finds every
misconfigured deployment.
Emits an info line.
What happened, as a dotted event name.
Optionaldata: Record<string, unknown>
Structured detail.
Optionalmessage: string
A sentence for someone reading the stream. Defaults to the event name.
Private StaticparseParses a level name, ignoring anything unrecognised.
Optionalvalue: string
The candidate level name.
The level, or null when the value is not one.
Private StaticserializeMakes a data payload serialisable, and safe to ship.
The payload to convert.
The payload scrubbed of secrets, with errors expanded to message and stack.
Two passes for two different problems. redact runs first, because a
credential must not survive into the line whatever shape it arrived in;
then Error expansion, which JSON.stringify would otherwise flatten to
{}, losing the only useful part of an error line.
The order is load-bearing in one direction only: redaction returns Error
values untouched precisely so this pass can still recognise them.
Emits a warn line.
What happened, as a dotted event name.
Optionaldata: Record<string, unknown>
Structured detail.
Optionalmessage: string
A sentence for someone reading the stream. Defaults to the event name.
Structured, level-classified logging — and nothing else.
Each line is one JSON object emitted through the matching
console.*method, so the runtime's log collector records the native level and keeps the fields searchable. Nothing here aggregates — that is TelemetrySink, a separate capability, because a sink samples at volume and is the wrong place for lines that have to be read back verbatim.Remarks
An event name and a sentence
The first argument is an event name — a stable, lowercase, dotted identifier such as
notification.sentorexternal_api.error— emitted under theeventkey. A collector that indexes JSON fields turns that key into a column, soevent = notification.failedbecomes a filter rather than a substring search, and the count of one event over time becomes a series. A prose sentence can be neither:'Failed to send notification for user 7'is a different string on every line it is written.That is why the third argument exists rather than replacing the first. A
messageis written for whoever is reading the stream, and it is the only thing most log viewers show by default. Omit it and the event name stands in, so the column is never empty; supply it and the line says something in English without anyone opening it:Anything that varies belongs in the payload, where it stays structured and searchable on its own — never interpolated into the event name.
One flat object
Context and payload are flattened onto the line, not nested under a wrapper, so a field is indexed as
statusrather thandata.status.Context passes through an allow-list first; see
LogFields. Payload never does — it was named at the call site for this one call, and dropping it there would discard the answer somebody logged the line to get.Which deployment wrote it
servicenames the deployment, fromSERVICE_NAMEin the environment or, failing that,logging.serviceon the configuration surface. It is context, so it reaches the line only while the allow-list admits it — which the kernel default does.Some runtimes already record it: Cloudflare stamps the script name on every log record before it is stored, so a Worker that never leaves that console gains nothing from a second copy and can drop the field. A deployment whose lines travel — exported to a file, shipped through Logpush, read on a runtime with no such column — should keep it, because a record that only makes sense inside one vendor's console is not a record.
That is the trade the allow-list exists to let a deployment make, rather than this class making it for every consumer. The value comes from the environment rather than a constant compiled into the bundle, so whichever way it is decided the name cannot drift from the deployment it describes.
serviceanswers which service; a line that needs to say which part of it ran names acomponentin the context.The threshold comes from the surface registered by configure, and
LOG_LEVELin the environment overrides it per deployment, so staging can run atdebugwithout a code change.Retention is the runtime's business, not this class's — it writes lines and stops there. Whether they are kept, and for how long, is a deployment setting wherever the process runs.
Example
Author
Bayu Dwiyan Satria
Version
1.2.1
Since
1.0.0