Skip to content

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 ColdStart metric 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>Attempt on entry, <OperationName>Success on clean completion
  • X-Ray span — wraps the invocation in a span named after operationName
  • Structured log contextoperationName, correlationId, and traceId in every log line
  • Error logging — structured error log with operation context on any unhandled exception
  • Metrics flushpublishStoredMetrics() runs in a finally block; you never need to call it yourself
typescript
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

MetricUnitWhen
ColdStartCountFirst invocation only
<OperationName>AttemptCountEvery invocation
<OperationName>SuccessCountSuccessful 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:

json
{
  "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:

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

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

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

ExportDescription
loggerConfigured Powertools Logger singleton
metricsConfigured Powertools Metrics singleton
MetricUnitCloudWatch 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
typescript
// 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:

VariableSourceDescription
METRICS_NAMESPACEmantle.config.ts nameCloudWatch metrics namespace
OTEL_SERVICE_NAMELambda function nameOpenTelemetry service name
LOG_LEVELTerraform variableLogger level (INFO, DEBUG, WARN, ERROR)
ENVIRONMENTTerraform variableDeployment 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