@wiolett/gateway-logger
v1.1.2
Published
`@wiolett/gateway-logger` is a Node-first TypeScript SDK for sending application logs into Wiolett Industries Gateway. It handles Gateway ingest tokens, context merging, batching, retries, rate-limit backoff, trace/span identifiers, and graceful shutdown
Readme
Gateway Logging SDK
@wiolett/gateway-logger is a Node-first TypeScript SDK for sending application logs into Wiolett Industries Gateway. It handles Gateway ingest tokens, context merging, batching, retries, rate-limit backoff, trace/span identifiers, and graceful shutdown flushing.
Use it when an external service, worker, script, or backend needs to write structured logs into Gateway without calling the ingest API manually.
Installation
pnpm add @wiolett/gateway-loggernpm install @wiolett/gateway-loggerThe package is ESM-only and ships TypeScript declarations. It expects a runtime with fetch; modern Node.js versions provide it globally. For older runtimes, pass a custom fetch implementation in the logger options.
Gateway Setup
- Open Gateway.
- Create a logging ingest token for the target schema/source.
- Store the token as a server-side secret, for example
GATEWAY_LOGGING_TOKEN. - Configure the SDK with your Gateway URL and token.
Logging ingest tokens use the gwl_ prefix. Keep them server-side. Do not put them in browser bundles, public config, mobile apps, or any client-controlled code.
Quick Start
import { GatewayLogger } from "@wiolett/gateway-logger";
const logger = new GatewayLogger({
endpoint: "https://gateway.example.com",
token: process.env.GATEWAY_LOGGING_TOKEN!,
service: "billing-api",
source: "api",
labels: {
app: "billing",
environment: process.env.NODE_ENV ?? "development",
},
fields: {
version: process.env.npm_package_version,
},
});
logger.info("Service started");
logger.error("Payment capture failed", {
requestId: "req_123",
labels: { provider: "stripe" },
fields: {
statusCode: 502,
durationMs: 1834,
},
});
await logger.flush();
await logger.close();Configuration
const logger = new GatewayLogger({
endpoint: "https://gateway.example.com",
token: process.env.GATEWAY_LOGGING_TOKEN!,
service: "billing-api",
source: "api",
traceId: "trace_123",
spanId: "span_456",
requestId: "req_123",
labels: { app: "billing" },
fields: { version: "1.4.0" },
batching: {
enabled: true,
maxBatchSize: 100,
flushIntervalMs: 5000,
flushDebounceMs: 250,
maxQueueSize: 10000,
overflow: "drop-oldest",
},
retry: {
maxAttempts: 5,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: true,
},
onError: (error, logs, failure) => {
console.error("Gateway logging delivery failed", {
reason: failure?.reason,
status: failure?.status,
count: logs?.length ?? 0,
error,
});
},
onDrop: (log, reason) => {
console.warn("Gateway log dropped", reason, log.message);
},
onFallback: async (logs, failure) => {
await persistFailedGatewayLogs(logs, failure);
},
});Logger Options
| Option | Type | Description |
| --- | --- | --- |
| endpoint | string | Base Gateway URL, for example https://gateway.example.com. Trailing slashes are removed automatically. |
| token | string | Gateway logging ingest token. |
| service | string | Logical service name attached to every log from this logger. |
| source | string | Source/channel name, such as api, worker, cron, or queue. |
| traceId | string | Default trace identifier attached to every log. |
| spanId | string | Default span identifier attached to every log. |
| requestId | string | Default request/correlation identifier attached to every log. |
| labels | Record<string, string \| number \| boolean \| null \| undefined> | Indexed dimensions. Values are normalized to strings; null and undefined values are dropped. |
| fields | Record<string, GatewayLogValue \| undefined> | Structured event data. Date values are serialized to ISO strings; undefined values are dropped. |
| batching | GatewayBatchingOptions | Queue and flush behavior. |
| retry | GatewayRetryOptions | Retry and backoff behavior for transient delivery failures. |
| fetch | typeof fetch | Optional fetch implementation for custom runtimes or tests. |
| onError | (error, logs?, failure?) => void | Called when delivery fails or a fallback handler throws. Failed logs and failure metadata are included when available. |
| onDrop | (log, reason) => void | Called for each dropped log. |
| onFallback | (logs, failure) => void \| Promise<void> | Optional final handler for logs that could not be delivered after retries or were rejected permanently. Use this to write logs to disk, another queue, or another transport. |
Writing Logs
The logger provides severity helpers for all Gateway severities:
logger.trace("Cache lookup", { fields: { key: "user:123" } });
logger.debug("Computed quote", { fields: { amount: 42.13 } });
logger.info("Checkout started");
logger.warn("Provider latency is high", { fields: { durationMs: 2900 } });
logger.error("Capture failed", { fields: { statusCode: 502 } });
logger.fatal("Worker cannot start", { fields: { reason: "missing_config" } });You can also submit a complete event with log:
logger.log({
timestamp: new Date(),
severity: "info",
message: "Invoice generated",
service: "billing-worker",
source: "invoice",
labels: { tenant: "acme" },
fields: { invoiceId: "inv_123", total: 149.99 },
});Context Merging
Context is inherited and merged from parent loggers, child loggers, and per-event options.
const root = new GatewayLogger({
endpoint: "https://gateway.example.com",
token: process.env.GATEWAY_LOGGING_TOKEN!,
service: "api",
labels: { app: "billing", region: "eu" },
});
const payments = root.child({
service: "billing-payments",
labels: { module: "payments" },
fields: { provider: "stripe" },
});
payments.error("Payment capture failed", {
labels: { region: "us" },
fields: { statusCode: 502 },
});The emitted event uses:
service: "billing-payments"because later scalar context overrides earlier scalar context.labels: { app: "billing", region: "us", module: "payments" }because labels are merged by key.fields: { provider: "stripe", statusCode: 502 }because fields are merged by key.
Request Scoped Logging
Use withContext to create a temporary derived logger for a request, job, or operation.
await logger.withContext(
{
requestId: "req_123",
labels: { route: "POST /payments" },
},
async (requestLogger) => {
requestLogger.info("Request started");
requestLogger.info("Request completed", { fields: { durationMs: 148 } });
}
);withContext returns the value returned by your callback and supports async callbacks.
Traces And Spans
The SDK can create lightweight trace and span contexts. This does not require a separate tracing backend; it only attaches traceId, spanId, and optional names to Gateway log events.
const trace = logger.createTrace({
name: "checkout",
requestId: "req_123",
labels: { flow: "checkout" },
});
trace.info("Checkout started");
const span = trace.createSpan("stripe.capture");
span.info("Capture requested");
span.error("Capture failed", {
fields: { statusCode: 502, durationMs: 1834 },
});
await span.end();
await trace.end();createTrace generates a UUID trace ID when one is not provided. createSpan keeps the trace ID and generates a UUID span ID when one is not provided. Passing a trace or span name stores it in fields.traceName or fields.spanName.
Batching And Delivery
Logs are queued in memory and delivered to Gateway over HTTP:
- One event is sent to
/api/logging/ingest. - Multiple events are sent to
/api/logging/ingest/batch. Authorization: Bearer <token>is used for every request.Content-Type: application/jsonis set automatically.
Default batching options:
| Option | Default | Description |
| --- | ---: | --- |
| enabled | true | Enables queued batching. When set to false, the SDK flushes one event at a time. |
| maxBatchSize | 100 | Maximum events sent in one batch. |
| flushIntervalMs | 5000 | Periodic flush interval. |
| flushDebounceMs | 250 | Delay after enqueue before a flush is scheduled. |
| maxQueueSize | 10000 | Maximum in-memory queued events. |
| overflow | "drop-oldest" | Queue overflow strategy: "drop-oldest" or "drop-newest". |
Call flush() when you need to wait until currently queued logs are delivered:
logger.info("Job completed");
await logger.flush();Call close() during shutdown. It stops timers, marks the logger closed, and flushes queued events:
await logger.close();Events logged after close() are dropped with reason "closed".
Retries And Failures
The SDK retries transient delivery failures:
- Network errors.
- HTTP
408. - HTTP
425. - HTTP
429. - HTTP
5xx.
Default retry options:
| Option | Default | Description |
| --- | ---: | --- |
| maxAttempts | 5 | Total send attempts for a batch. |
| minDelayMs | 500 | Initial backoff delay. |
| maxDelayMs | 30000 | Maximum backoff delay. |
| jitter | true | Adds random jitter to retry delays. |
For 429 responses, the SDK prefers the HTTP Retry-After header. If the header is absent, it also understands Gateway's details.retryAfterSeconds response shape.
Permanent validation or authorization failures are not retried. Those batches are dropped with reason "permanent_failure". Retryable batches that still fail after maxAttempts are dropped with reason "retry_exhausted".
When a batch cannot be delivered, the SDK calls callbacks in this order:
onDrop(log, reason)once for each failed log.onError(error, logs, failure)with the failed log array and metadata.onFallback(logs, failure)if configured.
onFallback is intended for recovery paths where logs should not disappear after Gateway delivery fails:
const logger = new GatewayLogger({
endpoint: "https://gateway.example.com",
token: process.env.GATEWAY_LOGGING_TOKEN!,
async onFallback(logs, failure) {
await backupLogSink.write({
reason: failure.reason,
status: failure.status,
logs,
});
},
});If onFallback throws, the SDK calls onError again with the fallback error.
Drop Reasons
onDrop receives the dropped log and one of these reasons:
| Reason | Meaning |
| --- | --- |
| queue_full | The in-memory queue reached maxQueueSize. |
| closed | Code attempted to log after close(). |
| invalid_event | Reserved for invalid event handling. |
| permanent_failure | Gateway rejected the batch with a non-retryable response. |
| retry_exhausted | Retry attempts were exhausted. |
Example:
const logger = new GatewayLogger({
endpoint: "https://gateway.example.com",
token: process.env.GATEWAY_LOGGING_TOKEN!,
onDrop(log, reason) {
console.warn(`Dropped Gateway log: ${reason}`, log);
},
});Shutdown Hooks
For long-running Node processes, install the shutdown hook so queued logs are flushed on common process shutdown signals:
import { GatewayLoggerHook } from "@wiolett/gateway-logger";
const uninstallHook = logger.hooks.install(GatewayLoggerHook.SHUTDOWN);
logger.hooks.uninstall(GatewayLoggerHook.SHUTDOWN);
// Later, if the logger lifecycle ends before the process:
uninstallHook();
await logger.close();The shutdown hook listens for SIGINT, SIGTERM, and beforeExit. Hook installation is idempotent: calling install(GatewayLoggerHook.SHUTDOWN) more than once does not register duplicate process listeners. The hook registry is intentionally exposed as logger.hooks so future SDK versions can add more hook types without adding more top-level logger methods.
Framework Examples
Express-style request middleware
app.use(async (req, res, next) => {
const startedAt = Date.now();
await logger.withContext(
{
requestId: req.headers["x-request-id"]?.toString(),
labels: {
method: req.method,
route: req.path,
},
},
async (requestLogger) => {
requestLogger.info("Request started");
res.once("finish", () => {
requestLogger.info("Request completed", {
fields: {
statusCode: res.statusCode,
durationMs: Date.now() - startedAt,
},
});
});
next();
}
);
});Worker job
export async function runJob(job: { id: string; tenantId: string }) {
await logger.withContext(
{
requestId: job.id,
labels: { worker: "invoice-sync", tenant: job.tenantId },
},
async (jobLogger) => {
jobLogger.info("Job started");
try {
await syncInvoices(job);
jobLogger.info("Job completed");
} catch (error) {
jobLogger.error("Job failed", {
fields: { error: error instanceof Error ? error.message : String(error) },
});
throw error;
} finally {
await jobLogger.flush();
}
}
);
}Data Modeling Guidance
Use labels for low-cardinality dimensions you want to filter or group by, such as app, environment, region, tenant, module, route, or provider.
Use fields for event-specific structured data, such as durations, status codes, entity IDs, error messages, payload sizes, or version information.
Avoid placing secrets, passwords, API keys, tokens, raw authorization headers, private keys, or credential material in either labels or fields. Gateway can store and search logs, so treat log content as durable operational data.
API Reference
new GatewayLogger(options)
Creates a Gateway logger instance.
import { GatewayLogger } from "@wiolett/gateway-logger";GatewayLogger class
class GatewayLogger {
readonly hooks: GatewayLoggerHooks;
constructor(options: GatewayLoggerOptions);
trace(message: string, options?: GatewayLogOptions): void;
debug(message: string, options?: GatewayLogOptions): void;
info(message: string, options?: GatewayLogOptions): void;
warn(message: string, options?: GatewayLogOptions): void;
error(message: string, options?: GatewayLogOptions): void;
fatal(message: string, options?: GatewayLogOptions): void;
log(event: GatewayLogEvent): void;
child(context: GatewayLogContext): GatewayLogger;
createTrace(context?: GatewayTraceContext): GatewayTraceLogger;
withContext<T>(context: GatewayLogContext, fn: (logger: GatewayLogger) => T | Promise<T>): Promise<T>;
flush(): Promise<void>;
close(): Promise<void>;
}Runtime Exports
GatewayLoggerGatewayLoggerHook
Exported Types
The package exports these public types:
GatewayBatchingOptionsGatewayLoggerMethodsGatewayLoggerHooksGatewayLogGatewayLogContextGatewayLogDropReasonGatewayLogEventGatewayLogFailureInfoGatewayLogFailureReasonGatewayLoggerOptionsGatewayLogOptionsGatewayLogSeverityGatewayLogValueGatewayRetryOptionsGatewaySpanContextGatewaySpanLoggerGatewayTraceContextGatewayTraceLogger
Development
From the repository root:
pnpm --filter @wiolett/gateway-logger test
pnpm --filter @wiolett/gateway-logger lint
pnpm --filter @wiolett/gateway-logger typecheck
pnpm --filter @wiolett/gateway-logger buildThe package publishes dist only.
