npm package: @steve31415/log-logger-ts
Description: Lightweight logging library for Cloudflare Workers and browsers
Latest version: 1.5.0

# @steve31415/log-logger-ts

Lightweight logging library for Cloudflare Workers and browsers with automatic batching and dual logging (console + backend).

## Installation

```bash
npm install @steve31415/log-logger-ts
```

## Features

- **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 with `ctx.waitUntil`
- **Best-effort delivery**: `flushSync()` uses `sendBeacon` for browser page unload
- **Workers utilities**: `waitUntilFlush`, `createRequestLogger`, `withRequestLogger` for 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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
import { Logger, FetchDeliveryAdapter } 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 {
      ctx.waitUntil(logger.flushWithTimeout(3000));
    }
  }
}
```

### Cloudflare Workers (cron/queue)

```typescript
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

```typescript
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 level
- `logger.info(message, meta?)` - INFO level
- `logger.warn(message, meta?)` - WARN level
- `logger.error(message, meta?)` - ERROR level (triggers immediate flush)

### Child Loggers

```typescript
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 events
- `flushWithTimeout(ms)` - Flush with timeout, falls back to `sendBeacon` if available
- `flushSync()` - Synchronous best-effort flush using `sendBeacon` (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.

```typescript
waitUntilFlush(ctx, logger);          // Default 3000ms timeout
waitUntilFlush(ctx, logger, 5000);    // Custom timeout
```

#### `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.

```typescript
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.

```typescript
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)
}));
```

#### `getLogger(c)` / `getLoggerOrUndefined(c)`

Retrieve the logger from Hono context.

```typescript
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.

```typescript
import { serializeError } from '@steve31415/log-logger-ts';

try {
  await riskyOperation();
} catch (error) {
  logger.error('Operation failed', { error: serializeError(error) });
}
```

Output for Error instances:
```json
{
  "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:

```toml
# wrangler.toml
[[services]]
binding = "LOG_INGEST"
service = "log-ingest"
```

```typescript
// Pass to FetchDeliveryAdapter
new FetchDeliveryAdapter(endpoint, env.LOG_INGEST)

// Or use convenience functions
createRequestLogger({ app: 'my-app', logIngest: env.LOG_INGEST })
```

## License

MIT