@steve31415/log-logger-ts
v1.8.1
Published
Lightweight logging library for Cloudflare Workers and browsers
Maintainers
Readme
@steve31415/log-logger-ts
Lightweight logging library for Cloudflare Workers and browsers with automatic batching and dual logging (console + backend).
Installation
npm install @steve31415/log-logger-tsFeatures
- Dual logging: Logs to console immediately and batches for backend delivery
- Automatic batching: Flushes on 10 events, 1 second timeout, or any ERROR (whichever first)
- Timeout-based flush:
flushWithTimeout(ms)for Workers withctx.waitUntil - Best-effort delivery:
flushSync()usessendBeaconfor browser page unload - Workers utilities:
waitUntilFlush,createRequestLogger,withRequestLoggerfor easy setup - Hono middleware: Built-in middleware for Hono framework
- Service binding support: Pass service bindings instead of using global fetch
- Error serialization:
serializeError()utility for logging errors with full context
Usage
Cloudflare Workers (recommended - with service binding)
For Cloudflare Workers, use a service binding for LOG_INGEST to route requests internally:
import { createRequestLogger, waitUntilFlush } from '@steve31415/log-logger-ts';
// wrangler.toml: [[services]] binding = "LOG_INGEST" service = "log-ingest"
export default {
async fetch(request, env, ctx) {
const logger = createRequestLogger({
app: 'my-app',
logIngest: env.LOG_INGEST,
});
try {
logger.info('Request received', { url: request.url });
// ... handle request
return new Response('OK');
} finally {
waitUntilFlush(ctx, logger);
}
}
}Cloudflare Workers (with handler wrapper)
The withRequestLogger wrapper automatically sets up logging, generates run IDs, and handles flushing:
import { withRequestLogger } from '@steve31415/log-logger-ts';
type Env = { LOG_INGEST: ServiceBindingLike };
const handler = withRequestLogger<Env>(
async (request, env, ctx, logger) => {
logger.info('Processing request', { url: request.url });
return new Response('OK');
},
{
app: 'my-app',
getLogIngest: (env) => env.LOG_INGEST,
}
);
export default { fetch: handler };Cloudflare Workers (with Hono)
Use the built-in Hono middleware for seamless integration:
import { Hono } from 'hono';
import { honoLoggerMiddleware, getLogger } from '@steve31415/log-logger-ts';
type Env = { Bindings: { LOG_INGEST: ServiceBindingLike } };
const app = new Hono<Env>();
app.use('*', honoLoggerMiddleware({
app: 'my-app',
getLogIngest: (env) => env.LOG_INGEST,
}));
app.get('/', (c) => {
const logger = getLogger(c);
logger.info('Hello from route handler');
return c.text('Hello!');
});
export default app;Cloudflare Workers (manual setup)
For full control, create the logger manually:
import { Logger, FetchDeliveryAdapter, waitUntilFlush } from '@steve31415/log-logger-ts';
const INGEST_ENDPOINT = 'https://log-ingest.example.com/v1/logs:ingest';
export default {
async fetch(request, env, ctx) {
// Pass service binding as second argument
const logger = new Logger(
{ app: 'my-app', context: 'worker_http' },
new FetchDeliveryAdapter(INGEST_ENDPOINT, env.LOG_INGEST)
);
try {
logger.info('Request received', { url: request.url });
// ... handle request
return new Response('OK');
} finally {
// Prefer waitUntilFlush over a bare ctx.waitUntil(logger.flushWithTimeout(...)):
// it also enables post-response mode, so logs emitted later (e.g. from a
// ctx.waitUntil continuation) are still delivered.
waitUntilFlush(ctx, logger);
}
}
}Cloudflare Workers (cron/queue)
import { createRequestLogger, waitUntilFlush } from '@steve31415/log-logger-ts';
export default {
async scheduled(event, env, ctx) {
const logger = createRequestLogger({
app: 'my-app',
logIngest: env.LOG_INGEST,
context: 'worker_cron',
runId: crypto.randomUUID(),
});
try {
logger.info('Cron started');
// ... do work
logger.info('Cron completed');
} finally {
waitUntilFlush(ctx, logger);
}
}
}Browser
import { Logger, BrowserDeliveryAdapter } from '@steve31415/log-logger-ts';
const logger = new Logger(
{ app: 'my-app', context: 'web_frontend' },
new BrowserDeliveryAdapter('/api/logs')
);
// Flush on page visibility change (uses sendBeacon for reliability)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
logger.flushSync();
}
});API
Logger Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| app | string | required | Application name (see reserved names below) |
| context | string | required | Execution context (e.g., worker_http, web_frontend) |
| runId | string | optional | Correlation ID for invocation/session |
| logger | string | optional | Module/subsystem name |
| batchSize | number | 10 | Max events before flush |
| batchTimeoutMs | number | 1000 | Max time before flush (ms) |
| consoleLog | boolean | true | Enable console logging |
| minLevel | LogLevel | 'DEBUG' | Minimum level to log |
Reserved App Names
The app name log is reserved. When app: 'log' is used, the logger writes only to the native console and skips backend delivery entirely. This prevents recursive logging when the logging infrastructure itself needs to log.
Log Levels
logger.debug(message, meta?)- DEBUG levellogger.info(message, meta?)- INFO levellogger.warn(message, meta?)- WARN levellogger.error(message, meta?)- ERROR level (triggers immediate flush)
Child Loggers
const logger = new Logger({ app: 'my-app', context: 'worker_http' }, adapter);
const dbLogger = logger.child('database');
const authLogger = logger.child('auth');
dbLogger.info('Query executed'); // Logs as [my-app:database]
authLogger.warn('Token expired'); // Logs as [my-app:auth]Flush Methods
flush()- Async flush of buffered eventsflushWithTimeout(ms)- Flush with timeout, falls back tosendBeaconif availableflushSync()- Synchronous best-effort flush usingsendBeacon(for page unload)close()- Flush and prevent further logging
Workers Utilities
waitUntilFlush(ctx, logger, timeoutMs?)
Safely schedule a logger flush using ctx.waitUntil. This is the recommended way to ensure logs are delivered before the worker terminates.
waitUntilFlush(ctx, logger); // Default 3000ms timeout
waitUntilFlush(ctx, logger, 5000); // Custom timeoutAlso puts the logger (and its child() loggers) into post-response mode: any
event logged afterwards — e.g. from a ctx.waitUntil continuation the route
registered — is flushed immediately, with the flush registered on
ctx.waitUntil so the invocation stays alive until delivery completes. Batch
timers never fire once the response is sent and pending waitUntil promises
settle, so without this such events would be silently dropped (since v1.8.0;
on older versions, background work had to call flushWithTimeout itself).
honoLoggerMiddleware and withRequestLogger call waitUntilFlush
internally, so post-response logging works there with no extra code.
createRequestLogger(options)
Create a logger configured for request handling. Returns a Logger instance.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| app | string | required | Application name |
| logIngest | ServiceBindingLike | required | Service binding for log ingest |
| context | string | 'worker_http' | Execution context |
| runId | string | optional | Correlation ID |
| endpoint | string | default endpoint | Custom ingest URL |
| config | object | {} | Additional Logger config |
withRequestLogger(handler, options)
Wrap a Workers fetch handler with automatic logger setup. The logger is injected as the fourth parameter.
const handler = withRequestLogger<Env>(
async (request, env, ctx, logger) => {
// logger is automatically created and flushed
return new Response('OK');
},
{
app: 'my-app',
getLogIngest: (env) => env.LOG_INGEST,
generateRunId: () => crypto.randomUUID(), // optional
flushTimeoutMs: 3000, // optional
}
);Hono Middleware
honoLoggerMiddleware(options)
Create Hono middleware that sets up logging for each request.
app.use('*', honoLoggerMiddleware({
app: 'my-app',
getLogIngest: (env) => env.LOG_INGEST,
logRequestStart: true, // Log "Request started" (default: true)
logRequestComplete: true, // Log "Request completed" (default: true)
}));Every auto-emitted request log (Request started / Request completed / Request failed) includes standard metadata pulled from the incoming request, so bot floods, scrapers, and abuse patterns are triageable from logs alone: method, url, path, query, host, userAgent, referer, ip (CF-Connecting-IP), country (CF-IPCountry), asn (CF-IPASN), cfRay (CF-Ray), acceptLanguage, accept. Completion and failure events also include durationMs and the response status.
getLogger(c) / getLoggerOrUndefined(c)
Retrieve the logger from Hono context.
app.get('/api', (c) => {
const logger = getLogger(c); // Throws if middleware not configured
// or
const logger = getLoggerOrUndefined(c); // Returns undefined if not available
});Error Serialization
serializeError(error)
Serialize an error for logging. Extracts useful properties from Error objects and handles non-Error values.
import { serializeError } from '@steve31415/log-logger-ts';
try {
await riskyOperation();
} catch (error) {
logger.error('Operation failed', { error: serializeError(error) });
}Output for Error instances:
{
"name": "TypeError",
"message": "Cannot read property 'x' of undefined",
"stack": "TypeError: Cannot read property...",
"cause": { ... }, // If present (ES2022)
"customProp": "value" // Any additional enumerable properties
}Service Bindings
For Cloudflare Workers, always use a service binding instead of global fetch:
# wrangler.toml
[[services]]
binding = "LOG_INGEST"
service = "log-ingest"// Pass to FetchDeliveryAdapter
new FetchDeliveryAdapter(endpoint, env.LOG_INGEST)
// Or use convenience functions
createRequestLogger({ app: 'my-app', logIngest: env.LOG_INGEST })License
MIT
