Observability
Observability is thinner than the rest of the infrastructure, and knowing exactly where the gaps are is more useful than a list of features.
| Signal | Status | Notes |
|---|---|---|
Structured logs |
Active |
CloudWatch, per function, no retention policy |
Sentry error reporting |
Disabled |
|
X-Ray tracing |
Off |
No |
CloudWatch alarms |
One |
Bedrock token consumption |
Application Insights |
Auto-configured |
On the SAM stack resource group |
Dashboards |
None |
— |
Sentry
libs/morpheus-common/morpheus_common/sentry.py exposes configure(), and every Lambda
handler calls it at cold start before anything else — including before load_secrets(), so a
failure to load the secret is itself reportable.
| Environment variable | Behaviour |
|---|---|
|
Empty or absent → |
|
|
|
Optional, passed through to |
|
Documented as |
Integrations: AwsLambdaIntegration(timeout_warning=True) and
LoggingIntegration(level=logging.INFO, event_level=logging.ERROR). send_default_pii is
False.
|
Until it is set, an unhandled exception exists only as a CloudWatch log line. Nothing pages, nothing aggregates, and a failure that lands in a dead-letter queue is invisible until someone looks at the queue. |
The two configured integrations are worth understanding for when it is turned on:
timeout_warning=True reports a Lambda approaching its timeout — the failure mode that
otherwise produces no exception at all, just a truncated invocation. And the logging
integration’s split levels mean logger.info() calls become breadcrumbs attached to an
event, while logger.error() creates the event. That is why the convention is
logger.error(…, exc_info=True) on every caught exception: it is what carries the stack
trace into the report.
send_default_pii=False matters here specifically. These handlers process contact names,
email addresses, and phone numbers; the default-on setting would attach request data to every
event. See PII.
CloudWatch logs
| Source | Log group | Retention |
|---|---|---|
Lambda functions |
|
Never expires |
AgentCore agent |
|
One month |
AgentCore platform |
|
Platform-managed |
|
The SAM template sets no retention on any Lambda log group, so they default to never expiring
and accumulate cost indefinitely. The CDK log group does set |
Logging conventions the pages elsewhere depend on:
-
logger = logging.getLogger(name), neverprint(). -
Structured fields as
key=valuein the message string —"sellerId=%r stage=%r"— so CloudWatch Logs Insights can filter on them. -
Never log a full invocation payload. They carry PII.
-
logger.debug()for per-record detail, which theLogLevelstack parameter turns on without a code change. See SAM template.
Tailing a function:
aws logs tail /aws/lambda/morpheus-sam-hubspot-sync --follow --region us-east-1
More query recipes in Operations and runbooks.
X-Ray
Not enabled. The SAM Globals block has no Tracing: Active, so no function is traced.
The AgentCore runtime role does carry xray:PutTraceSegments, PutTelemetryRecords,
GetSamplingRules, and GetSamplingTargets. Whether traces are produced depends on the
AgentCore platform’s own configuration rather than on anything in this repository.
Tracing would be most valuable on the paths that cross services — a CrmSyncQueue message
produced by enrichment-scorer and consumed by morpheus-hubspot currently has no correlation
identifier tying the two invocations together. Following one seller through the pipeline means
grepping two log groups on a shared business id.
The one alarm
| Property | Value |
|---|---|
Name |
|
Metric |
|
Statistic |
|
Threshold |
|
Evaluation periods |
1 |
Missing data |
|
Action |
SNS |
It is a cost guard, not a health check: it fires when something is consuming far more Bedrock input tokens than normal — a runaway retry loop, an agent given an oversized context, a backfill schedule re-enabled at a development cadence.
|
The SNS topic |
TreatMissingData: notBreaching is right for this metric — no Bedrock invocations means no
data points, and quiet is not a problem here.
Application Insights
ApplicationResourceGroup groups the SAM stack’s resources, and
ApplicationInsightsMonitoring enables AutoConfigurationEnabled: true over it. AWS then
provisions its own default Lambda and SQS monitors and a dashboard.
Because it is auto-configured, what it watches is not declared anywhere in this repository, and it changes as AWS updates its defaults. Treat it as a convenience layer rather than as the monitoring plan.
Gaps
Ordered by how likely each is to matter:
-
Sentry disabled. Setting a real
SENTRY_DSNis the single highest-value change available — it converts silent failures into alerts. -
No DLQ alarms. All three dead-letter queues can fill without notifying anyone. An alarm on
ApproximateNumberOfMessagesVisible > 0for each is straightforward. -
No queue-depth or message-age alarms. With
enrichment-scorercurrently untriggered,EnrichmentPipelineQueuegrows until messages expire at four days, and nothing reports it. See Queues and dead-letter queues. -
No Lambda error-rate or throttle alarms.
ReservedConcurrentExecutionsmakes throttling a real possibility onphone-agent, which is capped at 2. -
No log retention on Lambda groups. Unbounded storage growth.
-
No schedule-failure signal. A schedule whose
detail-typeno longer matches a registry entry fires successfully into a handler that does not exist. The observable symptom is a watermark inconfigsthat stops advancing — see Runtime state inconfigs.
Debugging without alarms
Given the above, the practical order when something looks wrong:
-
Check the relevant DLQ depth. A message there carries the failing payload.
-
Check
updatedAton the relevantconfigswatermark. A stale one names the schedule that stopped completing. -
Check
ApproximateAgeOfOldestMessageon the main queue. A growing age means the consumer is disabled, throttled, or erroring. -
Only then read logs, filtered on the business id you already have from steps 1–3.
The runbooks are in Operations and runbooks.