Read the default log output, adjust levels and categories, and send records to an existing logger
Every client prints what it is doing by default: Startup, each shard (gateway connection) becoming ready, lost connections with their close code and next step, long rate-limit waits, shutdown, and every application error in full. Adjust the level for the whole client or one area, turn on Debug records while investigating, or send records to an existing logger. Records never contain tokens or other credentials
Read the Default Output
On a terminal the client prints one readable line per record: Time, level, category, the record's code in brackets, a message and its facts. Warnings and errors go to standard error, everything else to standard output. A two-shard bot that starts, loses one connection and stops prints lines like these
2026-09-27 03:14:20.850 INFO lifecycle [lifecycle.starting] Starting Fluxerly <version> for fluxer.app with 2 shards version=<version> instance=fluxer.app shards=2 totalShards=22026-09-27 03:14:20.853 INFO lifecycle [lifecycle.startupDeadline] Startup of 2 shards may take up to 31.0 s: The configured 30.0 s (connection.startupTimeoutMs) plus 1 s per additional shard, because the SDK starts shard sessions 1 s apart startupTimeoutMs=31000 configuredMs=30000 shards=22026-09-27 03:14:20.891 INFO lifecycle [lifecycle.ready] Shard 0 is ready shardId=0 attempt=1 durationMs=36 mode=identify shards=22026-09-27 03:14:21.890 INFO lifecycle [lifecycle.ready] Shard 1 is ready shardId=1 attempt=1 durationMs=1033 mode=identify shards=22026-09-27 03:14:21.891 INFO lifecycle [lifecycle.connected] Connected to Fluxer as MyBot in 1520 communities communities=1520 shards=22026-09-27 03:14:22.233 WARN lifecycle [lifecycle.connectionLost] Shard 1 lost its connection. Close code 4000 (UNKNOWN_ERROR): The gateway reported an unspecified error. Reconnecting and resuming the session in 755 ms shardId=1 closeCode=4000 attempt=1 delayMs=755 reason=closed classification=closed next=resume2026-09-27 03:14:22.999 INFO lifecycle [lifecycle.ready] Shard 1 resumed its session after 766 ms offline shardId=1 attempt=2 durationMs=1 mode=resume shards=22026-09-27 03:14:23.737 INFO lifecycle [lifecycle.stopRequested] The signal passed to runBot was aborted, so the bot is shutting down2026-09-27 03:14:23.739 INFO lifecycle [lifecycle.shutdown] Shutting down2026-09-27 03:14:23.742 INFO lifecycle [lifecycle.shutdownComplete] Shutdown complete in 3 ms durationMs=3
When a stream is not a terminal, such as under a process manager or in a container, its records are printed as one JSON object per line instead. Standard output and standard error decide separately. Set format to choose explicitly, or set the FLUXERLY_LOG_FORMAT environment variable to pretty or json for a client without format. A supervisor prints forwarded child records in the format of the stream they go to
Readable output is colored on a terminal. Set FLUXERLY_LOG_COLOR to 1, true, yes or on to force color, or to 0, false, no or off to turn it off. The NO_COLOR variable also turns color off
A failed handler is printed at Error with its message, stack and cause chain, because the error came from the bot's own code. Credentials are masked in names, application error codes, messages and stacks, including causes. Stack frames inside the SDK and Effect collapse into one line, so the bot's own frames stand out
2026-09-26 21:27:52.689 ERROR events [events.handlerFailed] The messageCreate handler failed: Cannot read properties of undefined (reading 'content') event=messageCreate subscriptionId=messageCreate#1 messageId=1456074443980800005 channelId=1456074443980800002 guildId=1456074443980800001 TypeError: Cannot read properties of undefined (reading 'content') at messageCreate (file:///srv/bot/bot.js:3:78) ... 9 SDK and Effect frames hidden
An expected SDK error, such as a rejected request, prints its code, hint and details instead of stack frames. Here a handler returned a failed reply because the bot lacks permission in the channel. For a missing permission, the hint and requiredPermissions name the permissions Fluxer checks for the operation, because Fluxer does not report which one is missing
2026-09-26 21:27:52.804 ERROR events [events.handlerFailed] The messageCreate handler failed: Message send failed: Fluxer reports that the bot lacks a required permission (MISSING_PERMISSIONS, HTTP 403) event=messageCreate subscriptionId=messageCreate#1 messageId=1456074443980800006 channelId=1456074443980800002 guildId=1456074443980800001 MessageError [message.send.rejected]: Message send failed: Fluxer reports that the bot lacks a required permission (MISSING_PERMISSIONS, HTTP 403) Hint: The bot needs View channel and Send messages for this operation. Grant them to the bot's role in the community settings or the channel's permission overrides. Embeds also need Embed links, files need Attach files, and a forward needs View channel in its source channel Details: operation=send reason=rejected outcome=rejected status=403 apiError=MISSING_PERMISSIONS requiredPermissions=["ViewChannel","SendMessages"]
A 401 or 403 rejection waits up to one second for its handler to finish. If the handler fails with that same rejection, directly or in its cause chain, before the Warn is logged and no onError hook is configured for that failure, only the handler's failure record appears. A handler that fails more than one second later also logs its failure, because the rest.rejectedWarn has already appeared
Different requests with the same status and code remain separate rejections
When the handler handles such a rejection itself, or passes it to an onError hook, the rest.rejectedWarn still appears, because it usually points at a token or permission problem that persists. Repeats of it collapse like other Warn records
2026-09-26 21:27:52.802 WARN rest [rest.rejected] POST /channels/:id/messages was rejected for channel 1456074443980800002: Fluxer reports that the bot lacks a required permission (MISSING_PERMISSIONS, HTTP 403) route=/channels/:id/messages status=403 method=POST apiError=MISSING_PERMISSIONS hint="Grant the bot's role View channel and Send messages in this channel, plus Embed links or Attach files when the message has them, in the community settings or the channel's permission overrides"
Look Up a Record Code
Every record has a stable code, shown in brackets in readable output. The error and log codes page lists every code with its meaning and next step. These Warn records often surprise a new bot:
Fluxer rejected a request with HTTP 401 or 403, usually a token or permission problem. It is logged even when the application handles the Result. A handler that fails with the rejection within one second and has no onError hook logs only its own failure record. A later failure logs both. A bot that expects these rejections can set the restcategory to error
The FLUXERLY_LOG_FORMAT or FLUXERLY_LOG_COLOR variable has a value the SDK does not recognize, so it is ignored
Choose Levels and Categories
The default level is info. Set level for the whole client and categories for single areas. The categories are lifecycle, gateway, rest, ratelimit, cache, events, commands, collectors, supervisor and sdk
Application errors, failed hooks, connections that cannot continue and cleanup failures
warn
Lost connections, session resets, rate-limit waits of one second or more, rejected requests and responses, skipped malformed gatewayevents and dropped events
info
Startup, shard readiness and shutdown with its duration
debug
Every REST request with its route template, status and duration, retries, gateway dispatch summaries, rejected and unmatched commands
The silent level turns a category or the whole client off, including application errors that have no onError hook and unsafe payload records
To change levels while the bot runs, for example to investigate a problem without a restart, pass new level and categories settings to client.logging.configure. Omitted level and categories return to their defaults. A debug setting there replaces the current Debug categories, as described in Turn on debug output without changing code, and an omitted one keeps them. The other logging settings stay as the client was created
import type { Client } from "@neontechspace/fluxerly"export function traceRequests(client: Client) { client.logging.configure({ level: "warn", categories: { rest: "debug" } })}
Turn on Debug Output Without Changing Code
Set the FLUXERLY_DEBUG environment variable before starting the bot. The value 1 enables Debug records for every category, while a comma-separated list such as gateway,rest enables only those. The variable is read when the client is created
.env
FLUXERLY_DEBUG=rest,ratelimit node bot.js
The debug option does the same from code, with true or a list of categories. Debug settings only lower a threshold, so level: "silent" with FLUXERLY_DEBUG=rest still prints RESTDebug records
To change Debug output while the bot runs, pass debug to client.logging.configure. It replaces the categories chosen at creation, including those from FLUXERLY_DEBUG, and debug: false turns them all off
export function stopDebugOutput(client) { client.logging.configure({ debug: false })}
import type { Client } from "@neontechspace/fluxerly"export function stopDebugOutput(client: Client) { client.logging.configure({ debug: false })}
Handle Failures in One Place
Pass onError to receive every failure that has no returned result: Handler and command failures, collector callbacks and filters, cleanup progress callbacks, subscriptions closed by a full queue with overflow: "stop", message cache callbacks and failed state observers. The report holds the original error and the IDs of the message involved, never its content. With a hook in place the SDK keeps those failures at Debug instead of printing them again
A failure only queues its report, so a slow hook never holds up eventhandlers.
Reports are delivered one at a time in order. Up to 256 wait while the hook is busy, and later ones are logged instead and counted in diagnostics().counters.reportsDropped.
A hook that throws or rejects is logged together with the original failure and is not retried.
Shutdown does not wait for a hook that never finishes, and logs the reports it had not delivered with a note.
The record's fields.reportOutcome names why a report was logged instead of delivered: queueFull, interrupted, clientClosed or subscriptionClosed
Send Records to an Existing Logger
The sink option replaces the console output. It receives frozen LogRecord objects with a stable code, a readable message and optional facts such as shardId, route, status, closeCode and error. Other facts are in fields, such as next on lifecycle.retry and lifecycle.connectionLost records, which is resume when the next attempt resumes the session and identify when it starts a new one. Match on code and these facts, because message wording can change between releases
Many loggers have one method per level that takes an object of fields and a message. This sink passes each record to such a logger
import { createClient } from "@neontechspace/fluxerly"/** A logger with one log method that reads the level from the entry */interface EntryLogger { log(entry: { level: string; message: string; [key: string]: unknown }): void}export function createEntryLoggerClient(token: string, logger: EntryLogger) { return createClient({ token, logging: { sink: (record) => logger.log({ ...record, level: record.level === "fatal" ? "error" : record.level }), }, })}
A sink runs synchronously and its return value is ignored. A sink that throws or returns a rejected Promise is counted in diagnostics().counters.sinkFailures and reported once on standard error, and SDK work continues. Pass an array to use several sinks
Collapse Repeated Errors
Identical Warn, Error and Fatal records within one minute are printed once. Records about different shards or subscriptions are not identical, so each one's first occurrence is printed. The next identical record after the window says how many were hidden, as in (repeated 12× since last shown), and shutdown prints any remainder. A sink finds that count in fields.repeated. Change the window with dedupe: { windowMs: 10_000 } or turn collapsing off with dedupe: false. Counters in diagnostics().counters still include every occurrence
Count What Happened
The client.diagnostics().counters object keeps running totals since the client was created: Handler and hook failures, dropped events by reason and by event name, rejected gateway messages, unknown dispatches and opcodes, REST retries, rate-limit waits, reconnects, resumes, sink failures, cleanup failures and command rejections
The eventsDroppedByEvent object counts the same drops by event name, such as messageCreate, so a bot can see which handler loses events. Drops that have no known event name appear only in the reason totals of eventsDropped, so the per-event sum can be lower
import type { Client } from "@neontechspace/fluxerly"export function healthSnapshot(client: Client) { const { counters, state } = client.diagnostics() return { state, reconnects: counters.reconnects, droppedEvents: counters.eventsDropped.overflow + counters.eventsDropped.malformed, handlerFailures: counters.handlerFailures, }}
Debug Payloads Safely
The setting unsafe: { payloads: true } adds Trace records with gateway and REST payload bodies, for all categories or the listed ones. Payloads can contain private message content, so the client prints a Warn banner once, at startup or before the first payload record, even when the level hides warnings. A category set to silent prints no payloads, and a received REST body shows at most its first 64 KiB. Tokens, Authorization headers, client secrets, passwords, cookies, invite codes and the user name and password in a URL such as https://user:password@host/path stay masked even then, including credential-key values in truncated JSON text. Leave it off in production
A native client, created from the Effect entry point @neontechspace/fluxerly/effect, sends records through the running program's Effect logger with Effect.logWithLevel. Each record carries fluxerly.* annotations such as fluxerly.code, fluxerly.category and fluxerly.shardId, so any Effect logger, span and tracer applies. Records with errors also carry a Cause with credentials masked
A record with an SDK error also carries fluxerly.error.code, fluxerly.error.hint and one fluxerly.error.<detail> annotation per safe detail, such as fluxerly.error.status. Each error in its cause chain adds the same annotations with one more cause. in the prefix, such as fluxerly.error.cause.code, fluxerly.error.cause.hint and fluxerly.error.cause.<detail>
Native clients accept the same logging settings. A sink or an explicit format replaces the Effect logger with SDK output
Debug and Trace records chosen by the SDK settings bypass the Effect minimum log level, while Info and higher records still respect it
Export Metrics and Traces with OpenTelemetry
Pass an observe function to the client to receive a measurement each time the SDK finishes a piece of work: A REST request attempt with its method, route template, status and duration, a rate-limit wait, a reconnection attempt, a resumed session, and a handler or command run with its duration and outcome. Each measurement is an Observation whose type field says which kind it is. Observations never contain tokens, payloads or message content, so they can go to any monitoring system. Both entry points accept the same option
A command router emits one handler observation per message, including event and command middleware in its duration. A matched command supplies its canonical name in command, even when a guard denies it or middleware stops it. A reported failure gives outcome: "failure" even if middleware recovers, interruption gives "cancelled", and other completions give "success". Messages with no command match still produce an observation without command
This example records durations and reconnection attempts with an OpenTelemetry meter. The Meter interface lists only the methods the example calls, so the code needs no extra package, and the meter from metrics.getMeter("bot") in @opentelemetry/api fits it
The SDK calls observe while it finishes the measured work, so the function should only record values and leave sending them to the exporter. An observe function that throws does not change what the bot does, and each throw is counted in diagnostics().counters.sinkFailures
In a native program, the SDK also opens spans named fluxerly.gateway.connect, fluxerly.rest.request, fluxerly.event.handle and fluxerly.command.execute, and updates the metrics fluxerly_rest_duration_ms, fluxerly_ratelimit_wait_ms, fluxerly_gateway_reconnects_total, fluxerly_events_dropped_total and fluxerly_handler_failures_total. The dropped-events metric carries the attribute reason and, where the event name is known, event
A command span covers its middleware, guards, argument conversion, cooldown and execution. Command failure metrics use the canonical command name
Without a metric exporter, metrics stay in memory. Provide the Layer from @effect/opentelemetry, the Effect value that supplies the exporter, to the native program to export them
import { Effect, type Layer } from "effect"import { runBot } from "@neontechspace/fluxerly/effect"/** Pass the tracing Layer built with @effect/opentelemetry, such as its NodeSdk layer */export function runTracedBot(token: string | undefined, telemetry: Layer.Layer<never>) { return runBot({ token, processSignals: true, events: {} }).pipe(Effect.provide(telemetry))}