@ubercode/winston-cloudwatch
v1.3.1
Published
A TypeScript Winston transport for Amazon CloudWatch.
Downloads
966
Maintainers
Readme
@ubercode/winston-cloudwatch
A modern TypeScript Winston transport for Amazon CloudWatch using AWS SDK v3.
Features
- TypeScript - Full TypeScript support with complete type definitions
- AWS SDK v3 - Uses the modern modular AWS SDK v3
- Rate Limiting - Built-in throttling to respect CloudWatch API limits
- Bounded Memory - Delivery is decoupled from Winston's stream; a CloudWatch outage can never stall the logger or leak memory
- Automatic Retries - Handles sequence token errors automatically
- Customizable Formatting - Flexible log formatting options
- JSON Formatting - Optional structured JSON log output
- Retention Policies - Automatic log group retention configuration
- Byte-Aware Batching - Respects the 1 MB PutLogEvents payload limit
- Graceful Shutdown - Flush pending logs before process exit
- Client Injection - Bring your own
CloudWatchLogsClient - Well Tested - 100% test coverage with Jest
Installation
npm install @ubercode/winston-cloudwatch winston
# or
yarn add @ubercode/winston-cloudwatch winston
# or
pnpm add @ubercode/winston-cloudwatch winstonUsage
JavaScript (CommonJS)
const winston = require('winston')
const CloudWatchTransport = require('@ubercode/winston-cloudwatch').default
const logger = winston.createLogger({
transports: [
new CloudWatchTransport({
logGroupName: 'my-app-logs', // REQUIRED
logStreamName: 'my-app-stream', // REQUIRED
createLogGroup: true,
createLogStream: true,
submissionInterval: 2000,
batchSize: 20,
awsConfig: {
region: 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
}
})
]
})
logger.info('Hello CloudWatch!', { userId: 123, action: 'login' })TypeScript
import winston from 'winston'
import CloudWatchTransport from '@ubercode/winston-cloudwatch'
const logger = winston.createLogger({
transports: [
new CloudWatchTransport({
logGroupName: 'my-app-logs',
logStreamName: 'my-app-stream',
createLogGroup: true,
createLogStream: true,
awsConfig: {
region: 'us-east-1'
}
})
]
})
logger.info('Hello CloudWatch!', { userId: 123, action: 'login' })Configuration Options
| Option | Type | Required | Default | Description |
|----------------------|------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------|
| name | string | No | cloudwatch| Transport name used by Winston to identify this transport |
| logGroupName | string | Yes | - | CloudWatch log group name (1-512 characters) |
| logStreamName | string | Yes | - | CloudWatch log stream name (1-512 characters) |
| awsConfig | CloudWatchLogsClientConfig | No | {} | AWS SDK v3 client configuration. Ignored when cloudWatchLogs is provided |
| cloudWatchLogs | CloudWatchLogsClient | No | - | Pre-built AWS SDK client. When provided, awsConfig is ignored and the client is not destroyed on close |
| createLogGroup | boolean | No | false | Automatically create log group if it doesn't exist |
| createLogStream | boolean | No | false | Automatically create log stream if it doesn't exist |
| retentionInDays | RetentionInDays | No | - | Set the retention policy on the log group (e.g. 1, 7, 30, 90, 365). Works on pre-existing groups |
| timeout | number | No | 10000 | Timeout in ms for each AWS SDK call |
| maxEventSize | number | No | 1048576 | Max event size in bytes (including 26-byte overhead). Messages exceeding the limit are truncated |
| jsonMessage | boolean | No | false | Format log messages as JSON objects. Ignored if formatLog or formatLogItem is provided |
| formatLog | function | No | - | Custom function to format log messages. Takes precedence over formatLogItem |
| formatLogItem | function | No | - | Custom function to format log items (message + timestamp). Ignored if formatLog is provided |
| submissionInterval | number | No | 2000 | Milliseconds between batch submissions |
| batchSize | number | No | 20 | Maximum number of logs per batch |
| maxQueueSize | number | No | 10000 | Maximum queued log items (oldest dropped when full) |
| maxRetries | number | No | 10 | Consecutive failed delivery attempts of the head batch before it is dropped (frees head-of-line) |
| retryBackoffCap | number | No | 30000 | Upper bound (ms) on the exponential backoff between failed retries. 0 disables backoff |
| level | string | No | - | Minimum log level for this transport (inherited from Winston) |
| silent | boolean | No | false | Suppress all output (inherited from Winston) |
| handleExceptions | boolean | No | false | Handle uncaught exceptions (inherited from Winston) |
Custom Formatting
new CloudWatchTransport({
logGroupName: 'my-app',
logStreamName: 'my-stream',
formatLog: (item) => {
const meta = item.meta ? ` ${JSON.stringify(item.meta)}` : ''
return `[${item.level}] ${item.message}${meta}`
}
})Graceful Shutdown
close() is asynchronous: it performs a best-effort flush of any queued logs
(bounded by the flush timeout) before stopping the relay. For a graceful
shutdown, await it so pending logs get a chance to ship:
await transport.close()Winston also calls close() automatically when the logger ends, but it does
not await it. If you exit the process immediately (e.g. process.exit()),
await the flush yourself first:
await transport.flush() // drain the queue (default timeout: 10 seconds)
await transport.close()You can also specify a custom flush timeout in milliseconds:
await transport.flush(5000) // wait up to 5 seconds
await transport.close()Logs that cannot be delivered before shutdown (or that are evicted when the
queue is full) are reported to Winston as not delivered and silently dropped.
They are not raised as error events, so a missed log on shutdown will
never crash your process.
Backpressure & Delivery Semantics
CloudWatch delivery is decoupled from Winston's writable stream. When you log, the entry is accepted into a bounded in-memory queue and Winston's write callback is resolved immediately — delivery to CloudWatch then happens asynchronously in rate-limited batches.
This is deliberate. If the write callback were deferred until CloudWatch
confirmed delivery, any persistent delivery failure (throttling, request
timeouts, missing IAM permissions, a CloudWatch outage) would stall Winston's
serialized objectMode stream at the head-of-line: every subsequent log would
accumulate, unbounded, in the stream's internal buffer until the process ran
out of memory. (This is the long-standing leak inherited from the original
winston-cloudwatch / winston-aws-cloudwatch lineage — see
#9.)
Practical implications:
- Memory is strictly bounded by
maxQueueSize(default10000), regardless of CloudWatch availability. When the queue is full the oldest queued log is dropped (reported to Winston as not delivered, never as anerror). logger.info(...)returning does not mean the log reached CloudWatch — only that it was accepted into the queue. Genuine delivery failures are surfaced via the transport'serrorevent, not via the logging call.- For maximum delivery on shutdown,
await transport.flush()/await transport.close()(see Graceful Shutdown). - During a persistent outage a failing batch is retried with exponential
backoff (capped by
retryBackoffCap) and, aftermaxRetriesconsecutive failures, dropped — so an undeliverable head batch never head-of-line blocks newer logs. Each failed attempt is surfaced as anerrorevent.
Tune the buffer with maxQueueSize, batchSize, submissionInterval,
maxRetries, and retryBackoffCap.
Migration Guides
Coming from another CloudWatch Winston transport? See our migration guides:
- Migrating from
winston-cloudwatch(lazywithclass) - Migrating from
winston-aws-cloudwatch(timdp/pascencio)
Error Handling
The transport emits an error event only when a CloudWatch submission fails
with an unrecoverable error. Dropped logs (queue overflow or shutdown) are
not emitted as errors, so they cannot crash a process that has no error
listener. It's still recommended to subscribe to this event so genuine
CloudWatch failures are surfaced:
const transport = new CloudWatchTransport({
logGroupName: 'my-app',
logStreamName: 'my-stream'
})
transport.on('error', (error) => {
console.error('CloudWatch logging error:', error)
})
const logger = winston.createLogger({ transports: [transport] })AWS Credentials
This library uses AWS SDK v3, which supports multiple authentication methods:
- Environment Variables:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION - AWS Config Files:
~/.aws/credentialsand~/.aws/config - IAM Roles: Automatic in EC2, ECS, Lambda
- Explicit Config: Pass credentials in
awsConfig
// Option 1: Use environment variables (recommended)
new CloudWatchTransport({
logGroupName: 'my-app',
logStreamName: 'my-stream',
awsConfig: { region: 'us-east-1' }
})
// Option 2: Explicit credentials (not recommended for production)
new CloudWatchTransport({
logGroupName: 'my-app',
logStreamName: 'my-stream',
awsConfig: {
region: 'us-east-1',
credentials: {
accessKeyId: 'YOUR_ACCESS_KEY',
secretAccessKey: 'YOUR_SECRET_KEY'
}
}
})Why This Fork?
This is a modernized TypeScript fork of winston-aws-cloudwatch with the following improvements:
- Full TypeScript rewrite with proper types
- Updated to AWS SDK v3 (modular, tree-shakeable)
- Modern testing with Jest (replacing Mocha)
- Updated dependencies and security fixes
- Better error handling and type safety
Requirements
- Node.js >= 20.9.0
- Winston ^3.0.0
Development
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests with coverage
pnpm test:cover
# Sustained memory soak (node --expose-gc; not part of `pnpm test` or CI)
pnpm test:stress
# Build
pnpm build
# Lint
pnpm lint
# Format
pnpm formatThis project follows the mission-critical TypeScript standard documented in
docs/CodingStandards.md.
License
MIT
Original Author
Original package by Tim De Pauw
Contributors
TypeScript modernization and AWS SDK v3 migration by Michael Lee Hobbs
