@ubercode/pino-cloudwatch
v1.0.1
Published
A TypeScript pino v7+ transport for Amazon CloudWatch Logs.
Maintainers
Readme
@ubercode/pino-cloudwatch
A modern TypeScript pino v7+ transport for Amazon CloudWatch Logs, built on AWS SDK v3.
This is the actively-maintained successor to the original pino-cloudwatch, rebuilt from the ground up in TypeScript. It replaces the legacy stdin-pipe CLI (AWS SDK v2, sequence-token protocol) with a worker-thread pino-abstract-transport, and inherits the mission-critical batching/throttling/bounded-memory core from its sibling @ubercode/winston-cloudwatch.
Project status. The original
pino-cloudwatchis unmaintained — no releases or issue activity in years.@ubercode/pino-cloudwatchis its de-facto continuation: every open upstream issue and PR has been triaged and addressed (seedocs/upstream-issue-audit.md), and bug reports and feature requests are tracked in this repository going forward.
Features
- pino v7+ transport — runs in pino's worker thread; only records emitted through pino are shipped (your
console.logis never captured) - AWS SDK v3 — modular, tree-shakeable; no sequence-token handshake
- Bounded memory — a CloudWatch outage can never stall pino or leak memory; the queue is strictly bounded (oldest-dropped)
- Resilient delivery — rate-limited batches, exponential-backoff retry, and head-of-line drop so a persistent outage never wedges the pipeline
- Byte-aware batching — respects the 1 MB
PutLogEventspayload limit - Graceful flush — drains queued logs on shutdown via the transport
closehook - Flexible formatting — default
[LEVEL] message {meta}, optionaljsonMessage, or fully custom - Dynamic stream names —
{hostname}/{pid}/{date}/{time}tokens - 100% test coverage with Jest, plus a sustained memory soak
Installation
npm install @ubercode/pino-cloudwatch pino
# or: pnpm add @ubercode/pino-cloudwatch pinoUsage
Recommended: worker-thread transport
import pino from 'pino'
const logger = pino({
transport: {
target: '@ubercode/pino-cloudwatch',
options: {
logGroupName: '/my-app/logs', // REQUIRED
logStreamName: 'production', // optional; defaults to "<hostname>-<pid>"
createLogGroup: true,
createLogStream: true,
awsConfig: { region: 'us-east-1' },
},
},
})
logger.info({ userId: 123, action: 'login' }, 'Hello CloudWatch!')Because pino runs the transport in a worker thread, the options object is
structured-cloned across the thread boundary, so it must be JSON-serializable.
Functions (formatLog, formatLogItem, a credential-provider function) and a
pre-built cloudWatchLogs client cannot be passed this way — use the
in-process form below for those.
Advanced: in-process (supports functions & custom clients)
pino accepts a destination stream as its second argument. This runs the transport in the main thread, so callbacks and a bring-your-own client work:
import pino from 'pino'
import pinoCloudWatch from '@ubercode/pino-cloudwatch'
const stream = pinoCloudWatch({
logGroupName: '/my-app/logs',
logStreamName: 'production',
formatLog: item => `[${item.level}] ${item.message}`,
awsConfig: { region: 'us-east-1', credentials: myCredentialProvider },
})
const logger = pino({ level: 'info' }, stream)Configuration Options
| Option | Type | Required | Default | Description |
|----------------------|------------------------------|----------|----------------|----------------------------------------------------------------------------------------------|
| logGroupName | string | Yes | – | CloudWatch log group name (1–512 chars) |
| logStreamName | string | No | <hostname>-<pid> | Log stream name (1–512 chars). Supports {hostname} {pid} {date} {time} tokens |
| awsConfig | CloudWatchLogsClientConfig | No | {} | AWS SDK v3 client config (region, endpoint, credentials, …). Ignored if cloudWatchLogs is set |
| cloudWatchLogs | CloudWatchLogsClient | No | – | Pre-built AWS SDK client (in-process usage only). Not destroyed on close |
| createLogGroup | boolean | No | false | Auto-create the log group on first submission |
| createLogStream | boolean | No | false | Auto-create the log stream on first submission |
| retentionInDays | RetentionInDays | No | – | Set the log-group retention policy (e.g. 7, 30, 365) |
| timeout | number | No | 10000 | Timeout (ms) for each AWS SDK call |
| maxEventSize | number | No | 1048576 | Max event size in bytes (incl. 26-byte overhead); longer messages are truncated |
| jsonMessage | boolean | No | false | Emit each event as a JSON object. Ignored if formatLog/formatLogItem is set |
| formatLog | (item) => string | No | – | Custom message formatter (in-process only). Takes precedence over formatLogItem |
| formatLogItem | (item) => {message,timestamp} | No | – | Custom message+timestamp formatter (in-process only) |
| levelLabels | Record<number,string> | No | pino defaults | Override the numeric-level → label map (merged over 10..60) |
| messageKey | string | No | 'msg' | Record key holding the message. Set to match a custom pino messageKey |
| timestampKey | string | No | 'time' | Record key holding the timestamp. Set to match a custom pino timestamp key |
| levelKey | string | No | 'level' | Record key holding the level. Set to match a custom pino levelKey |
| onError | (error) => void | No | stderr warning | Delivery-failure reporter (in-process only). Default writes one line to stderr |
| submissionInterval | number | No | 2000 | Minimum ms between batch submissions |
| batchSize | number | No | 20 | Max log events per batch |
| maxQueueSize | number | No | 10000 | Max queued events (oldest dropped when full) |
| maxRetries | number | No | 10 | Consecutive head-batch failures before the batch is dropped (frees head-of-line) |
| retryBackoffCap | number | No | 30000 | Upper bound (ms) on exponential backoff between retries; 0 disables backoff |
Backpressure & Delivery Semantics
CloudWatch delivery is decoupled from pino's log stream. Each record is accepted into a bounded in-memory queue and the stream keeps draining; delivery to CloudWatch then happens asynchronously in rate-limited batches.
This is deliberate. If delivery were coupled to the inbound stream, any
persistent failure (throttling, timeouts, missing IAM, a CloudWatch outage,
or an EMFILE/ulimit storm) would stall the pipeline head-of-line and buffer
every later log unbounded until the process ran out of memory — the failure
mode reported upstream in
#36 and
#37.
Practical implications:
- Memory is strictly bounded by
maxQueueSize, regardless of CloudWatch availability. When full, the oldest queued event is dropped. - A logging call returning does not mean the log reached CloudWatch — only
that it was queued. Genuine delivery failures go to
onError(orstderr). - During a persistent outage a failing batch is retried with exponential
backoff and, after
maxRetriesconsecutive failures, dropped — so an undeliverable head batch never blocks newer logs.
Graceful Shutdown / Flush
The transport implements pino's async close hook: on teardown it performs a
best-effort flush of the queue (bounded by the flush timeout) before stopping.
With a worker transport, await logger.flush() and let the process end so pino
tears the worker down cleanly; the transport drains on close.
AWS Credentials
AWS SDK v3 resolves credentials from the standard chain (env vars, shared
config files, IAM roles for EC2/ECS/Lambda). In a worker transport the
chain runs inside the worker, so IAM roles and assumed-role-via-config work
automatically. For a programmatic credential provider (e.g. a refreshing
assumed-role provider — upstream
#35), use the
in-process form and pass it as awsConfig.credentials.
Security considerations
This transport accepts trusted, developer-supplied configuration and ships log content to AWS. A few notes for hardened/multi-tenant deployments:
awsConfig.endpointmust be a trusted HTTPS URL. It is passed straight to the AWS SDK client; a value sourced from untrusted input could redirect log batches to an attacker-controlled host (SSRF), and a plainhttp://endpoint sends log content in clear text. Never populate it from untrusted data.- The default
onErrorwrites the raw provider error message tostderrso failures aren't silent. Provider messages can include operational metadata (endpoint, region, request id) — not your secret key, which the SDK never echoes. On a shared host, supply your ownonErrorto control this output. DEBUG=pino-cloudwatch:*logs option objects, which may includeawsConfig.credentials. Enable debug logging only in trusted environments.- Log message and metadata are written verbatim (not sanitized for terminal rendering); downstream viewers that interpret ANSI/newlines are the consumer's responsibility.
Migration
Coming from the original pino-cloudwatch? See
docs/migration-from-pino-cloudwatch.md.
Every open issue and PR from the upstream project and how this rewrite
addresses it is catalogued in
docs/upstream-issue-audit.md.
Requirements
- Node.js >= 20.9.0
- pino ^8 || ^9
Development
pnpm install
pnpm test # format + lint + unit (100% coverage required)
pnpm test:stress # sustained memory soak (node --expose-gc; not in CI)
pnpm buildThis project follows the mission-critical TypeScript standard in
docs/CodingStandards.md.
License
MIT — see LICENSE.
Maintenance & credits
This package is the actively-maintained successor to the original
pino-cloudwatch, which is no longer maintained. File bug reports and feature
requests against this repository.
The original pino-cloudwatch by David Howell is
gratefully acknowledged — its design informed this work. TypeScript v7-transport
rewrite, AWS SDK v3 migration, and ongoing maintenance by
Michael Lee Hobbs.
