Observability
Every Mantle Lambda handler gets structured logging, CloudWatch metrics, and X-Ray tracing automatically — no configuration required beyond naming your operation.
What you get for free
All define*Handler factories (defineApiHandler, defineEventBridgeHandler, defineScheduledHandler) call withObservability() internally. For every invocation this provides:
- Cold start detection — emits a
ColdStartmetric on the first invocation in an execution environment - Correlation ID — extracted from the event (API Gateway request ID, EventBridge detail) or generated as a UUID; injected into every log entry automatically
- Attempt/Success metrics —
<OperationName>Attempton entry,<OperationName>Successon clean completion - X-Ray span — wraps the invocation in a span named after
operationName - Structured log context —
operationName,correlationId, andtraceIdin every log line - Error logging — structured error log with operation context on any unhandled exception
- Metrics flush —
publishStoredMetrics()runs in afinallyblock; you never need to call it yourself
import { buildValidatedResponse } from '@mantleframework/core'
import { defineApiHandler, z } from '@mantleframework/validation'
const ResponseSchema = z.object({ synced: z.number() })
const api = defineApiHandler({ auth: 'bearer', operationName: 'SyncData' })
export const handler = api(async ({ context, body }) => {
// structured logging, metrics, and tracing are already active
return buildValidatedResponse(context, 200, { synced: 0 }, ResponseSchema)
})Automatic CloudWatch metrics
| Metric | Unit | When |
|---|---|---|
ColdStart | Count | First invocation only |
<OperationName>Attempt | Count | Every invocation |
<OperationName>Success | Count | Successful completions |
Failed invocations emit Attempt but not Success — error rate is 1 - (Success / Attempt).
The metrics namespace is set from the name field in mantle.config.ts and injected as METRICS_NAMESPACE automatically.
Structured logging
The logger is pre-configured with Lambda context for every invocation:
{
"level": "INFO",
"message": "Handler invoked",
"service": "my-app",
"operationName": "SyncData",
"correlationId": "abc-123-def",
"traceId": "1-abc-def",
"timestamp": "2026-03-26T12:00:00.000Z"
}Add your own log entries with the logger singleton from @mantleframework/observability:
import { defineApiHandler } from '@mantleframework/validation'
import { logger } from '@mantleframework/observability'
const api = defineApiHandler({ auth: 'bearer', operationName: 'SyncData' })
export const handler = api(async ({ context, body }) => {
logger.info('Processing sync request', { itemCount: body.items.length })
const count = await DataQueries.upsertBatch(body.items)
logger.info('Sync complete', { synced: count })
return buildValidatedResponse(context, 200, { synced: count }, ResponseSchema)
})correlationId, traceId, and operationName are injected automatically — do not add them manually.
Custom metrics
Add domain-level metrics with the metrics singleton:
import { defineApiHandler } from '@mantleframework/validation'
import { metrics, MetricUnit } from '@mantleframework/observability'
const api = defineApiHandler({ schema: SyncSchema, auth: 'bearer', operationName: 'SyncData' })
export const handler = api(async ({ context, body }) => {
const count = await DataQueries.upsertBatch(body.items)
metrics.addMetric('ItemsSynced', MetricUnit.Count, count)
return buildValidatedResponse(context, 200, { synced: count }, ResponseSchema)
})Custom metrics are published in the same finally flush as the automatic ones.
X-Ray tracing
Each invocation is wrapped in an X-Ray span named after operationName. EventBridge handlers automatically annotate the span with detailType and eventSource.
Add custom subsegments for expensive operations:
import { getTracer } from '@mantleframework/observability'
const tracer = getTracer()
const api = defineApiHandler({ auth: 'bearer', operationName: 'ProcessData' })
export const handler = api(async ({ context }) => {
const segment = tracer.getSegment()
const subsegment = segment?.addNewSubsegment('fetchExternalData')
try {
const data = await fetchFromExternalApi()
subsegment?.close()
return buildValidatedResponse(context, 200, { processed: data.length }, ResponseSchema)
} catch (error) {
subsegment?.addError(error as Error)
subsegment?.close()
throw error
}
})Traces appear in the AWS console under X-Ray > Traces — latency, downstream calls (DSQL, S3, SQS), and error annotations are all visible there.
Package exports
Always import from @mantleframework/observability, never from the underlying Powertools packages directly.
| Export | Description |
|---|---|
logger | Configured Powertools Logger singleton |
metrics | Configured Powertools Metrics singleton |
MetricUnit | CloudWatch metric unit enum |
getTracer() | Returns the configured Powertools Tracer |
getCurrentSpan() | Returns the current active span |
logError(message, error) | Log a structured error entry |
logInfo(message, data?) | Log a structured info entry |
resolveLoggingConfig() | Returns the active logging configuration |
sanitizeData(data) | Redact sensitive fields before logging |
// CORRECT
import { logger, metrics, MetricUnit } from '@mantleframework/observability'
// FORBIDDEN
import { Logger } from '@aws-lambda-powertools/logger'
import { Metrics } from '@aws-lambda-powertools/metrics'Environment variables
The Lambda module sets these automatically — no manual configuration needed:
| Variable | Source | Description |
|---|---|---|
METRICS_NAMESPACE | mantle.config.ts name | CloudWatch metrics namespace |
OTEL_SERVICE_NAME | Lambda function name | OpenTelemetry service name |
LOG_LEVEL | Terraform variable | Logger level (INFO, DEBUG, WARN, ERROR) |
ENVIRONMENT | Terraform variable | Deployment stage (dev, staging, prod) |
Singletons use a lazy Proxy — environment variables are only read on first access, not at import time. This is safe for Lambda cold starts.
Next steps
- Handlers —
operationNameand other factory options - Deployment — how environment variables are injected at deploy time
- Vendor Encapsulation Policy — why direct Powertools imports are forbidden